[Ovirt-devel] [PATCH node] Introduces the node-admin toolset.

Darryl L. Pierce dpierce at redhat.com
Fri Sep 11 18:41:26 UTC 2009


Defines a primary entry point that displays a menu of options that user
can use to administer the managed node. It leverages the code from
virtinst-python to do the heavy lifting and just provides a front end
for collecting user data.

Created a new configuration class to be used for all configuration
screens.

The user can select to create a node. They are then walked through the
steps to create a domain similar to virt-manager.

The user can create a defined domain.

The user can destroy a created domain.

The user can undefine a defined domain.

The user can list all domains on the system.

The user can create a new user account.

When the RPM is created, a separate entry point is created for each
node admin command. This allows the commands to be invoked individually
as well as from the main menu.

Signed-off-by: Darryl L. Pierce <dpierce at redhat.com>
---
 .gitignore                  |    1 +
 Makefile.am                 |   15 ++
 configure.ac                |    1 +
 nodeadmin/__init__.py       |   20 ++
 nodeadmin/configscreen.py   |  150 ++++++++++++++
 nodeadmin/createdomain.py   |   68 +++++++
 nodeadmin/createuser.py     |  106 ++++++++++
 nodeadmin/definedomain.py   |  470 +++++++++++++++++++++++++++++++++++++++++++
 nodeadmin/destroydomain.py  |   66 ++++++
 nodeadmin/domainconfig.py   |  217 ++++++++++++++++++++
 nodeadmin/halworker.py      |   37 ++++
 nodeadmin/libvirtworker.py  |  271 +++++++++++++++++++++++++
 nodeadmin/listdomains.py    |   68 +++++++
 nodeadmin/mainmenu.py       |   71 +++++++
 nodeadmin/nodeadmin.py      |   29 +++
 nodeadmin/setup.py.in       |   34 +++
 nodeadmin/undefinedomain.py |   83 ++++++++
 nodeadmin/userworker.py     |   38 ++++
 ovirt-node.spec.in          |   37 ++++
 19 files changed, 1782 insertions(+), 0 deletions(-)
 create mode 100644 nodeadmin/__init__.py
 create mode 100644 nodeadmin/configscreen.py
 create mode 100755 nodeadmin/createdomain.py
 create mode 100755 nodeadmin/createuser.py
 create mode 100755 nodeadmin/definedomain.py
 create mode 100755 nodeadmin/destroydomain.py
 create mode 100644 nodeadmin/domainconfig.py
 create mode 100644 nodeadmin/halworker.py
 create mode 100644 nodeadmin/libvirtworker.py
 create mode 100755 nodeadmin/listdomains.py
 create mode 100755 nodeadmin/mainmenu.py
 create mode 100755 nodeadmin/nodeadmin.py
 create mode 100644 nodeadmin/setup.py.in
 create mode 100755 nodeadmin/undefinedomain.py
 create mode 100644 nodeadmin/userworker.py

diff --git a/.gitignore b/.gitignore
index 26a0210..19b15d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ missing
 stamp-h1
 ovirt-node*.gz
 ovirt-node.spec
+*.pyc
diff --git a/Makefile.am b/Makefile.am
index 419cdf1..5192d9d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -26,6 +26,21 @@ EXTRA_DIST =			\
   ovirt-node-selinux.fc		\
   images/grub-splash.xpm.gz	\
   images/syslinux-vesa-splash.jpg	\
+  nodeadmin/__init__.py         \
+  nodeadmin/configscreen.py     \
+  nodeadmin/createuser.py       \
+  nodeadmin/destroydomain.py    \
+  nodeadmin/halworker.py        \
+  nodeadmin/libvirtworker.py    \
+  nodeadmin/userworker.py       \
+  nodeadmin/mainmenu.py         \
+  nodeadmin/undefinedomain.py   \
+  nodeadmin/createdomain.py     \
+  nodeadmin/definedomain.py     \
+  nodeadmin/domainconfig.py     \
+  nodeadmin/listdomains.py      \
+  nodeadmin/nodeadmin.py        \
+  nodeadmin/setup.py            \
   scripts/collectd.conf.in	\
   scripts/ovirt			\
   scripts/ovirt-awake		\
diff --git a/configure.ac b/configure.ac
index b60afeb..780b757 100644
--- a/configure.ac
+++ b/configure.ac
@@ -8,6 +8,7 @@ test x"$ac_ct_CC:$CFLAGS" = 'xgcc:-g -O2' \
   && CFLAGS="$CFLAGS -Wshadow -Wall -Werror"
 
 AC_CONFIG_FILES([Makefile
+  nodeadmin/setup.py
   gptsync/Makefile
   ovirt-node.spec
   ])
diff --git a/nodeadmin/__init__.py b/nodeadmin/__init__.py
new file mode 100644
index 0000000..1f3c72c
--- /dev/null
+++ b/nodeadmin/__init__.py
@@ -0,0 +1,20 @@
+# __init__.py - Copyright (C) 2009 Red Hat, Inc.
+# Written by Darryl L. Pierce <dpierce at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+from nodeadmin import NodeAdmin
+
diff --git a/nodeadmin/configscreen.py b/nodeadmin/configscreen.py
new file mode 100644
index 0000000..0282eee
--- /dev/null
+++ b/nodeadmin/configscreen.py
@@ -0,0 +1,150 @@
+# configscreen.py - Copyright (C) 2009 Red Hat, Inc.
+# Written by Darryl L. Pierce <dpierce at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+from snack import *
+from halworker import HALWorker
+from libvirtworker import LibvirtWorker
+import traceback
+
+BACK_BUTTON   = "back"
+NEXT_BUTTON   = "next"
+CANCEL_BUTTON = "cancel"
+FINISH_BUTTON = "finish"
+
+class ConfigScreen:
+    '''Enables the creation of navigable, multi-paged configuration screens.'''
+
+    def __init__(self, title):
+        self.__title = title
+        self.__current_page = 1
+        self.__finished = False
+        self.__hal = HALWorker()
+        self.__libvirt = LibvirtWorker()
+
+    def get_hal(self):
+        return self.__hal
+
+    def get_libvirt(self):
+        return self.__libvirt
+
+    def set_finished(self):
+        self.__finished = True
+
+    def get_elements_for_page(self, screen, page):
+        return []
+
+    def page_has_next(self, page):
+        return False
+
+    def page_has_finish(self, page):
+        return False
+
+    def get_back_page(self, page):
+        if page > 1: return page - 1
+        return page
+
+    def go_back(self):
+        self.__current_page = self.get_back_page(self.__current_page)
+
+    def get_next_page(self, page):
+        return page + 1
+
+    def go_next(self):
+        self.__current_page = self.get_next_page(self.__current_page)
+
+    def validate_input(self, page, errors):
+        return True
+
+    def process_input(self, page):
+        return
+
+    def start(self):
+        active = True
+        while active and (self.__finished == False):
+            screen = SnackScreen()
+            gridform = GridForm(screen, self.__title, 1, 4)
+            elements = self.get_elements_for_page(screen, self.__current_page)
+            current_element = 0
+            for element in elements:
+                gridform.add(element, 0, current_element)
+                current_element += 1
+            # create the navigation buttons
+            buttons = []
+            if self.__current_page > 1: buttons.append(["Back", BACK_BUTTON, "F11"])
+            if self.page_has_next(self.__current_page): buttons.append(["Next", NEXT_BUTTON, "F12"])
+            if self.page_has_finish(self.__current_page): buttons.append(["Finish", FINISH_BUTTON, "F10"])
+            buttons.append(["Cancel", CANCEL_BUTTON, "ESC"])
+            buttonbar = ButtonBar(screen, buttons)
+            gridform.add(buttonbar, 0, current_element, growx = 1)
+            current_element += 1
+            try:
+                result = gridform.runOnce()
+                pressed = buttonbar.buttonPressed(result)
+                if pressed == BACK_BUTTON:
+                    self.go_back()
+                elif pressed == NEXT_BUTTON or pressed == FINISH_BUTTON:
+                    errors = []
+                    if self.validate_input(self.__current_page, errors):
+                        self.process_input(self.__current_page)
+                        self.go_next()
+                    else:
+                        error_text = ""
+                        for error in errors:
+                            error_text += "%s\n" % error
+                            ButtonChoiceWindow(screen,
+                                               "There Were Errors",
+                                               error_text,
+                                               buttons = ["OK"])
+                elif pressed == CANCEL_BUTTON:
+                    active = False
+            except Exception, error:
+                ButtonChoiceWindow(screen,
+                                   "An Exception Has Occurred",
+                                   str(error) + "\n" + traceback.format_exc(),
+                                   buttons = ["OK"])
+            screen.popWindow()
+            screen.finish()
+
+class DomainListConfigScreen(ConfigScreen):
+    '''Provides a base class for all config screens that require a domain list.'''
+
+    def __init__(self, title):
+        ConfigScreen.__init__(self, title)
+
+    def get_domain_list_page(self, screen, defined=True, created=True):
+        domains = self.get_libvirt().list_domains(defined, created)
+        result = None
+
+        if len(domains) > 0:
+            self.__has_domains = True
+            self.__domain_list = Listbox(0)
+            for name in self.get_libvirt().list_domains(defined, created):
+                self.__domain_list.append(name, name)
+            result = [self.__domain_list]
+        else:
+            self.__has_domains = False
+            grid = Grid(1, 1)
+            grid.setField(Label("There are no domains available."), 0, 0)
+            result = [grid]
+        return result
+
+    def get_selected_domain(self):
+        return self.__domain_list.current()
+
+    def has_selectable_domains(self):
+        return self.__has_domains
diff --git a/nodeadmin/createdomain.py b/nodeadmin/createdomain.py
new file mode 100755
index 0000000..b73a09e
--- /dev/null
+++ b/nodeadmin/createdomain.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+#
+# createdomain.py - Copyright (C) 2009 Red Hat, Inc.
+# Written by Darryl L. Pierce <dpierce at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+from snack import *
+from configscreen import *
+
+class CreateDomainConfigScreen(DomainListConfigScreen):
+    LIST_PAGE  = 1
+    CREATE_PAGE = 2
+
+    def __init__(self):
+        DomainListConfigScreen.__init__(self, "Create A Domain")
+
+    def get_elements_for_page(self, screen, page):
+        if page is self.LIST_PAGE:
+            return self.get_domain_list_page(screen, created = False)
+        elif page is self.CREATE_PAGE:
+            return self.get_create_domain_page(screen)
+
+    def page_has_next(self, page):
+        if page is self.LIST_PAGE: return self.has_selectable_domains()
+        return False
+
+    def page_has_back(self, page):
+        if page is self.CREATE_PAGE: return True
+        return False
+
+    def validate_input(self, page, errors):
+        if page is self.LIST_PAGE:
+            if self.get_selected_domain() is not None:
+                domain = self.get_selected_domain()
+                try:
+                    self.get_libvirt().create_domain(domain)
+                    return True
+                except Exception, error:
+                    errors.append("There was an error creating the domain: %s" % domain)
+                    errors.append(str(error))
+            else:
+                errors.append("You must first select a domain to create.")
+
+    def process_input(self, page):
+        print "foo"
+
+    def get_create_domain_page(self, screen):
+        grid = Grid(1, 1)
+        grid.setField(Label("%s was successfully created." % self.get_selected_domain()), 0, 0)
+        return [grid]
+
+def CreateDomain():
+    screen = CreateDomainConfigScreen()
+    screen.start()
diff --git a/nodeadmin/createuser.py b/nodeadmin/createuser.py
new file mode 100755
index 0000000..dbc4626
--- /dev/null
+++ b/nodeadmin/createuser.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python
+#
+# createuser.py - Copyright (C) 2009 Red Hat, Inc.
+# Written by Darryl L. Pierce <dpierce at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+from snack import *
+from configscreen import ConfigScreen
+from userworker import UserWorker
+
+import libuser
+
+DETAILS_PAGE = 1
+CONFIRM_PAGE = 2
+
+class CreateUserConfigScreen(ConfigScreen):
+    def __init__(self):
+        ConfigScreen.__init__(self, "Create A User Account")
+        self.__username = None
+        self.__useradmin = libuser.admin()
+        self.__user_worker = UserWorker()
+
+    def get_elements_for_page(self, screen, page):
+        if   page is DETAILS_PAGE: return self.get_details_page(screen)
+        elif page is CONFIRM_PAGE: return self.get_confirm_page(screen)
+
+    def validate_input(self, page, errors):
+        if page is DETAILS_PAGE:
+            if len(self.__username.value()) > 0:
+                name = self.__username.value()
+                if self.__useradmin.lookupUserByName(name) is None:
+                    if len(self.__password.value()) > 0:
+                        if self.__password.value() == self.__confirm.value():
+                            return True
+                        else:
+                            errors.append("Passwords do not match.")
+                    else:
+                        errors.append("You must enter a password.")
+                else:
+                    errors.append("User %s already exists." % name)
+            else:
+                errors.append("You must enter a username.")
+            self.__confirm.value()
+        return False
+
+    def process_input(self, page):
+        if page is CONFIRM_PAGE:
+            self.__user_worker.create_user(self.__username.value(),
+                                           self.__password.value(),
+                                           "wheel" if self.__adminuser.value() else None)
+            self.set_finished()
+
+    def page_has_next(self, page):
+        return (page is DETAILS_PAGE)
+
+    def page_has_back(self, page):
+        return (page is CONFIRM_PAGE)
+
+    def page_has_finish(self, page):
+        return (page is CONFIRM_PAGE)
+
+    def get_details_page(self, screen):
+        if self.__username is None:
+            self.__username = Entry(50, "")
+            self.__password = Entry(50, "", password = 1)
+            self.__confirm  = Entry(50, "", password = 1)
+            self.__adminuser = Checkbox("This user is an administrator", False)
+        grid = Grid(2, 4)
+        grid.setField(Label("Username:"), 0, 0, anchorRight = 1)
+        grid.setField(self.__username, 1, 0, anchorLeft = 1)
+        grid.setField(Label("Password:"), 0, 1, anchorRight = 1)
+        grid.setField(self.__password, 1, 1, anchorLeft = 1)
+        grid.setField(Label("Confirm password:"), 0, 2, anchorRight = 1)
+        grid.setField(self.__confirm, 1, 2, anchorLeft = 1)
+        grid.setField(Label(" "), 0, 3)
+        grid.setField(self.__adminuser, 1, 3, anchorLeft = 1)
+        return [Label("Enter The User Details"),
+                grid]
+
+    def get_confirm_page(self, screen):
+        grid = Grid(1, 2)
+        grid.setField(Label("Username: %s" % self.__username.value()), 0, 0)
+        admin_label = "is not"
+        if self.__adminuser.value():
+            admin_label = "is"
+        grid.setField(Label("This user %s an administrator." % admin_label), 0, 1)
+        return [Label("Create this user account?"),
+                grid]
+
+def CreateUser():
+    screen = CreateUserConfigScreen()
+    screen.start()
diff --git a/nodeadmin/definedomain.py b/nodeadmin/definedomain.py
new file mode 100755
index 0000000..ea4c986
--- /dev/null
+++ b/nodeadmin/definedomain.py
@@ -0,0 +1,470 @@
+#!/usr/bin/env python
+#
+# definedomain.py - Copyright (C) 2009 Red Hat, Inc.
+# Written by Darryl L. Pierce <dpierce at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+from snack import *
+import os
+from domainconfig import DomainConfig
+from configscreen import ConfigScreen
+import urlgrabber.progress as progress
+
+from virtinst import *
+
+VM_DETAILS_PAGE      = 1
+LOCAL_INSTALL_PAGE   = 2
+SELECT_CDROM_PAGE    = 3
+SELECT_ISO_PAGE      = 4
+NETWORK_INSTALL_PAGE = 10
+OS_TYPE_PAGE         = 11
+OS_VARIANT_PAGE      = 12
+RAM_CPU_PAGE         = 13
+ENABLE_STORAGE_PAGE  = 14
+LOCAL_STORAGE_PAGE   = 15
+MANAGED_STORAGE_PAGE = 16
+BRIDGE_PAGE          = 17
+VIRT_DETAILS_PAGE    = 18
+CONFIRM_PAGE         = 19
+
+LOCATION="location"
+KICKSTART="kickstart"
+KERNELOPTS="kernel.options"
+OS_TYPE="os.type"
+OS_VARIANT="os.variant"
+MEMORY="memory"
+CPUS="cpus"
+
+class DummyMeter(progress.BaseMeter):
+
+    def _do_start(self, now = None):
+        print "Starting..."
+
+    def _do_end(self, amount_read, now = None):
+        print "Ending: read=%d" % amount_read
+
+    def _do_update(self, amount_read, now = None):
+        print "Update: read=%d" % amount_read
+
+class DomainConfigScreen(ConfigScreen):
+
+    def __init__(self):
+        ConfigScreen.__init__(self, "Create A New Virtual Machine")
+        self.__config = DomainConfig()
+        self.__config.set_architecture(self.get_libvirt().get_default_architecture())
+        self.__config.set_virt_type(self.get_libvirt().get_default_virt_type())
+
+    def get_elements_for_page(self, screen, page):
+        if page == VM_DETAILS_PAGE:        return self.get_vm_details_page(screen)
+        elif page == LOCAL_INSTALL_PAGE:   return self.get_local_install_page(screen)
+        elif page == SELECT_CDROM_PAGE:    return self.get_select_cdrom_page(screen)
+        elif page == SELECT_ISO_PAGE:      return self.get_select_iso_page(screen)
+        elif page == NETWORK_INSTALL_PAGE: return self.get_network_install_page(screen)
+        elif page == OS_TYPE_PAGE:         return self.get_os_type_page(screen)
+        elif page == OS_VARIANT_PAGE:      return self.get_os_variant_page(screen)
+        elif page == RAM_CPU_PAGE:         return self.get_ram_and_cpu_page(screen)
+        elif page == ENABLE_STORAGE_PAGE:  return self.get_enable_storage_page(screen)
+        elif page == LOCAL_STORAGE_PAGE:   return self.get_local_storage_page(screen)
+        elif page == MANAGED_STORAGE_PAGE: return self.get_managed_storage_page(screen)
+        elif page == BRIDGE_PAGE:          return self.get_bridge_page(screen)
+        elif page == VIRT_DETAILS_PAGE:    return self.get_virt_details_page(screen)
+        elif page == CONFIRM_PAGE:         return self.get_confirm_page(screen)
+        return []
+
+    def validate_input(self, page, errors):
+        if page == VM_DETAILS_PAGE:
+            if len(self.__guest_name.value()) > 0:
+                if self.get_libvirt().domain_exists(self.__guest_name.value()):
+                    errors.append("Guest name '%s' is already in use." % self.__guest_name.value())
+                else:
+                    return True
+            else:
+                errors.append("Guest name must be a string between 0 and 50 characters.")
+        elif page == LOCAL_INSTALL_PAGE:
+            if self.__install_source.getSelection() == DomainConfig.INSTALL_SOURCE_CDROM:
+                return True
+            elif self.__install_source.getSelection() == DomainConfig.INSTALL_SOURCE_ISO:
+                return True
+        elif page == SELECT_CDROM_PAGE:
+            if self.__install_media.getSelection() != None:
+                if len(self.get_hal().list_installable_volumes()) == 0:
+                    errors.append("No installable media is available.")
+                else:
+                    return True
+            else:
+                errors.append("You must select an install media.")
+        elif page == SELECT_ISO_PAGE:
+            if len(self.__iso_path.value()) > 0:
+                if os.path.exists(self.__iso_path.value()):
+                    if os.path.isfile(self.__iso_path.value()):
+                        return True
+                    else:
+                        errors.append("%s is not a file." % self.__iso_path.value())
+                else:
+                    errors.append("No such install media exists:")
+                    errors.append(self.__iso_path.value())
+            else:
+                errors.append("An install media selection is required.")
+        elif page == NETWORK_INSTALL_PAGE:
+            if len(self.__install_url.value()) > 0:
+                return True
+            else:
+                errors.append("An install tree is required.")
+        elif page == OS_TYPE_PAGE: return True
+        elif page == OS_VARIANT_PAGE: return True
+        elif page == RAM_CPU_PAGE:
+            if (len(self.__memory.value()) > 0 and len(self.__cpus.value()) > 0) \
+                    and  (int(self.__memory.value()) > 0 and int(self.__cpus.value()) > 0):
+                return True
+            else:
+                if len(self.__memory.value()) == 0:
+                    errors.append("A value must be entered for memory.")
+                elif int(self.__memory.value()) <= 0:
+                    errors.append("A positive integer value must be entered for memory.")
+                if len(self.__cpus.value()) == 0:
+                    errors.append("A value must be entered for CPUs.")
+                elif int(self.__cpus.value()) <= 0:
+                    errors.append("A positive integer value must be entered for memory.")
+        elif page == ENABLE_STORAGE_PAGE: return True
+        elif page == LOCAL_STORAGE_PAGE:
+            if len(self.__storage_size.value()) > 0:
+                if float(self.__storage_size.value()) > 0:
+                    return True
+                else:
+                    errors.append("A positive value must be entered for the storage size.")
+            else:
+                errors.append("A value must be entered for the storage size.")
+        elif page == MANAGED_STORAGE_PAGE:
+            if self.__existing_storage.getSelection() is not None:
+                return True
+            else:
+                errors.append("Please select a storage volume.")
+        elif page == BRIDGE_PAGE:
+            if self.__network_bridges.getSelection() != None:
+                if len(self.__mac_address.value()) > 0:
+                    # TODO: regex check the format
+                    return True
+                else:
+                    errors.append("MAC address must be supplied.")
+            else:
+                errors.append("A network bridge must be selected.")
+        elif page == VIRT_DETAILS_PAGE:
+            if self.__virt_types.getSelection() != None and self.__architectures.getSelection() != None:
+                return True
+            if self.__virt_types.getSelection() is None:
+                errors.append("Please select a virtualization type.")
+            if self.__architectures.getSelection() is None:
+                errors.append("Please selection an architecture.")
+        elif page == CONFIRM_PAGE: return True
+        return False
+
+    def process_input(self, page):
+        if page == VM_DETAILS_PAGE:
+            self.__config.set_guest_name(self.__guest_name.value())
+            self.__config.set_install_type(self.__install_type.getSelection())
+        elif page == LOCAL_INSTALL_PAGE:
+            self.__config.set_use_cdrom_source(self.__install_source.getSelection() == DomainConfig.INSTALL_SOURCE_CDROM)
+        elif page == SELECT_CDROM_PAGE:
+            self.__config.set_install_media(self.__install_media.getSelection())
+        elif page == SELECT_ISO_PAGE:
+            self.__config.set_iso_path(self.__iso_path.value())
+        elif page == NETWORK_INSTALL_PAGE:
+            self.__config.set_install_url(self.__install_url.value())
+            self.__config.set_kickstart_url(self.__kickstart_url.value())
+            self.__config.set_kernel_options(self.__kernel_options.value())
+        elif page == OS_TYPE_PAGE:
+            self.__config.set_os_type(self.__os_types.getSelection())
+        elif page == OS_VARIANT_PAGE:
+            self.__config.set_os_variant(self.__os_variants.getSelection())
+        elif page == RAM_CPU_PAGE:
+            self.__config.set_memory(int(self.__memory.value()))
+            self.__config.set_cpus(int(self.__cpus.value()))
+        elif page == ENABLE_STORAGE_PAGE:
+            self.__config.set_enable_storage(self.__enable_storage.value())
+            if self.__storage_type.getSelection() == DomainConfig.NEW_STORAGE:
+                self.__config.set_use_local_storage(True)
+            elif self.__storage_type.getSelection() == Node.STORAGE_TYPE_EXISTING:
+                self.__config.set_use_local_storage(False)
+        elif page == LOCAL_STORAGE_PAGE:
+            self.__config.set_storage_size(float(self.__storage_size.value()))
+            self.__config.set_allocate_storage(self.__allocate_storage.value())
+        elif page == MANAGED_STORAGE_PAGE:
+            self.__config.set_use_local_storage(False)
+            self.__config.set_existing_storage(self.__existing_storage.getSelection())
+            self.__config.set_storage_size(self.get_libvirt().get_storage_size(self.__existing_storage.getSelection()))
+        elif page == BRIDGE_PAGE:
+            self.__config.set_network_bridge(self.__network_bridges.getSelection())
+        elif page == VIRT_DETAILS_PAGE:
+            self.__config.set_virt_type(self.__virt_types.getSelection())
+            self.__config.set_architecture(self.__architectures.getSelection())
+        elif page == CONFIRM_PAGE:
+            self.get_libvirt().define_domain(self.__config, DummyMeter())
+            self.set_finished()
+
+    def get_back_page(self, page):
+        result = page
+        if page == OS_TYPE_PAGE:
+            install_type = self.__config.get_install_type()
+            if install_type == DomainConfig.LOCAL_INSTALL:
+                if self.__config.get_use_cdrom_source():
+                    result = SELECT_CDROM_PAGE
+                else:
+                    result = SELECT_ISO_PAGE
+            elif install_type == DomainConfig.NETWORK_INSTALL:
+                result = NETWORK_INSTALL_PAGE
+            elif install_type == DomainConfig.PXE_INSTALL:
+                result = VM_DETAILS_PAGE
+        elif page == LOCAL_STORAGE_PAGE or page ==  MANAGED_STORAGE_PAGE:
+            result = ENABLE_STORAGE_PAGE
+        elif page == NETWORK_INSTALL_PAGE:
+            result = VM_DETAILS_PAGE
+        elif page == SELECT_CDROM_PAGE or page == SELECT_ISO_PAGE:
+            result = LOCAL_INSTALL_PAGE
+        elif page == BRIDGE_PAGE:
+            if self.__config.get_use_local_storage():
+                result = LOCAL_STORAGE_PAGE
+            else:
+                result = MANAGED_STORAGE_PAGE
+        else:
+            if page > 1: result = page - 1
+        return result
+
+    def get_next_page(self, page):
+        result = page
+        if page == VM_DETAILS_PAGE:
+            install_type = self.__config.get_install_type()
+            if install_type == DomainConfig.LOCAL_INSTALL:
+                result = LOCAL_INSTALL_PAGE
+            elif install_type == DomainConfig.NETWORK_INSTALL:
+                result = NETWORK_INSTALL_PAGE
+            elif install_type == DomainConfig.PXE_INSTALL:
+                result = OS_TYPE_PAGE
+        elif page == LOCAL_INSTALL_PAGE:
+            if self.__config.get_use_cdrom_source():
+                result = SELECT_CDROM_PAGE
+            else:
+                result = SELECT_ISO_PAGE
+        elif page == SELECT_CDROM_PAGE or page == SELECT_ISO_PAGE:
+            result = OS_TYPE_PAGE
+        elif page == NETWORK_INSTALL_PAGE:
+            result = OS_TYPE_PAGE
+        elif page == ENABLE_STORAGE_PAGE:
+            result = BRIDGE_PAGE
+            if self.__config.get_enable_storage():
+                if self.__config.get_use_local_storage():
+                    result = LOCAL_STORAGE_PAGE
+                else:
+                    result = MANAGED_STORAGE_PAGE
+        elif page == LOCAL_STORAGE_PAGE or page == MANAGED_STORAGE_PAGE:
+            result = BRIDGE_PAGE
+        else:
+            result = page + 1
+        return result
+
+    def page_has_finish(self, page):
+        if page == CONFIRM_PAGE: return True
+        return False
+
+    def page_has_next(self, page):
+        if page < CONFIRM_PAGE:
+            return True
+
+    def get_vm_details_page(self, screen):
+        self.__guest_name = Entry(50, self.__config.get_guest_name())
+        self.__install_type = RadioBar(screen, (("Local install media (ISO image or CDROM)",
+                                                 DomainConfig.LOCAL_INSTALL,
+                                                 self.__config.is_install_type(DomainConfig.LOCAL_INSTALL)),
+                                                ("Network Install (HTTP, FTP, or NFS)",
+                                                 DomainConfig.NETWORK_INSTALL,
+                                                 self.__config.is_install_type(DomainConfig.NETWORK_INSTALL)),
+                                                ("Network Boot (PXE)",
+                                                 DomainConfig.PXE_INSTALL,
+                                                 self.__config.is_install_type(DomainConfig.PXE_INSTALL))))
+        grid = Grid(2,3)
+        grid.setField(Label("Name:"), 0, 0, anchorRight = 1)
+        grid.setField(self.__guest_name, 1, 0, anchorLeft = 1)
+        grid.setField(Label("Choose how you would like to install the operating system"), 1, 1,
+                      anchorLeft = 1, anchorTop = 1)
+        grid.setField(self.__install_type, 1, 2, anchorLeft = 1)
+        return [Label("Enter your machine details"),
+                grid]
+
+    def get_local_install_page(self, screen):
+        self.__install_source = RadioBar(screen, (("Use CDROM or DVD",
+                                                   DomainConfig.INSTALL_SOURCE_CDROM,
+                                                   self.__config.get_use_cdrom_source()),
+                                                  ("Use ISO image",
+                                                   DomainConfig.INSTALL_SOURCE_ISO,
+                                                   self.__config.get_use_cdrom_source() is False)))
+        grid = Grid(1,1)
+        grid.setField(self.__install_source, 0, 0, anchorLeft = 1)
+        return [Label("Locate your install media"),
+                grid]
+
+    def get_select_cdrom_page(self, screen):
+        drives = []
+        media = self.get_hal().list_installable_volumes()
+        for drive in media.keys():
+            drives.append([media[drive], drive, self.__config.is_install_media(drive)])
+        self.__install_media = RadioBar(screen, (drives))
+        grid = Grid(1, 1)
+        grid.setField(self.__install_media, 0, 0)
+        return [Label("Select the install media"),
+                grid]
+
+    def get_select_iso_page(self, screen):
+        self.__iso_path = Entry(50, self.__config.get_iso_path())
+        grid = Grid(1, 2)
+        grid.setField(Label("Enter ISO path:"), 0, 0, anchorLeft = 1)
+        grid.setField(self.__iso_path, 0, 1, anchorLeft = 1)
+        return [Label("Enter the full path to an install ISO"),
+                grid]
+
+    def get_network_install_page(self, screen):
+        self.__install_url    = Entry(50, self.__config.get_install_url())
+        self.__kickstart_url  = Entry(50, self.__config.get_kickstart_url())
+        self.__kernel_options = Entry(50, self.__config.get_kernel_options())
+        grid = Grid(2,3)
+        grid.setField(Label("URL:"), 0, 0, anchorRight = 1)
+        grid.setField(self.__install_url, 1, 0, anchorLeft = 1)
+        grid.setField(Label("Kickstart URL:"), 0, 1, anchorRight = 1)
+        grid.setField(self.__kickstart_url, 1, 1, anchorLeft = 1)
+        grid.setField(Label("Kernel Options:"), 0, 2, anchorRight = 1)
+        grid.setField(self.__kernel_options, 1, 2, anchorLeft = 1)
+        return [Label("Provide the operating system URL"),
+                grid]
+
+    def get_os_type_page(self, screen):
+        types = []
+        for type in Guest.list_os_types():
+            types.append([Guest.get_os_type_label(type), type, self.__config.is_os_type(type)])
+        self.__os_types = RadioBar(screen, types)
+        grid = Grid(1, 1)
+        grid.setField(self.__os_types, 0, 0, anchorLeft = 1)
+        return [Label("Choose the operating system type"),
+                grid]
+
+    def get_os_variant_page(self, screen):
+        variants = []
+        type = self.__config.get_os_type()
+        for variant in Guest.list_os_variants(type):
+            variants.append([Guest.get_os_variant_label(type, variant), variant, self.__config.is_os_variant(variant)])
+        self.__os_variants = RadioBar(screen, variants)
+        grid = Grid(1, 1)
+        grid.setField(self.__os_variants, 0, 0, anchorLeft = 1)
+        return [Label("Choose the operating system version"),
+                grid]
+
+    def get_ram_and_cpu_page(self, screen):
+        self.__memory = Entry(10, str(self.__config.get_memory()))
+        self.__cpus   = Entry(10, str(self.__config.get_cpus()))
+        grid = Grid(2,2)
+        grid.setField(Label("Memory (RAM):"), 0, 0, anchorRight = 1)
+        grid.setField(self.__memory, 1, 0, anchorLeft = 1)
+        grid.setField(Label("CPUs:"), 0, 1, anchorRight = 1)
+        grid.setField(self.__cpus, 1, 1, anchorLeft = 1)
+        return [Label("Choose memory and CPU settings"),
+                grid]
+
+    def get_enable_storage_page(self, screen):
+        self.__enable_storage = Checkbox("Enable storage for this virtual machine", self.__config.get_enable_storage())
+        self.__storage_type     = RadioBar(screen,((["Create a disk image on the computer's hard disk",
+                                                     DomainConfig.NEW_STORAGE,
+                                                     self.__config.get_use_local_storage()]),
+                                                   (["Select managed or other existing storage",
+                                                     DomainConfig.EXISTING_STORAGE,
+                                                     self.__config.get_use_local_storage() is False])))
+        grid = Grid(1,2)
+        grid.setField(self.__enable_storage, 0, 0, anchorLeft = 1)
+        grid.setField(self.__storage_type, 0, 1, anchorLeft = 1)
+        return [Label("Configure storage"),
+                grid]
+
+    def get_local_storage_page(self, screen):
+        self.__storage_size     = Entry(6, str(self.__config.get_storage_size()))
+        self.__allocate_storage = Checkbox("Allocate entire disk now", self.__config.get_allocate_storage())
+        grid = Grid(2, 2)
+        grid.setField(self.__allocate_storage, 0, 0, growx = 1, anchorLeft = 1)
+        grid.setField(Label("Storage size (GB):"), 0, 1, anchorLeft = 1)
+        grid.setField(self.__storage_size, 1, 1)
+        return [Label("Configure local storage"),
+                grid]
+
+    def get_managed_storage_page(self, screen):
+        volumes = []
+        for volume in self.get_libvirt().list_storage_volumes():
+            volumes.append(["%s (%d GB)" % (volume.name(), volume.info()[1] / (1024 ** 3)),
+                            volume.name(),
+                            self.__config.is_existing_storage(volume.name())])
+        self.__existing_storage = RadioBar(screen, (volumes))
+        grid = Grid(2, 1)
+        grid.setField(Label("Existing storage:"), 0, 0)
+        grid.setField(self.__existing_storage, 1, 0)
+        return [Label("Configure managed storage"),
+                grid]
+
+    def get_bridge_page(self, screen):
+        bridges = []
+        for bridge in self.get_libvirt().list_bridges():
+            bridges.append(["Virtual network '%s'" % bridge.name(), bridge.name(), self.__config.get_network_bridge() == bridge.name()])
+        self.__network_bridges = RadioBar(screen, (bridges))
+        if self.__config.get_mac_address() == None:
+            self.__config.set_mac_address(self.get_libvirt().generate_mac_address())
+        self.__mac_address = Entry(20, self.__config.get_mac_address())
+        grid = Grid(1, 1)
+        grid.setField(self.__network_bridges, 0, 0)
+        return [Label("Select an existing bridge"),
+                grid]
+
+    def get_virt_details_page(self, screen):
+        virt_types = []
+        for type in self.get_libvirt().list_virt_types():
+            virt_types.append([type, type, self.__config.is_virt_type(type)])
+        self.__virt_types = RadioBar(screen, (virt_types))
+        archs = []
+        for arch in self.get_libvirt().list_architectures():
+            archs.append([arch, arch, self.__config.is_architecture(arch)])
+        self.__architectures = RadioBar(screen, (archs))
+        grid = Grid(2, 2)
+        grid.setField(Label("Virt Type:"), 0, 0, anchorRight = 1, anchorTop = 1)
+        grid.setField(self.__virt_types, 1, 0, anchorLeft = 1)
+        grid.setField(Label("Architecture:"), 0, 1, anchorRight = 1, anchorTop = 1)
+        grid.setField(self.__architectures, 1, 1, anchorLeft = 1)
+        return [Label("Configure virtualization details"),
+                grid]
+
+    def get_confirm_page(self, screen):
+        grid = Grid(2, 6)
+        grid.setField(Label("OS:"), 0, 0, anchorRight = 1)
+        grid.setField(Label(Guest.get_os_variant_label(self.__config.get_os_type(),
+                                                       self.__config.get_os_variant())), 1, 0, anchorLeft = 1)
+        grid.setField(Label("Install:"), 0, 1, anchorRight = 1)
+        grid.setField(Label(self.__config.get_install_type_text()), 1, 1, anchorLeft = 1)
+        grid.setField(Label("Memory:"), 0, 2, anchorRight = 1)
+        grid.setField(Label("%s MB" % self.__config.get_memory()), 1, 2, anchorLeft = 1)
+        grid.setField(Label("CPUs:"), 0, 3, anchorRight = 1)
+        grid.setField(Label("%d" % self.__config.get_cpus()), 1, 3, anchorLeft = 1)
+        grid.setField(Label("Storage:"), 0, 4, anchorRight = 1)
+        grid.setField(Label(self.__config.get_existing_storage()), 1, 4, anchorLeft = 1)
+        grid.setField(Label("Network:"), 0, 5, anchorRight = 1)
+        grid.setField(Label(self.__config.get_network_bridge()), 1, 5, anchorLeft = 1)
+        return [Label("Ready to begin installation of %s" % self.__config.get_guest_name()),
+                grid]
+
+def DefineDomain():
+    screen = DomainConfigScreen()
+    screen.start()
diff --git a/nodeadmin/destroydomain.py b/nodeadmin/destroydomain.py
new file mode 100755
index 0000000..350c32e
--- /dev/null
+++ b/nodeadmin/destroydomain.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+#
+# destroydomain.py - Copyright (C) 2009 Red Hat, Inc.
+# Written by Darryl L. Pierce <dpierce at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+from snack import *
+from configscreen import *
+
+class DestroyDomainConfigScreen(DomainListConfigScreen):
+    LIST_PAGE    = 1
+    DESTROY_PAGE = 2
+
+    def __init__(self):
+        DomainListConfigScreen.__init__(self, "Destroy A Domain")
+
+    def get_elements_for_page(self, screen, page):
+        if page is self.LIST_PAGE:
+            return self.get_domain_list_page(screen, defined = False)
+        elif page is self.DESTROY_PAGE:
+            return self.get_destroy_page(screen)
+
+    def page_has_next(self, page):
+        if page is self.LIST_PAGE: return self.has_selectable_domains()
+        return False
+
+    def page_has_back(self, page):
+        if page is self.DESTROY_PAGE: return True
+        return False
+
+    def validate_input(self, page, errors):
+        if page is self.LIST_PAGE:
+            if self.get_selected_domain() is not None:
+                domain = self.get_selected_domain()
+                try:
+                    self.get_libvirt().destroy_domain(domain)
+                    return True
+                except Exception, error:
+                    errors.append("There was an error destroy the domain: %s" % domain)
+                    errors.append(str(error))
+            else:
+                errors.append("You must first select a domain to destroy.")
+        return False
+
+    def get_destroy_page(self, screen):
+        grid = Grid(1, 1)
+        grid.setField(Label("%s was successfully destroyed." % self.get_selected_domain()), 0, 0)
+        return [grid]
+
+def DestroyDomain():
+    screen = DestroyDomainConfigScreen()
+    screen.start()
diff --git a/nodeadmin/domainconfig.py b/nodeadmin/domainconfig.py
new file mode 100644
index 0000000..ef39fe0
--- /dev/null
+++ b/nodeadmin/domainconfig.py
@@ -0,0 +1,217 @@
+# domainconfig.py - Copyright (C) 2009 Red Hat, Inc.
+# Written by Darryl L. Pierce <dpierce at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+from virtinst import Guest
+
+class DomainConfig:
+    LOCAL_INSTALL   = "local"
+    NETWORK_INSTALL = "network"
+    PXE_INSTALL     = "pxe"
+    INSTALL_TYPE_TEXT = {LOCAL_INSTALL   : "Local CDROM/ISO",
+                         NETWORK_INSTALL : "URL INstall Tree",
+                         PXE_INSTALL     : "PXE Install"}
+
+    INSTALL_SOURCE_CDROM = "cdrom"
+    INSTALL_SOURCE_ISO   = "iso"
+
+    NEW_STORAGE      = "new"
+    EXISTING_STORAGE = "existing"
+
+    def __init__(self):
+        self.__guest_name = ""
+        self.__install_type = DomainConfig.LOCAL_INSTALL
+        self.__use_cdrom_source = True
+        self.__install_location = ""
+        self.__install_media = ""
+        self.__iso_path = ""
+        self.__install_url = ""
+        self.__kickstart_url = ""
+        self.__kernel_options = ""
+        self.__os_type = "other"
+        self.__os_variant = None
+        self.__memory = 512
+        self.__cpus = 1
+        self.__enable_storage = True
+        self.__use_local_storage = True
+        self.__storage_size = 8.0
+        self.__allocate_storage = True
+        self.__existing_storage = ""
+        self.__network_bridge = None
+        self.__mac_address = None
+        self.__virt_type = None
+        self.__architecture = None
+
+    def set_guest_name(self, name):
+        self.__guest_name = name
+
+    def get_guest_name(self):
+        return self.__guest_name
+
+    def set_install_type(self, type):
+        self.__install_type = type
+
+    def get_install_type(self):
+        return self.__install_type
+
+    def get_install_type_text(self):
+        return DomainConfig.INSTALL_TYPE_TEXT[self.get_install_type()]
+
+    def is_install_type(self, type):
+        return self.__install_type == type
+
+    def set_install_location(self, location):
+        self.__install_location = location
+
+    def set_use_cdrom_source(self, use):
+        self.__use_cdrom_source = use
+
+    def get_use_cdrom_source(self):
+        return self.__use_cdrom_source
+
+    def get_install_location(self):
+        return self.__install_location
+
+    def is_install_location(self, location):
+        return self.__install_location == location
+
+    def set_install_media(self, media):
+        self.__install_media = media
+
+    def get_install_media(self):
+        return self.__install_media
+
+    def is_install_media(self, media):
+        return self.__install_media == media
+
+    def set_iso_path(self, path):
+        self.__iso_path = path
+
+    def get_iso_path(self):
+        return self.__iso_path
+
+    def set_install_url(self, url):
+        self.__install_url = url
+
+    def get_install_url(self):
+        return self.__install_url
+
+    def set_kickstart_url(self, url):
+        self.__kickstart_url = url
+
+    def get_kickstart_url(self):
+        return self.__kickstart_url
+
+    def set_kernel_options(self, options):
+        self.__kernel_options = options
+
+    def get_kernel_options(self):
+        return self.__kernel_options
+
+    def set_os_type(self, type):
+        self.__os_type = type
+        self.__os_variant = Guest.list_os_variants(type)[0]
+
+    def get_os_type(self):
+        return self.__os_type
+
+    def is_os_type(self, type):
+        return self.__os_type == type
+
+    def set_os_variant(self, variant):
+        self.__os_variant = variant
+
+    def get_os_variant(self):
+        return self.__os_variant
+
+    def is_os_variant(self, variant):
+        return self.__os_variant == variant
+
+    def set_memory(self, memory):
+        self.__memory = int(memory)
+
+    def get_memory(self):
+        return self.__memory
+
+    def set_cpus(self, cpus):
+        self.__cpus = cpus
+
+    def get_cpus(self):
+        return self.__cpus
+
+    def set_enable_storage(self, enable):
+        self.__enable_storage = enable
+
+    def get_enable_storage(self):
+        return self.__enable_storage
+
+    def set_use_local_storage(self, use):
+        self.__use_local_storage = use
+
+    def get_use_local_storage(self):
+        return self.__use_local_storage
+
+    def set_storage_size(self, size):
+        self.__storage_size = size
+
+    def get_storage_size(self):
+        return self.__storage_size
+
+    def set_allocate_storage(self, allocate):
+        self.__allocate_storage = allocate
+
+    def get_allocate_storage(self):
+        return self.__allocate_storage
+
+    def set_existing_storage(self, storage):
+        self.__existing_storage = storage
+
+    def get_existing_storage(self):
+        return self.__existing_storage
+
+    def is_existing_storage(self, storage):
+        return self.__existing_storage == storage
+
+    def set_network_bridge(self, bridge):
+        self.__network_bridge = bridge
+
+    def get_network_bridge(self):
+        return self.__network_bridge
+
+    def set_mac_address(self, address):
+        self.__mac_address = address
+
+    def get_mac_address(self):
+        return self.__mac_address
+
+    def set_virt_type(self, type):
+        self.__virt_type = type
+
+    def get_virt_type(self):
+        return self.__virt_type
+
+    def is_virt_type(self, type):
+        return self.__virt_type == type
+
+    def set_architecture(self, architecture):
+        self.__architecture = architecture
+
+    def get_architecture(self):
+        return self.__architecture
+
+    def is_architecture(self, architecture):
+        return self.__architecture == architecture
diff --git a/nodeadmin/halworker.py b/nodeadmin/halworker.py
new file mode 100644
index 0000000..448c22d
--- /dev/null
+++ b/nodeadmin/halworker.py
@@ -0,0 +1,37 @@
+# halworker.py - Copyright (C) 2009 Red Hat, Inc.
+# Written by Darryl L. Pierce <dpierce at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+import dbus
+import virtinst
+
+class HALWorker:
+    '''Provides utilities for working with HAL to get hardware information.'''
+    def __init__(self):
+        self.__bus = dbus.SystemBus()
+        hobj = self.__bus.get_object("org.freedesktop.Hal", "/org/freedesktop/Hal/Manager")
+        self.__conn = dbus.Interface(hobj, "org.freedesktop.Hal.Manager")
+
+    def list_installable_volumes(self):
+        result = {}
+        for udi in self.__conn.FindDeviceByCapability("volume"):
+            device = self.__bus.get_object("org.freedesktop.Hal", udi)
+            info = dbus.Interface(device, "org.freedesktop.Hal.Device")
+            if info.GetProperty("volume.is_disc"):
+                if info.GetProperty("volume.disc.has_data"):
+                    result[str(info.GetProperty("block.device"))] = info.GetProperty("volume.label")
+        return result
diff --git a/nodeadmin/libvirtworker.py b/nodeadmin/libvirtworker.py
new file mode 100644
index 0000000..d10595c
--- /dev/null
+++ b/nodeadmin/libvirtworker.py
@@ -0,0 +1,271 @@
+# libvirtworker.py - Copyright (C) 2009 Red Hat, Inc.
+# Written by Darryl L. Pierce <dpierce at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+import dbus
+import libvirt
+import os
+import virtinst
+
+from domainconfig import DomainConfig
+
+DEFAULT_POOL_TARGET_PATH="/var/lib/libvirt/images"
+
+class LibvirtWorker:
+    '''Provides utilities for interfacing with libvirt.'''
+    def __init__(self, url = "qemu:///system"):
+        self.__conn = libvirt.open(url)
+        self.__capabilities = virtinst.CapabilitiesParser.parse(self.__conn.getCapabilities())
+        self.__net = virtinst.VirtualNetworkInterface(conn = self.__conn)
+        self.__net.setup(self.__conn)
+        (self.__new_guest, self.__new_domain) = virtinst.CapabilitiesParser.guest_lookup(conn = self.__conn)
+
+    def list_domains(self, defined = True, started = True):
+        '''Lists all domains.'''
+        result = []
+        if defined:
+            result.extend(self.__conn.listDefinedDomains())
+        if started:
+            for id in self.__conn.listDomainsID():
+                result.append(self.__conn.lookupByID(id).name())
+        return result
+
+    def get_domain(self, name):
+        '''Returns the specified domain.'''
+        result = self.__conn.lookupByName(name)
+        if result is None: raise Exception("No such domain exists: %s" % name)
+
+        return result
+
+    def domain_exists(self, name):
+        '''Returns whether a domain with the specified node exists.'''
+        domains = self.list_domains()
+        if name in domains: return True
+        return False
+
+    def create_domain(self, name):
+        '''Creates the specified domain.'''
+        domain = self.get_domain(name)
+        domain.create()
+
+    def destroy_domain(self, name):
+        '''Destroys the specified domain.'''
+        domain = self.get_domain(name)
+        domain.destroy()
+
+    def undefine_domain(self, name):
+        '''Undefines the specified domain.'''
+        domain = self.get_domain(name)
+        domain.undefine()
+
+    def list_storage_pools(self):
+        '''Returns the list of all defined storage pools.'''
+        return self.__conn.listStoragePools()
+
+    def storage_pool_exists(self, name):
+        '''Returns whether a storage pool exists.'''
+        pools = self.list_storage_pools()
+        if name in pools: return True
+        return False
+
+    def define_storage_pool(self, name):
+        '''Defines a storage pool with the given name.'''
+        try:
+            pool = virtinst.Storage.DirectoryPool(conn=self.__conn,
+                                                  name=name,
+                                                  target_path=DEFAULT_POOL_TARGET_PATH)
+            newpool = pool.install(build=True, create=True)
+            newpool.setAutostart(True)
+        except Exception, error:
+            raise RuntimeError("Could not create pool: %s - %s", str(error))
+
+    def list_bridges(self):
+        '''Lists all defined and active bridges.'''
+        bridges = self.__conn.listNetworks()
+        bridges.extend(self.__conn.listDefinedNetworks())
+        result = []
+        for name in bridges:
+            bridge = self.__conn.networkLookupByName(name)
+            result.append(bridge)
+        return result
+
+    def generate_mac_address(self):
+        return self.__net.macaddr
+
+    def list_storage_volumes(self):
+        '''Lists all defined storage volumes.'''
+        pools = self.__conn.listStoragePools()
+        pools.extend(self.__conn.listDefinedStoragePools())
+        result = []
+        for name in pools:
+            pool = self.__conn.storagePoolLookupByName(name)
+            for volname in pool.listVolumes():
+                volume = self.__conn.storageVolLookupByPath("/var/lib/libvirt/images/%s" % volname)
+                result.append(volume)
+        return result
+
+    def get_storage_size(self, name):
+        '''Returns the size of the specified storage volume.'''
+        volume = self.__conn.storageVolLookupByPath("/var/lib/libvirt/images/%s" % name)
+        return volume.info()[1] / (1024.0 ** 3)
+
+    def get_virt_types(self):
+        result = []
+        for guest in self.__capabilities.guests:
+            guest_type = guest.os_type
+            for domain in guest.domains:
+                domain_type = domain.hypervisor_type
+                label = domain_type
+
+                if domain_type is "kvm" and guest_type is "xen": label = "xenner"
+                elif domain_type is "xen":
+                    if guest_type is "xen":
+                        label = "xen (paravirt)"
+                    elif guest_type is "kvm":
+                        label = "xen (fullvirt)"
+                elif domain_type is "test":
+                    if guest_type is "xen":
+                        label = "test (xen)"
+                    elif guest_type is "hvm":
+                        label = "test (hvm)"
+
+                for row in result:
+                    if row[0] == label:
+                        label = None
+                        break
+                if label is None: continue
+
+                result.append([label, domain_type, guest_type])
+        return result
+
+    def list_virt_types(self):
+        virt_types = self.get_virt_types()
+        result = []
+        for type in virt_types:
+            result.append(type[0])
+        return result
+
+    def get_default_architecture(self):
+        '''Returns a default hypervisor type for new domains.'''
+        return self.__new_guest.arch
+
+    def get_hypervisor(self, virt_type):
+        virt_types = self.get_virt_types()
+        for type in virt_types:
+            if type[0] is virt_type: return type[1]
+        return None
+
+    def get_default_virt_type(self):
+        '''Returns the default virtualization type for new domains.'''
+        return self.__new_domain.hypervisor_type
+
+    def get_os_type(self, virt_type):
+        virt_types = self.get_virt_types()
+        for type in virt_types:
+            if type[0] is virt_type: return type[2]
+        return None
+
+    def list_architectures(self):
+        result = []
+        for guest in self.__capabilities.guests:
+            for domain in guest.domains:
+                label = guest.arch
+                for row in result:
+                    if row == label:
+                        label = None
+                        break
+                if label is None: continue
+
+                result.append(label)
+        return result
+
+    def define_domain(self, config, meter):
+        location = extra = kickstart = None
+
+        if config.get_install_type() == DomainConfig.LOCAL_INSTALL:
+            if config.get_use_cdrom_source():
+                iclass = virtinst.DistroInstaller
+                location = config.get_install_media()
+            else:
+                iclass = virtinst.LiveCDInstaller
+                location = config.get_is_path()
+        elif config.get_install_type() == DomainConfig.NETWORK_INSTALL:
+            iclass = virtinst.DistroInstaller
+            location = config.get_install_url()
+            extra = config.get_kernel_options()
+            kickstart = config.get_kickstart_url()
+        elif config.get_install_type() == DomainConfig.PXE_INSTALL:
+            iclass = virtinst.PXEInstaller
+
+        installer = iclass(conn = self.__conn,
+                           type = self.get_hypervisor(config.get_virt_type()),
+                           os_type = self.get_os_type(config.get_virt_type()))
+        self.__guest = installer.guest_from_installer()
+        self.__guest.name = config.get_guest_name()
+        self.__guest.vcpus = config.get_cpus()
+        self.__guest.memory = config.get_memory()
+        self.__guest.maxmemory = config.get_memory()
+
+        self.__guest.installer.location = location
+        if config.get_use_cdrom_source(): self.__guest.installer.cdrom = True
+        extraargs = ""
+        if extra: extraargs += extra
+        if kickstart: extraargs += " ks=%s" % kickstart
+        if extraargs: self.__guest.installer.extraarags = extraargs
+
+        self.__guest.uuid = virtinst.util.uuidToString(virtinst.util.randomUUID())
+
+        if config.get_os_type() != "generic": self.__guest.os_type = config.get_os_type()
+        if config.get_os_variant() != "generic": self.__guest.os_variant = config.get_os_variant()
+
+        self.__guest._graphics_dev = virtinst.VirtualGraphics(type = virtinst.VirtualGraphics.TYPE_VNC)
+        self.__guest.sound_devs = []
+        self.__guest.sound_devs.append(virtinst.VirtualAudio(model = "es1370"))
+
+        self._setup_nics(config)
+        self._setup_disks(config)
+
+        self.__guest.conn = self.__conn
+        self.__domain = self.__guest.start_install(False, meter = meter)
+
+    def _setup_nics(self, config):
+        self.__guest.nics = []
+        nic = virtinst.VirtualNetworkInterface(type = virtinst.VirtualNetworkInterface.TYPE_VIRTUAL,
+                                               bridge = config.get_network_bridge(),
+                                               network = config.get_network_bridge(),
+                                               macaddr = config.get_mac_address())
+        self.__guest.nics.append(nic)
+
+    def _setup_disks(self, config):
+        self.__guest.disks = []
+        if config.get_enable_storage():
+            path = None
+            if config.get_use_local_storage():
+                if self.storage_pool_exists("default") is False:
+                    self.define_storage_pool("default")
+                pool = self.__conn.storagePoolLookupByName("default")
+                path = virtinst.Storage.StorageVolume.find_free_name(config.get_guest_name(),
+                                                                     pool_object = pool,
+                                                                     suffix = ".img")
+                path = os.path.join(DEFAULT_POOL_TARGET_PATH, path)
+
+            if path is not None:
+                storage= virtinst.VirtualDisk(conn = self.__conn,
+                                              path = path,
+                                              size = config.get_storage_size())
+                self.__guest.disks.append(storage)
+        self.__guest.conn = self.__conn
diff --git a/nodeadmin/listdomains.py b/nodeadmin/listdomains.py
new file mode 100755
index 0000000..1b51ee2
--- /dev/null
+++ b/nodeadmin/listdomains.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+#
+# listdomains.py - Copyright (C) 2009 Red Hat, Inc.
+# Written by Darryl L. Pierce <dpierce at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+from snack import *
+from libvirtworker import LibvirtWorker
+from configscreen import *
+
+class ListDomainsConfigScreen(DomainListConfigScreen):
+    LIST_PAGE   = 1
+    DETAIL_PAGE = 2
+
+    def __init__(self):
+        DomainListConfigScreen.__init__(self, 'List Domains')
+
+    def page_has_next(self, page):
+        return (page == self.LIST_PAGE)
+
+    def page_has_back(self, page):
+        return (page == self.DETAIL_PAGE)
+
+    def validate_input(self, page, errors):
+        if page == self.LIST_PAGE:
+            if self.get_selected_domain() is None:
+                errors.append("Please select a domain to view.")
+            else:
+                return True
+
+    def get_elements_for_page(self, screen, page):
+        if page == self.LIST_PAGE:
+            return self.get_domain_list_page(screen)
+        elif page == self.DETAIL_PAGE:
+            return self.get_detail_page_elements(screen)
+
+    def get_detail_page_elements(self, screen):
+        domain = self.get_libvirt().get_domain(self.get_selected_domain())
+        grid = Grid(2, 5)
+        grid.setField(Label("Name:  "), 0, 0, anchorRight = 1)
+        grid.setField(Label(domain.name()), 1, 0, anchorLeft = 1)
+        grid.setField(Label("UUID:  "), 0, 1, anchorRight = 1)
+        grid.setField(Label(domain.UUIDString()), 1, 1, anchorLeft = 1)
+        grid.setField(Label("OS Type:  "), 0, 2, anchorRight = 1)
+        grid.setField(Label(domain.OSType()), 1, 2, anchorLeft = 1)
+        grid.setField(Label("Max. Memory:  "), 0, 3, anchorRight = 1)
+        grid.setField(Label(str(domain.maxMemory())), 1, 3, anchorLeft = 1)
+        grid.setField(Label("Max. VCPUs:  "), 0, 4, anchorRight = 1)
+        grid.setField(Label(str(domain.maxVcpus())), 1, 4, anchorLeft = 1)
+        return [grid]
+
+def ListDomains():
+    screen = ListDomainsConfigScreen()
+    screen.start()
diff --git a/nodeadmin/mainmenu.py b/nodeadmin/mainmenu.py
new file mode 100755
index 0000000..2808dad
--- /dev/null
+++ b/nodeadmin/mainmenu.py
@@ -0,0 +1,71 @@
+# mainmenu.py - Copyright (C) 2009 Red Hat, Inc.
+# Written by Darryl L. Pierce <dpierce at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+from snack import *
+import traceback
+from configscreen   import ConfigScreen
+from definedomain   import DefineDomain
+from createdomain   import CreateDomain
+from destroydomain  import DestroyDomain
+from undefinedomain import UndefineDomain
+from listdomains    import ListDomains
+from createuser     import CreateUser
+
+DEFINE_DOMAIN    = 1
+CREATE_DOMAIN    = 2
+DESTROY_DOMAIN   = 3
+UNDEFINE_DOMAIN  = 4
+LIST_DOMAINS     = 5
+CREATE_USER      = 6
+EXIT_CONSOLE     = 99
+
+def MainMenu():
+    finished = False
+    while finished == False:
+        screen = SnackScreen()
+        menu = Listbox(height = 0, width = 0, returnExit = 1)
+        menu.append("Define A Domain",     DEFINE_DOMAIN)
+        menu.append("Create A Domain",     CREATE_DOMAIN)
+        menu.append("Destroy A Domain",    DESTROY_DOMAIN)
+        menu.append("Undefine A Domain",   UNDEFINE_DOMAIN)
+        menu.append("List All Domains",    LIST_DOMAINS)
+        menu.append("Create A User",       CREATE_USER)
+        menu.append("Exit Administration", EXIT_CONSOLE)
+        gridform = GridForm(screen, "Node Administration Console", 1, 4)
+        gridform.add(menu, 0, 0)
+        result = gridform.run();
+        screen.popWindow()
+        screen.finish()
+
+        try:
+            if   result.current() == DEFINE_DOMAIN:   DefineDomain()
+            elif result.current() == CREATE_DOMAIN:   CreateDomain()
+            elif result.current() == DESTROY_DOMAIN:  DestroyDomain()
+            elif result.current() == UNDEFINE_DOMAIN: UndefineDomain()
+            elif result.current() == LIST_DOMAINS:    ListDomains()
+            elif result.current() == CREATE_USER:     CreateUser()
+            elif result.current() == EXIT_CONSOLE:    finished = True
+        except Exception, error:
+            screen = SnackScreen()
+            ButtonChoiceWindow(screen,
+                               "An Exception Has Occurred",
+                               str(error) + "\n" + traceback.format_exc(),
+                               buttons = ["OK"])
+            screen.popWindow()
+            screen.finish()
+            finished = True
diff --git a/nodeadmin/nodeadmin.py b/nodeadmin/nodeadmin.py
new file mode 100755
index 0000000..864a4c0
--- /dev/null
+++ b/nodeadmin/nodeadmin.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+#
+# node-admin - Copyright (C) 2009 Red Hat, Inc.
+# Written by Darryl L. Pierce <dpierce at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+import sys
+
+from mainmenu import MainMenu
+
+def NodeAdmin():
+    MainMenu()
+
+if __name__ == "__main__":
+    sys.exit(NodeAdmin())
diff --git a/nodeadmin/setup.py.in b/nodeadmin/setup.py.in
new file mode 100644
index 0000000..f51a34c
--- /dev/null
+++ b/nodeadmin/setup.py.in
@@ -0,0 +1,34 @@
+# setup.py - Copyright (C) 2009 Red Hat, Inc.
+# Written by Darryl L. Pierce <dpierce at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+from setuptools import setup, find_packages
+
+setup(name = "nodeadmin",
+      version = "@VERSION@",
+      package_dir = {'nodeadmin': 'nodeadmin'},
+      packages = find_packages('.'),
+      entry_points = {
+        'console_scripts': [
+            'nodeadmin   = nodeadmin.nodeadmin:NodeAdmin',
+            'definedom   = nodeadmin.definedomain:DefineDomain',
+            'createdom   = nodeadmin.createdomain:CreateDomain',
+            'destroydom  = nodeadmin.destroydomain:DestroyDomain',
+            'undefinedom = nodeadmin.undefinedomain:UndefineDomain',
+            'createuser  = nodeadmin.createuser:CreateUser',
+            'listdoms    = nodeadmin.listdomains:ListDomains']
+        })
diff --git a/nodeadmin/undefinedomain.py b/nodeadmin/undefinedomain.py
new file mode 100755
index 0000000..2620540
--- /dev/null
+++ b/nodeadmin/undefinedomain.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+#
+# undefinedomain.py - Copyright (C) 2009 Red Hat, Inc.
+# Written by Darryl L. Pierce <dpierce at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+from snack import *
+from configscreen import *
+
+class UndefineDomainConfigScreen(DomainListConfigScreen):
+    LIST_PAGE     = 1
+    CONFIRM_PAGE  = 2
+    UNDEFINE_PAGE = 3
+
+    def __init__(self):
+        DomainListConfigScreen.__init__(self, "Undefine A Domain")
+
+    def get_elements_for_page(self, screen, page):
+        if   page is self.LIST_PAGE:     return self.get_domain_list_page(screen)
+        elif page is self.CONFIRM_PAGE:  return self.get_confirm_page(screen)
+        elif page is self.UNDEFINE_PAGE: return self.get_undefine_page(screen)
+
+    def page_has_next(self, page):
+        if   page is self.LIST_PAGE:     return self.has_selectable_domains()
+        elif page is self.CONFIRM_PAGE:  return True
+        return False
+
+    def page_has_back(self, page):
+        if   page is self.CONFIRM_PAGE:  return True
+        elif page is self.UNDEFINE_PAGE: return True
+        return False
+
+    def get_back_page(self, page):
+        if   page is self.CONFIRM_PAGE:  return self.LIST_PAGE
+        elif page is self.UNDEFINE_PAGE: return self.LIST_PAGE
+
+    def validate_input(self, page, errors):
+        if page is self.LIST_PAGE:
+            if self.get_selected_domain() is not None:
+                return True
+            else:
+                errors.append("You must first select a domain.")
+        elif page is self.CONFIRM_PAGE:
+            if self.__confirm_undefine.value():
+                domain = self.get_selected_domain()
+                try:
+                    self.get_libvirt().undefine_domain(domain)
+                    return True
+                except Exception, error:
+                    errors.append("Failed to undefine %s." % domain)
+                    errors.append(str(error))
+            else:
+                errors.append("You must confirm undefining the domain to proceed.")
+        return False
+
+    def get_confirm_page(self, screen):
+        self.__confirm_undefine = Checkbox("Check here to confirm undefining %s." % self.get_selected_domain(), 0)
+        grid = Grid(1, 1)
+        grid.setField(self.__confirm_undefine, 0, 0)
+        return [grid]
+
+    def get_undefine_page(self, screen):
+        grid = Grid(1, 1)
+        grid.setField(Label("%s has been undefined." % self.get_selected_domain()), 0, 0)
+        return [grid]
+
+def UndefineDomain():
+    screen = UndefineDomainConfigScreen()
+    screen.start()
diff --git a/nodeadmin/userworker.py b/nodeadmin/userworker.py
new file mode 100644
index 0000000..167197b
--- /dev/null
+++ b/nodeadmin/userworker.py
@@ -0,0 +1,38 @@
+# userworker.py - Copyright (C) 2009 Red Hat, Inc.
+# Written by Darryl L. Pierce <dpierce at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+
+import libuser
+
+class UserWorker:
+    '''Provides APIs for creating, modifying and deleting user accounts.'''
+    def __init__(self):
+        self.__admin = libuser.admin()
+
+    def create_user(self, username, password, other_group):
+        '''Creates a new user account with the provides username,
+        password. The user is also added to the optional group
+        if one is specified.'''
+        user = self.__admin.initUser(username)
+        user.set('pw_passwd', password)
+        self.__admin.addUser(user)
+        if other_group is not None:
+            group = self.__admin.lookupGroupByName(other_group)
+            if group is None: raise Exception("Invalid group specified: %s" % other_group)
+            user.add('pw_gid', group.get('pw_gid')[0])
+            self.__admin.modifyUser(user)
+
diff --git a/ovirt-node.spec.in b/ovirt-node.spec.in
index 12815c9..9091f4f 100644
--- a/ovirt-node.spec.in
+++ b/ovirt-node.spec.in
@@ -1,5 +1,7 @@
 %define product_family oVirt Node
 %define beta Beta
+%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
+
 
 Summary:        The oVirt Node daemons/scripts
 Name:           ovirt-node
@@ -21,6 +23,8 @@ Requires(post):  /sbin/chkconfig
 Requires(preun): /sbin/chkconfig
 BuildRequires:  libvirt-devel >= 0.5.1
 BuildRequires:  dbus-devel hal-devel
+BuildRequires:  python-devel
+BuildRequires:  python-setuptools
 Requires:       libvirt >= 0.6.3
 Requires:       augeas >= 0.3.5
 Requires:       libvirt-qpid >= 0.2.14-3
@@ -44,6 +48,10 @@ Requires:       nc
 Requires:       grub
 Requires:       /usr/sbin/crond
 Requires:       anyterm
+Requires:       newt-python
+Requires:       libuser-python
+Requires:       dbus-python
+
 ExclusiveArch:  %{ix86} x86_64
 
 %define app_root %{_datadir}/%{name}
@@ -144,6 +152,7 @@ cd -
 %{__install} -d -m0755 %{buildroot}%{_sysconfdir}/cron.d
 %{__install} -d -m0755 %{buildroot}%{_sysconfdir}/cron.hourly
 %{__install} -d -m0755 %{buildroot}%{_sysconfdir}/logrotate.d
+%{__install} -d -m0755 %{buildroot}%{python_sitelib}/nodeadmin
 
 %{__install} -p -m0755 scripts/ovirt-awake %{buildroot}%{_sbindir}
 %{__install} -p -m0755 scripts/ovirt-config-boot %{buildroot}%{_sbindir}
@@ -164,6 +173,21 @@ cd -
 %{__install} -p -m0755 scripts/persist %{buildroot}%{_sbindir}
 %{__install} -p -m0755 scripts/unpersist %{buildroot}%{_sbindir}
 
+%{__install} -p -m0644 nodeadmin/__init__.py %{buildroot}%{python_sitelib}/nodeadmin
+%{__install} -p -m0644 nodeadmin/configscreen.py %{buildroot}%{python_sitelib}/nodeadmin
+%{__install} -p -m0755 nodeadmin/createdomain.py %{buildroot}%{python_sitelib}/nodeadmin
+%{__install} -p -m0755 nodeadmin/createuser.py %{buildroot}%{python_sitelib}/nodeadmin
+%{__install} -p -m0755 nodeadmin/definedomain.py %{buildroot}%{python_sitelib}/nodeadmin
+%{__install} -p -m0755 nodeadmin/destroydomain.py %{buildroot}%{python_sitelib}/nodeadmin
+%{__install} -p -m0644 nodeadmin/domainconfig.py %{buildroot}%{python_sitelib}/nodeadmin
+%{__install} -p -m0644 nodeadmin/halworker.py %{buildroot}%{python_sitelib}/nodeadmin
+%{__install} -p -m0644 nodeadmin/libvirtworker.py %{buildroot}%{python_sitelib}/nodeadmin
+%{__install} -p -m0644 nodeadmin/userworker.py %{buildroot}%{python_sitelib}/nodeadmin
+%{__install} -p -m0755 nodeadmin/listdomains.py %{buildroot}%{python_sitelib}/nodeadmin
+%{__install} -p -m0644 nodeadmin/mainmenu.py %{buildroot}%{python_sitelib}/nodeadmin
+%{__install} -p -m0755 nodeadmin/nodeadmin.py %{buildroot}%{python_sitelib}/nodeadmin
+%{__install} -p -m0755 nodeadmin/undefinedomain.py %{buildroot}%{python_sitelib}/nodeadmin
+
 # gptsync
 %{__install} -p -m0755 gptsync/gptsync %{buildroot}%{_sbindir}
 %{__install} -p -m0755 gptsync/showpart %{buildroot}%{_sbindir}
@@ -182,6 +206,10 @@ cd -
 %{__install} -p -m0644 logrotate/ovirt-logrotate %{buildroot}%{_sysconfdir}/cron.d
 %{__install} -p -m0644 logrotate/ovirt-logrotate.conf %{buildroot}%{_sysconfdir}/logrotate.d
 
+# install the admin tools
+python nodeadmin/setup.py install --root %{buildroot}
+# rm -rf %{buildroot}%{python_sitelib}/nodeadmin- at VERSION@*
+
 echo "oVirt Node release %{version}-%{release}" > %{buildroot}%{_sysconfdir}/ovirt-release
 mkdir -p %{buildroot}/%{_sysconfdir}/default
 touch %{buildroot}/%{_sysconfdir}/default/ovirt
@@ -325,7 +353,16 @@ fi
 %{_sbindir}/ovirt-awake
 %{_initrddir}/ovirt-functions
 %defattr(-,root,root,0644)
+%{_bindir}/nodeadmin
+%{_bindir}/definedom
+%{_bindir}/createdom
+%{_bindir}/destroydom
+%{_bindir}/undefinedom
+%{_bindir}/listdoms
+%{_bindir}/createuser
 %{_sysconfdir}/collectd.conf.in
+%{python_sitelib}/nodeadmin
+%{python_sitelib}/nodeadmin- at VERSION@-py2.6.egg-info
 %config %attr(0644,root,root) %{_sysconfdir}/ovirt-release
 %config %attr(0644,root,root) %{_sysconfdir}/default/ovirt
 
-- 
1.6.2.5




More information about the ovirt-devel mailing list