[Ovirt-devel] [PATCH 1/2] Provides a new storage administration system to the managed node.

Joey Boggs jboggs at redhat.com
Mon Nov 9 19:54:50 UTC 2009


Darryl L. Pierce wrote:
> Users can now:
>  * Add a new storage pool.
>  * Delete a storage pool.
>  * Start and stop storage pools.
>  * Add a new storage volume.
>  * Delete a storage volume.
>  * List existing storage pools, with details.
>
> Signed-off-by: Darryl L. Pierce <dpierce at redhat.com>
> ---
>  Makefile.am                |   28 +++++--
>  nodeadmin/adddomain.py     |   15 +---
>  nodeadmin/addpool.py       |  183 ++++++++++++++++++++++++++++++++++++++++++++
>  nodeadmin/addvolume.py     |  177 ++++++++++++++++++++++++++++++++++++++++++
>  nodeadmin/configscreen.py  |   52 +++++++++++++
>  nodeadmin/createmeter.py   |   30 +++++++
>  nodeadmin/libvirtworker.py |   67 ++++++++++++++--
>  nodeadmin/listpools.py     |   63 +++++++++++++++
>  nodeadmin/mainmenu.py      |   24 ++++---
>  nodeadmin/poolconfig.py    |  143 ++++++++++++++++++++++++++++++++++
>  nodeadmin/removepool.py    |   72 +++++++++++++++++
>  nodeadmin/removevolume.py  |   76 ++++++++++++++++++
>  nodeadmin/setup.py.in      |    9 ++-
>  nodeadmin/startpool.py     |   62 +++++++++++++++
>  nodeadmin/stoppool.py      |   62 +++++++++++++++
>  nodeadmin/storagemenu.py   |   63 +++++++++++++++
>  nodeadmin/utils.py         |   10 +++
>  nodeadmin/volumeconfig.py  |   83 ++++++++++++++++++++
>  ovirt-node.spec.in         |    7 ++
>  19 files changed, 1186 insertions(+), 40 deletions(-)
>  create mode 100644 nodeadmin/addpool.py
>  create mode 100644 nodeadmin/addvolume.py
>  create mode 100644 nodeadmin/createmeter.py
>  create mode 100644 nodeadmin/listpools.py
>  create mode 100644 nodeadmin/poolconfig.py
>  create mode 100644 nodeadmin/removepool.py
>  create mode 100644 nodeadmin/removevolume.py
>  create mode 100644 nodeadmin/startpool.py
>  create mode 100644 nodeadmin/stoppool.py
>  create mode 100644 nodeadmin/storagemenu.py
>  create mode 100644 nodeadmin/volumeconfig.py
>
> diff --git a/Makefile.am b/Makefile.am
> index 3ce24c1..55ef277 100644
> --- a/Makefile.am
> +++ b/Makefile.am
> @@ -28,29 +28,39 @@ EXTRA_DIST =			\
>    images/syslinux-vesa-splash.jpg	\
>    nodeadmin/__init__.py         \
>    nodeadmin/adddomain.py        \
> +  nodeadmin/addpool.py          \
> +  nodeadmin/addvolume.py        \
>    nodeadmin/configscreen.py     \
> +  nodeadmin/createmeter.py      \
>    nodeadmin/createnetwork.py    \
>    nodeadmin/createuser.py       \
> +  nodeadmin/definenet.py        \
>    nodeadmin/destroynetwork.py   \
> +  nodeadmin/domainconfig.py     \
>    nodeadmin/halworker.py        \
>    nodeadmin/libvirtworker.py    \
> -  nodeadmin/userworker.py       \
> +  nodeadmin/listdomains.py      \
> +  nodeadmin/listnetworks.py     \
> +  nodeadmin/listpools.py        \
>    nodeadmin/mainmenu.py         \
>    nodeadmin/menuscreen.py       \
> +  nodeadmin/networkconfig.py    \
>    nodeadmin/netmenu.py          \
> +  nodeadmin/nodeadmin.py        \
>    nodeadmin/nodemenu.py         \
>    nodeadmin/removedomain.py     \
> -  nodeadmin/undefinenetwork.py  \
> +  nodeadmin/removepool.py       \
> +  nodeadmin/removevolume.py     \
> +  nodeadmin/setup.py            \
>    nodeadmin/startdomain.py      \
> +  nodeadmin/startpool.py        \
>    nodeadmin/stopdomain.py       \
> -  nodeadmin/definenet.py        \
> -  nodeadmin/domainconfig.py     \
> -  nodeadmin/networkconfig.py    \
> -  nodeadmin/listdomains.py      \
> -  nodeadmin/listnetworks.py     \
> -  nodeadmin/nodeadmin.py        \
> -  nodeadmin/setup.py            \
> +  nodeadmin/stoppool.py         \
> +  nodeadmin/storagemenu.py      \
> +  nodeadmin/undefinenetwork.py  \
> +  nodeadmin/userworker.py       \
>    nodeadmin/utils.py            \
> +  nodeadmin/volumeconfig.py     \
>    scripts/collectd.conf.in	\
>    scripts/ovirt			\
>    scripts/ovirt-awake		\
> diff --git a/nodeadmin/adddomain.py b/nodeadmin/adddomain.py
> index 70a2011..bb06a62 100755
> --- a/nodeadmin/adddomain.py
> +++ b/nodeadmin/adddomain.py
> @@ -20,11 +20,10 @@
>  
>  from snack import *
>  import os
> +from createmeter  import CreateMeter
>  from domainconfig import DomainConfig
>  from configscreen import ConfigScreen
> -import urlgrabber.progress as progress
>  import utils
> -import logging
>  
>  from virtinst import *
>  
> @@ -51,16 +50,6 @@ OS_VARIANT="os.variant"
>  MEMORY="memory"
>  CPUS="cpus"
>  
> -class DummyMeter(progress.BaseMeter):
> -    def _do_start(self, now = None):
> -        logging.info("Starting...")
> -
> -    def _do_end(self, amount_read, now = None):
> -        logging.info("Ending: read=%d" % amount_read)
> -
> -    def _do_update(self, amount_read, now = None):
> -        logging.info("Update: read=%d" % amount_read)
> -
>  class DomainConfigScreen(ConfigScreen):
>      def __init__(self):
>          ConfigScreen.__init__(self, "Create A New Virtual Machine")
> @@ -212,7 +201,7 @@ class DomainConfigScreen(ConfigScreen):
>              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.get_libvirt().define_domain(self.__config, CreateMeter())
>              self.set_finished()
>  
>      def get_back_page(self, page):
> diff --git a/nodeadmin/addpool.py b/nodeadmin/addpool.py
> new file mode 100644
> index 0000000..9fa1e7d
> --- /dev/null
> +++ b/nodeadmin/addpool.py
> @@ -0,0 +1,183 @@
> +# addstorage.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
> +import utils
> +
> +from configscreen import *
> +from poolconfig import PoolConfig
> +from virtinst import Storage
> +
> +POOL_NAME_PAGE    = 1
> +POOL_DETAILS_PAGE = 2
> +CONFIRM_PAGE      = 3
> +
> +class AddStoragePoolConfigScreen(ConfigScreen):
> +    def __init__(self):
> +        ConfigScreen.__init__(self, "Add A Storage Pool")
> +        self.__config = PoolConfig(self.get_libvirt())
> +
> +    def get_elements_for_page(self, screen, page):
> +        if   page is POOL_NAME_PAGE:    return self.get_pool_name_page(screen)
> +        elif page is POOL_DETAILS_PAGE: return self.get_pool_details_page(screen)
> +        elif page is CONFIRM_PAGE:      return self.get_confirm_page(screen)
> +
> +    def page_has_next(self, page):
> +        return page < CONFIRM_PAGE
> +
> +    def page_has_back(self, page):
> +        return page > POOL_NAME_PAGE
> +
> +    def page_has_finish(self, page):
> +        return page is CONFIRM_PAGE
> +
> +    def validate_input(self, page, errors):
> +        if   page is POOL_NAME_PAGE:
> +            if utils.string_is_not_blank(self.__name.value()):
> +                if self.get_libvirt().storage_pool_exists(self.__name.value()):
> +                    errors.append("Name '%s' already in use by another pool." % self.__name.value())
> +                else:
> +                    return True
> +            else:
> +                errors.append("Storage object name must be a string between 0 and 50 characters.")
> +        elif page is POOL_DETAILS_PAGE:
> +            result = True
> +            if self.__config.needs_target_path():
> +                if utils.string_is_not_blank(self.__target_path.value()):
> +                    if self.__target_path.value()[0:1] is not '/':
> +                        errors.append("'%s' is not an absolute path." % self.__target_path.value())
> +                        result = False
> +                else:
> +                    errors.append("You must enter a target path.")
> +                    result = False
> +            if self.__config.needs_format():
> +                if self.__formats.getSelection() is None:
> +                    errors.append("You must select a pool format.")
> +                    result = False
> +            if self.__config.needs_hostname():
> +                if not utils.string_is_not_blank(self.__hostname.value()):
> +                    errors.append("You must enter a hostname.")
> +                    result = False
> +            if self.__config.needs_source_path():
> +                if utils.string_is_not_blank(self.__source_path.value()):
> +                    if self.__config.source_must_be_absolute():
> +                        if self.__source_path.value()[0:1] is not '/':
> +                            errors.append("'%s' is not an absolute path." % self.__source_path.value())
> +                            result = False
> +                else:
> +                    errors.append("you  must enter a source path.")
> +                    result = False
> +            return result
> +        elif page is CONFIRM_PAGE: return True
> +        return False
> +
> +    def process_input(self, page):
> +        if page is POOL_NAME_PAGE:
> +            self.__config.set_name(self.__name.value())
> +            self.__config.set_type(self.__type.getSelection())
> +            #self._reset_flags(self.__type.current())
> +        elif page is POOL_DETAILS_PAGE:
> +            if self.__config.needs_target_path():
> +                self.__config.set_target_path(self.__target_path.value())
> +            if self.__config.needs_format():
> +                self.__config.set_format(self.__formats.getSelection())
> +            if self.__config.needs_hostname():
> +                self.__config.set_hostname(self.__hostname.value())
> +            if self.__config.needs_source_path():
> +                self.__config.set_source_path(self.__source_path.value())
> +            if self.__config.needs_build_pool():
> +                self.__config.set_build_pool(self.__build_pool.value())
> +        elif page is CONFIRM_PAGE:
> +            self.get_libvirt().define_storage_pool(self.__config.get_name(), config = self.__config)
> +            self.get_libvirt().create_storage_pool(self.__config.get_name())
> +            self.set_finished()
> +
> +    def get_pool_name_page(self, screen):
> +        self.__name = Entry(50, self.__config.get_name())
> +        pooltypes = []
> +        for pooltype in Storage.StoragePool.get_pool_types():
> +            pooltypes.append(["%s: %s" % (pooltype, Storage.StoragePool.get_pool_type_desc(pooltype)),
> +                              pooltype,
> +                              self.__config.get_type() is pooltype])
> +        self.__type = RadioBar(screen, pooltypes)
> +        grid = Grid(2, 2)
> +        grid.setField(Label("Name:"), 0, 0, anchorRight = 1)
> +        grid.setField(self.__name, 1, 0, anchorLeft = 1)
> +        grid.setField(Label("Type:"), 0, 1, anchorRight = 1, anchorTop = 1)
> +        grid.setField(self.__type, 1, 1, anchorLeft = 1)
> +        return [Label("Add Storage Pool"),
> +                grid]
> +
> +    def get_pool_details_page(self, screen):
> +        rows = 0
> +        if self.__config.needs_target_path():
> +            self.__target_path = Entry(50, self.__config.get_target_path())
> +            rows += 1
> +        if self.__config.needs_format():
> +            formats = []
> +            for format in self.__config.get_formats():
> +                formats.append([format, format, format is self.__config.get_format()])
> +            self.__formats = RadioBar(screen, formats)
> +            rows += 1
> +        if self.__config.needs_hostname():
> +            self.__hostname = Entry(50, self.__config.get_hostname())
> +            rows += 1
> +        if self.__config.needs_source_path():
> +            self.__source_path = Entry(50, self.__config.get_source_path())
> +            rows += 1
> +        if self.__config.needs_build_pool():
> +            self.__build_pool = Checkbox("Build Pool", self.__config.get_build_pool())
> +            rows += 1
> +        grid = Grid(2, rows)
> +        currentrow = 0
> +        if self.__config.needs_target_path():
> +            grid.setField(Label("Target Path:"), 0, currentrow, anchorRight = 1)
> +            grid.setField(self.__target_path, 1, currentrow, anchorLeft = 1)
> +            currentrow += 1
> +        if self.__config.needs_format():
> +            grid.setField(Label("Format:"), 0, currentrow, anchorRight = 1, anchorTop = 1)
> +            grid.setField(self.__formats, 1, currentrow, anchorLeft = 1)
> +            currentrow += 1
> +        if self.__config.needs_hostname():
> +            grid.setField(Label("Host Name:"), 0, currentrow, anchorRight = 1)
> +            grid.setField(self.__hostname, 1, currentrow, anchorRight = 1)
> +            currentrow += 1
> +        if self.__config.needs_source_path():
> +            grid.setField(Label("Source Path:"), 0, currentrow, anchorRight = 1)
> +            grid.setField(self.__source_path, 1, currentrow, anchorLeft = 1)
> +            currentrow += 1
> +        if self.__config.needs_build_pool():
> +            grid.setField(Label(" "), 0, currentrow, anchorRight = 1)
> +            grid.setField(self.__build_pool, 1, currentrow, anchorLeft = 1)
> +            currentrow += 1
> +        return [Label("Specify a storage location to be later split into virtual machine storage"),
> +                grid]
> +
> +    def get_confirm_page(self, screen):
> +        grid = Grid(2, 2)
> +        grid.setField(Label("Name:"), 0, 0, anchorRight = 1)
> +        grid.setField(Label(self.__config.get_name()), 1, 0, anchorLeft = 1)
> +        grid.setField(Label("Target Path:"), 0, 1, anchorRight = 1)
> +        grid.setField(Label(self.__config.get_target_path()), 1, 1, anchorLeft = 1)
> +        return [Label("Confirm Pool Details"),
> +                grid]
> +
> +def AddStoragePool():
> +    screen = AddStoragePoolConfigScreen()
> +    screen.start()
> diff --git a/nodeadmin/addvolume.py b/nodeadmin/addvolume.py
> new file mode 100644
> index 0000000..63cc54e
> --- /dev/null
> +++ b/nodeadmin/addvolume.py
> @@ -0,0 +1,177 @@
> +# addvolume.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 createmeter import CreateMeter
> +from configscreen import *
> +from volumeconfig import StorageVolumeConfig
> +from utils import *
> +
> +SELECT_POOL_PAGE   = 1
> +VOLUME_NAME_PAGE   = 2
> +VOLUME_FORMAT_PAGE = 3
> +MAX_CAPACITY_PAGE  = 4
> +CONFIRM_PAGE       = 5
> +
> +class AddVolumeConfigScreen(StorageListConfigScreen):
> +    def __init__(self):
> +        StorageListConfigScreen.__init__(self, "Add A New Storage Volume")
> +        self.__config = StorageVolumeConfig()
> +
> +    def get_elements_for_page(self, screen, page):
> +        if   page is SELECT_POOL_PAGE:   return self.get_storage_pool_list_page(screen)
> +        elif page is VOLUME_NAME_PAGE:   return self.get_volume_name_page(screen)
> +        elif page is VOLUME_FORMAT_PAGE: return self.get_volume_format_page(screen)
> +        elif page is MAX_CAPACITY_PAGE:  return self.get_max_capacity_page(screen)
> +        elif page is CONFIRM_PAGE:       return self.get_confirm_page(screen)
> +
> +    def page_has_next(self, page):
> +        if page is SELECT_POOL_PAGE:
> +            return self.has_selectable_pools()
> +        else:
> +            if page < CONFIRM_PAGE: return True
> +        return False
> +
> +    def page_has_back(self, page):
> +        if page > SELECT_POOL_PAGE: return True
> +        return False
> +
> +    def page_has_finish(self, page):
> +        return page is CONFIRM_PAGE
> +
> +    def get_next_page(self, page):
> +        if page is VOLUME_NAME_PAGE:
> +            if self.__config.needs_format():
> +                return VOLUME_FORMAT_PAGE
> +            else:
> +                return MAX_CAPACITY_PAGE
> +        return StorageListConfigScreen.get_next_page(self, page)
> +
> +    def get_back_page(self, page):
> +        if page is MAX_CAPACITY_PAGE:
> +            if self.__config.needs_format():
> +                return VOLUME_FORMAT_PATH
> +            else:
> +                return VOLUME_NAME_PAGE
> +        return StorageListConfigScreen.get_back_page(self, page)
> +
> +    def validate_input(self, page, errors):
> +        if page is SELECT_POOL_PAGE:
> +            if self.get_selected_pool() is not None:
> +                return True
> +            else:
> +                errors.append("You must select a storage pool.")
> +        elif page is VOLUME_NAME_PAGE:
> +            if string_is_not_blank(self.__name.value()):
> +                return True
> +            else:
> +                errors.append("Storage object name can only contain alphanumeric, '_', '.', or '-' characters.")
> +        elif page is VOLUME_FORMAT_PAGE:
> +            if self.__formats.current() is not None:
> +                return True
> +            else:
> +                errors.append("You must select a volume format.")
> +        elif page is MAX_CAPACITY_PAGE:
> +            if string_is_not_blank(self.__capacity.value()):
> +                if string_is_not_blank(self.__allocation.value()):
> +                    capacity = int(self.__capacity.value())
> +                    allocation = int(self.__allocation.value())
> +                    if capacity > 0:
> +                        if capacity <= self.__config.get_pool().info()[3] / 1024**2:
> +                            if allocation >= 0:
> +                                if allocation <= capacity:
> +                                    return True
> +                                else:
> +                                    errors.append("Allocation cannot exceed the maximum capacity.")
> +                            else:
> +                                errors.append("The allocation must be greater than or equal to 0.")
> +                        else:
> +                            errors.append("The maximum capacity cannot exceed the storage pool size.")
> +                    else:
> +                        errors.append("The capacity must be greater than zero.")
> +                else:
> +                    errors.append("An allocation value must be entered.")
> +            else:
> +                errors.append("A maximum volume capacity must be entered.")
> +        elif page is CONFIRM_PAGE: return True
> +        return False
> +
> +    def process_input(self, page):
> +        if page is SELECT_POOL_PAGE:
> +            self.__config.set_pool(self.get_libvirt().get_storage_pool(self.get_selected_pool()))
> +        elif page is VOLUME_NAME_PAGE:
> +            self.__config.set_name(self.__name.value())
> +        elif page is VOLUME_FORMAT_PAGE:
> +            self.__config.set_format(self.__formats.current())
> +        elif page is MAX_CAPACITY_PAGE:
> +            self.__config.set_max_capacity(int(self.__capacity.value()))
> +            self.__config.set_allocation(int(self.__allocation.value()))
> +        elif page is CONFIRM_PAGE:
> +            self.get_libvirt().define_storage_volume(self.__config, CreateMeter())
> +            self.set_finished()
> +
> +    def get_volume_name_page(self, screen):
> +        self.__name = Entry(50, self.__config.get_name())
> +        grid = Grid(2, 1)
> +        grid.setField(Label("Name:"), 0, 0, anchorRight = 1)
> +        grid.setField(self.__name, 1, 0, anchorLeft = 1)
> +        return [Label("New Storage Volume"),
> +                grid,
> +                Label("Name of the volume to create. File extension may be appended.")]
> +
> +    def get_volume_format_page(self, screen):
> +        self.__formats = Listbox(0)
> +        for format in self.__config.get_formats_for_pool():
> +            self.__formats.append(format, format)
> +        grid = Grid(1, 1)
> +        grid.setField(self.__formats, 0, 0)
> +        return [Label("Select The Volume Format"),
> +                grid]
> +
> +    def get_max_capacity_page(self, screen):
> +        self.__capacity = Entry(6, str(self.__config.get_max_capacity()))
> +        self.__allocation = Entry(6, str(self.__config.get_allocation()))
> +        grid = Grid(2, 2)
> +        grid.setField(Label("Max. Capacity (MB):"), 0, 0, anchorRight = 1)
> +        grid.setField(self.__capacity, 1, 0, anchorLeft = 1)
> +        grid.setField(Label("Allocation (MB):"), 0, 1, anchorRight = 1)
> +        grid.setField(self.__allocation, 1, 1, anchorLeft = 1)
> +        return [Label("Storage Volume Quota"),
> +                Label("%s's available space: %0.2f GB" % (self.__config.get_pool().name(),
> +                                                          self.__config.get_pool().info()[3] / 1024.0**3)),
> +                grid]
> +
> +    def get_confirm_page(self, screen):
> +        grid = Grid(2, 5)
> +        grid.setField(Label("Volume Name:"), 0, 0, anchorRight = 1)
> +        grid.setField(Label("%s (%s)" % (self.__config.get_name(), self.__config.get_pool().name())), 1, 0, anchorLeft = 1)
> +        if self.__config.needs_format():
> +            grid.setField(Label("Format:"), 0, 1, anchorRight = 1)
> +            grid.setField(Label(self.__config.get_format()), 1, 1, anchorLeft = 1)
> +        grid.setField(Label("Max. Capacity:"), 0, 2, anchorRight = 1)
> +        grid.setField(Label("%0.2f GB" % (self.__config.get_max_capacity() / 1024.0)), 1, 2, anchorLeft = 1)
> +        grid.setField(Label("Allocation:"), 0, 3, anchorRight = 1)
> +        grid.setField(Label("%0.2f GB" % (self.__config.get_allocation() / 1024.0)), 1, 3, anchorLeft = 1)
> +        return [Label("Ready To Allocation New Storage Volume"),
> +                grid]
> +
> +def AddStorageVolume():
> +    screen = AddVolumeConfigScreen()
> +    screen.start()
> diff --git a/nodeadmin/configscreen.py b/nodeadmin/configscreen.py
> index f214aea..7654697 100644
> --- a/nodeadmin/configscreen.py
> +++ b/nodeadmin/configscreen.py
> @@ -179,3 +179,55 @@ class NetworkListConfigScreen(ConfigScreen):
>  
>      def has_selectable_networks(self):
>          return self.__has_networks
> +
> +class StorageListConfigScreen(ConfigScreen):
> +    '''Provides a base class for any configuration screen that deals with storage pool lists.'''
> +
> +    def __init__(self, title):
> +        ConfigScreen.__init__(self, title)
> +
> +    def get_storage_pool_list_page(self, screen, defined=True, created=True):
> +        pools = self.get_libvirt().list_storage_pools(defined=defined, created=created)
> +        if len(pools) > 0:
> +            self.__has_pools = True
> +            self.__pools_list = Listbox(0)
> +            for pool in pools:
> +                self.__pools_list.append(pool, pool)
> +            result = self.__pools_list
> +        else:
> +            self.__has_pools = False
> +            result = Label("There are no storage pools available.")
> +        grid = Grid(1, 1)
> +        grid.setField(result, 0, 0)
> +        return [Label("Storage Pool List"),
> +                grid]
> +
> +    def get_selected_pool(self):
> +        return self.__pools_list.current()
> +
> +    def has_selectable_pools(self):
> +        return self.__has_pools
> +
> +    def get_storage_volume_list_page(self, screen):
> +        '''Requires that self.__pools_list have a selected element.'''
> +        pool = self.get_libvirt().get_storage_pool(self.get_selected_pool())
> +        if len(pool.listVolumes()) > 0:
> +            self.__has_volumes = True
> +            self.__volumes_list = Listbox(0)
> +            for volname in pool.listVolumes():
> +                volume = pool.storageVolLookupByName(volname)
> +                self.__volumes_list.append("%s (%0.2f GB)" % (volume.name(), volume.info()[2] / 1024**3), volume.name())
> +            result = self.__volumes_list
> +        else:
> +            self.__has_volumes = False
> +            result = Label("There are no storage volumes available.")
> +        grid = Grid(1, 1)
> +        grid.setField(result, 0, 0)
> +        return [Label("Storage Volume List"),
> +                grid]
> +
> +    def get_selected_volume(self):
> +        return self.__volumes_list.current()
> +
> +    def has_selectable_volumes(self):
> +        return self.__has_volumes
> diff --git a/nodeadmin/createmeter.py b/nodeadmin/createmeter.py
> new file mode 100644
> index 0000000..521e7d8
> --- /dev/null
> +++ b/nodeadmin/createmeter.py
> @@ -0,0 +1,30 @@
> +# createmeter.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 urlgrabber.progress as progress
> +import logging
> +
> +class CreateMeter(progress.BaseMeter):
> +    def _do_start(self, now = None):
> +        logging.info("Starting...")
> +
> +    def _do_end(self, amount_read, now = None):
> +        logging.info("Ending: read=%d" % amount_read)
> +
> +    def _do_update(self, amount_read, now = None):
> +        logging.info("Update: read=%d" % amount_read)
> diff --git a/nodeadmin/libvirtworker.py b/nodeadmin/libvirtworker.py
> index ba07605..b2acabe 100644
> --- a/nodeadmin/libvirtworker.py
> +++ b/nodeadmin/libvirtworker.py
> @@ -35,6 +35,10 @@ class LibvirtWorker:
>          self.__net.setup(self.__conn)
>          (self.__new_guest, self.__new_domain) = virtinst.CapabilitiesParser.guest_lookup(conn = self.__conn)
>  
> +    def get_connection(self):
> +        '''Returns the underlying connection.'''
> +        return self.__conn
> +
>      def list_domains(self, defined = True, started = True):
>          '''Lists all domains.'''
>          result = []
> @@ -134,9 +138,12 @@ class LibvirtWorker:
>          network = self.get_network(name)
>          network.undefine()
>  
> -    def list_storage_pools(self):
> +    def list_storage_pools(self, defined=True, created=True):
>          '''Returns the list of all defined storage pools.'''
> -        return self.__conn.listStoragePools()
> +        pools = []
> +        if defined: pools.extend(self.__conn.listDefinedStoragePools())
> +        if created: pools.extend(self.__conn.listStoragePools())
> +        return pools
>  
>      def storage_pool_exists(self, name):
>          '''Returns whether a storage pool exists.'''
> @@ -144,16 +151,62 @@ class LibvirtWorker:
>          if name in pools: return True
>          return False
>  
> -    def define_storage_pool(self, name):
> +    def create_storage_pool(self, name):
> +        '''Starts the named storage pool if it is not currently started.'''
> +        if name not in self.list_storage_pools(defined = False):
> +            pool = self.get_storage_pool(name)
> +            pool.create(0)
> +
> +    def destroy_storage_pool(self, name):
> +        '''Stops the specified storage pool.'''
> +        if name in self.list_storage_pools(defined = False):
> +            pool = self.get_storage_pool(name)
> +            pool.destroy()
> +
> +    def define_storage_pool(self, name, config = None, meter = None):
>          '''Defines a storage pool with the given name.'''
> -        try:
> +        if config is None:
>              pool = virtinst.Storage.DirectoryPool(conn=self.__conn,
>                                                    name=name,
>                                                    target_path=DEFAULT_POOL_TARGET_PATH)
> -            newpool = pool.install(build=True, create=True)
> +            newpool = pool.install(build=True, create=True, meter=meter)
>              newpool.setAutostart(True)
> -        except Exception, error:
> -            raise RuntimeError("Could not create pool: %s - %s", str(error))
> +        else:
> +            pool = config.get_pool()
> +            pool.target_path = config.get_target_path()
> +            if config.needs_hostname():
> +                pool.host = config.get_hostname()
> +            if config.needs_source_path():
> +                pool.source_path = config.get_source_path()
> +            if config.needs_format():
> +                pool.format = config.get_format()
> +            pool.conn = self.__conn
> +            pool.get_xml_config()
> +            newpool = pool.install(meter=meter,
> +                                   build=True, # config.get_build_pool(),
> +                                   create=True)
> +            newpool.setAutostart(True)
> +
> +    def undefine_storage_pool(self, name):
> +        '''Undefines the specified storage pool.'''
> +        pool = self.get_storage_pool(name)
> +        pool.undefine()
> +
> +    def get_storage_pool(self, name):
> +        '''Returns the storage pool with the specified name.'''
> +        return self.__conn.storagePoolLookupByName(name)
> +
> +    def define_storage_volume(self, config, meter):
> +        '''Defines a new storage volume.'''
> +        self.create_storage_pool(config.get_pool().name())
> +        volume = config.create_volume()
> +        volume.install(meter = meter)
> +
> +    def remove_storage_volume(self, poolname, volumename):
> +        '''Removes the specified storage volume.'''
> +        pool = self.get_storage_pool(poolname)
> +        volume = pool.storageVolLookupByName(volumename)
> +        volume.delete(0)
>  
>      def list_bridges(self):
>          '''Lists all defined and active bridges.'''
> diff --git a/nodeadmin/listpools.py b/nodeadmin/listpools.py
> new file mode 100644
> index 0000000..686c42d
> --- /dev/null
> +++ b/nodeadmin/listpools.py
> @@ -0,0 +1,63 @@
> +# listpools.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 *
> +
> +LIST_PAGE    = 1
> +DETAILS_PAGE = 2
> +
> +class ListStoragePoolsConfigScreen(StorageListConfigScreen):
> +    def __init__(self):
> +        StorageListConfigScreen.__init__(self, "List Storage Pools")
> +
> +    def get_elements_for_page(self, screen, page):
> +        if   page is LIST_PAGE:    return self.get_storage_pool_list_page(screen)
> +        elif page is DETAILS_PAGE: return self.get_pool_details_page(screen)
> +
> +    def page_has_next(self, page):
> +        if page is LIST_PAGE and self.has_selectable_pools():
> +                return True
> +        return False
> +
> +    def page_has_back(self, page):
> +        if page is DETAILS_PAGE: return True
> +        return False
> +
> +    def get_pool_details_page(self, screen):
> +        pool = self.get_libvirt().get_storage_pool(self.get_selected_pool())
> +        volumes = Listbox(0);
> +        for name in pool.listVolumes():
> +            volume = pool.storageVolLookupByName(name)
> +            volumes.append("%s (%0.1f G)" % (name, volume.info()[1] / 1024**3), name)
> +        grid = Grid(2, 3)
> +        grid.setField(Label("Name:"), 0, 0, anchorRight = 1)
> +        grid.setField(Label(pool.name()), 1, 0, anchorLeft = 1)
> +        grid.setField(Label("Volumes:"), 0, 1, anchorRight = 1)
> +        grid.setField(volumes, 1, 1, anchorLeft = 1)
> +        grid.setField(Label("Autostart:"), 0, 2, anchorRight = 1)
> +        label = "No"
> +        if pool.autostart(): label = "Yes"
> +        grid.setField(Label(label), 1, 2, anchorLeft = 1)
> +        return [Label("Details For Storage Pool: %s" % self.get_selected_pool()),
> +                grid]
> +
> +def ListStoragePools():
> +    screen = ListStoragePoolsConfigScreen()
> +    screen.start()
> diff --git a/nodeadmin/mainmenu.py b/nodeadmin/mainmenu.py
> index 73501fa..52d9298 100755
> --- a/nodeadmin/mainmenu.py
> +++ b/nodeadmin/mainmenu.py
> @@ -19,28 +19,32 @@
>  from snack import *
>  import traceback
>  
> -from menuscreen     import MenuScreen
> -from nodemenu       import NodeMenu
> -from netmenu        import NetworkMenu
> +from menuscreen  import MenuScreen
> +from nodemenu    import NodeMenu
> +from netmenu     import NetworkMenu
> +from storagemenu import StoragePoolMenu
>  
>  import utils
>  import logging
>  
>  NODE_MENU    = 1
>  NETWORK_MENU = 2
> -EXIT_CONSOLE = 99
> +STORAGE_MENU = 3
> +EXIT_CONSOLE = 4
>  
>  class MainMenuScreen(MenuScreen):
>      def __init__(self):
>          MenuScreen.__init__(self, "Main Menu")
>  
>      def get_menu_items(self):
> -        return (("Node Administration", NODE_MENU),
> -                ("Network Administration", NETWORK_MENU))
> -
> -    def handle_selection(self, page):
> -        if   page is NODE_MENU:    NodeMenu()
> -        elif page is NETWORK_MENU: NetworkMenu()
> +        return (("Node Administration",         NODE_MENU),
> +                ("Network Administration",      NETWORK_MENU),
> +                ("Storage Pool Administration", STORAGE_MENU))
> +
> +    def handle_selection(self, item):
> +        if   item is NODE_MENU:    NodeMenu()
> +        elif item is NETWORK_MENU: NetworkMenu()
> +        elif item is STORAGE_MENU: StoragePoolMenu()
>  
>  def MainMenu():
>      screen = MainMenuScreen()
> diff --git a/nodeadmin/poolconfig.py b/nodeadmin/poolconfig.py
> new file mode 100644
> index 0000000..6ece6c7
> --- /dev/null
> +++ b/nodeadmin/poolconfig.py
> @@ -0,0 +1,143 @@
> +# poolconfig.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 Storage
> +
> +ROOT_TARGET_PATH="/var/lib/libvirt/images/%s"
> +
> +class PoolConfig:
> +    def __init__(self, libvirt):
> +        self.__libvirt = libvirt
> +        self.__name = ""
> +        self.set_type(None)
> +        self.__format = None
> +        self.__hostname = ""
> +        self.__target_path = ""
> +        self.__source_path = ""
> +        self.__build_pool  = False
> +
> +    def get_pool(self):
> +        return self.__pool
> +
> +    def set_name(self, name):
> +        self.__name = name
> +
> +    def get_name(self):
> +        return self.__name
> +
> +    def set_type(self, pooltype):
> +        self.__type = pooltype
> +        self.__needs_target_path = False
> +        self.__needs_format      = False
> +        self.__needs_hostname    = False
> +        self.__needs_source_path = False
> +        self.__needs_build_pool  = False
> +        if pooltype is not None:
> +            if   pooltype is Storage.StoragePool.TYPE_DIR:
> +                self.__needs_target_path = True
> +                self.__target_path = ROOT_TARGET_PATH % self.__name
> +                self.__build_pool = True
> +            elif pooltype is Storage.StoragePool.TYPE_DISK:
> +                self.__needs_target_path = True
> +                self.__needs_format      = True
> +                self.__needs_source_path = True
> +                self.__needs_build_pool  = True
> +            elif pooltype is Storage.StoragePool.TYPE_FS:
> +                self.__needs_target_path = True
> +                self.__needs_format      = True
> +                self.__needs_source_path = True
> +                self.__build_pool  = True
> +            elif pooltype is Storage.StoragePool.TYPE_ISCSI:
> +                self.__needs_target_path = True
> +                self.__needs_hostname    = True
> +                self.__needs_source_path = True
> +                self.__build_pool  = False
> +            elif pooltype is Storage.StoragePool.TYPE_LOGICAL:
> +                self.__needs_target_path = True
> +                self.__needs_source_path = True
> +                self.__needs_build_pool  = True
> +            elif pooltype is Storage.StoragePool.TYPE_NETFS:
> +                self.__needs_target_path = True
> +                self.__needs_format      = True
> +                self.__needs_hostname    = True
> +                self.__needs_source_path = True
> +                self.__build_pool  = True
> +            # create pool
> +            pool_class = Storage.StoragePool.get_pool_class(self.__type)
> +            self.__pool = pool_class(name = self.__name,
> +                                     conn = self.__libvirt.get_connection())
> +            if self.__needs_format:
> +                self.__format = self.__pool.formats[0]
> +        else:
> +            self.__type = Storage.StoragePool.get_pool_types()[0]
> +
> +    def get_type(self):
> +        return self.__type
> +
> +    def needs_target_path(self):
> +        return self.__needs_target_path
> +
> +    def needs_format(self):
> +        return self.__needs_format
> +
> +    def needs_hostname(self):
> +        return self.__needs_hostname
> +
> +    def source_must_be_absolute(self):
> +        if self.__type is Storage.StoragePool.TYPE_ISCSI:
> +            return False
> +        return True
> +
> +    def needs_source_path(self):
> +        return self.__needs_source_path
> +
> +    def needs_build_pool(self):
> +        return self.__needs_build_pool
> +
> +    def set_target_path(self, path):
> +        self.__target_path = path
> +
> +    def get_target_path(self):
> +        return self.__target_path
> +
> +    def get_formats(self):
> +        return self.__pool.formats
> +
> +    def set_format(self, format):
> +        self.__format = format
> +
> +    def get_format(self):
> +        return self.__format
> +
> +    def set_hostname(self, hostname):
> +        self.__hostname = hostname
> +
> +    def get_hostname(self):
> +        return self.__hostname
> +
> +    def set_source_path(self, source_path):
> +        self.__source_path = source_path
> +
> +    def get_source_path(self):
> +        return self.__source_path
> +
> +    def set_build_pool(self, build_pool):
> +        self.__build_pool = build_pool
> +
> +    def  get_build_pool(self):
> +        return self.__build_pool
> diff --git a/nodeadmin/removepool.py b/nodeadmin/removepool.py
> new file mode 100644
> index 0000000..7a7f46d
> --- /dev/null
> +++ b/nodeadmin/removepool.py
> @@ -0,0 +1,72 @@
> +#!/usr/bin/env python
> +#
> +# removepool.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 *
> +
> +LIST_POOLS_PAGE    = 1
> +CONFIRM_PAGE       = 2
> +
> +class RemoveStoragePoolConfigScreen(StorageListConfigScreen):
> +    def __init__(self):
> +        StorageListConfigScreen.__init__(self, "Remove A Storage Pool")
> +
> +    def get_elements_for_page(self, screen, page):
> +        if   page is LIST_POOLS_PAGE: return self.get_storage_pool_list_page(screen)
> +        elif page is CONFIRM_PAGE:    return self.get_confirm_page(screen)
> +
> +    def page_has_next(self, page):
> +        return page is LIST_POOLS_PAGE and self.has_selectable_pools()
> +
> +    def page_has_back(self, page):
> +        return False
> +
> +    def page_has_finish(self, page):
> +        return page is CONFIRM_PAGE
> +
> +    def validate_input(self, page, errors):
> +        if page is LIST_POOLS_PAGE:
> +            if self.get_selected_pool() is not None:
> +                return True
> +            else:
> +                errors.append("Please select a storage pool to be removed.")
> +        elif page is CONFIRM_PAGE:
> +            if self.__confirm.value():
> +                return True
> +            else:
> +                errors.append("You must confirm removing a storage pool.")
> +        return False
> +
> +    def process_input(self, page):
> +        if page is CONFIRM_PAGE:
> +            self.get_libvirt().destroy_storage_pool(self.get_selected_pool())
> +            self.get_libvirt().undefine_storage_pool(self.get_selected_pool())
> +            self.set_finished()
> +
> +    def get_confirm_page(self, screen):
> +        self.__confirm = Checkbox("Check here to confirm deleting pool: %s" % self.get_selected_pool())
> +        grid = Grid(1, 1)
> +        grid.setField(self.__confirm, 0, 0)
> +        return [Label("Remove Selected Storage Pool"),
> +                grid]
> +
> +def RemoveStoragePool():
> +    screen = RemoveStoragePoolConfigScreen()
> +    screen.start()
> diff --git a/nodeadmin/removevolume.py b/nodeadmin/removevolume.py
> new file mode 100644
> index 0000000..5ad3058
> --- /dev/null
> +++ b/nodeadmin/removevolume.py
> @@ -0,0 +1,76 @@
> +# removevolume.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 createmeter import CreateMeter
> +from configscreen import *
> +from volumeconfig import StorageVolumeConfig
> +from utils import *
> +
> +SELECT_POOL_PAGE   = 1
> +SELECT_VOLUME_PAGE = 2
> +CONFIRM_PAGE       = 3
> +
> +class RemoveVolumeConfigScreen(StorageListConfigScreen):
> +    def __init__(self):
> +        StorageListConfigScreen.__init__(self, "Add A New Storage Volume")
> +        self.__config = StorageVolumeConfig()
> +
> +    def get_elements_for_page(self, screen, page):
> +        if   page is SELECT_POOL_PAGE:   return self.get_storage_pool_list_page(screen)
> +        elif page is SELECT_VOLUME_PAGE: return self.get_storage_volume_list_page(screen)
> +        elif page is CONFIRM_PAGE:       return self.get_confirm_page(screen)
> +
> +    def page_has_next(self, page):
> +        if   page is SELECT_POOL_PAGE:   return self.has_selectable_pools()
> +        elif page is SELECT_VOLUME_PAGE: return self.has_selectable_volumes()
> +        return False
> +
> +    def validate_input(self, page, errors):
> +        if   page is SELECT_POOL_PAGE:   return self.get_selected_pool() is not None
> +        elif page is SELECT_VOLUME_PAGE: return self.get_selected_volume() is not None
> +        elif page is CONFIRM_PAGE:
> +            if self.__confirm.value():
> +                return True
> +            else:
> +                errors.append("You must confirm deleting a storage volume.")
> +        return False
> +
> +    def process_input(self, page):
> +        if page is CONFIRM_PAGE:
> +            self.get_libvirt().remove_storage_volume(self.get_selected_pool(), self.get_selected_volume())
> +            self.set_finished()
> +
> +    def page_has_back(self, page):
> +        return page > SELECT_POOL_PAGE
> +
> +    def page_has_finish(self, page):
> +        return page is CONFIRM_PAGE
> +
> +    def get_confirm_page(self, screen):
> +        self.__confirm = Checkbox("Check here to confirm deleting volume: %s" % self.get_selected_volume())
> +        grid = Grid(1, 1)
> +        grid.setField(self.__confirm, 0, 0)
> +        return [Label("Remove Selected Storage Volume"),
> +                grid]
> +
> +def RemoveStorageVolume():
> +    screen = RemoveVolumeConfigScreen()
> +    screen.start()
> diff --git a/nodeadmin/setup.py.in b/nodeadmin/setup.py.in
> index 1e6e028..17bfe93 100644
> --- a/nodeadmin/setup.py.in
> +++ b/nodeadmin/setup.py.in
> @@ -35,5 +35,12 @@ setup(name = "nodeadmin",
>              'createnet   = nodeadmin.createnetwork:CreateNetwork',
>              'destroynet  = nodeadmin.destroynetwork:DestroyNetwork',
>              'undefinenet = nodeadmin.undefinenetwork:UndefineNetwork',
> -            'listnets    = nodeadmin.listnetworks:ListNetworks']
> +            'listnets    = nodeadmin.listnetworks:ListNetworks',
> +            'addpool     = nodeadmin.addpool:AddStoragePool',
> +            'rmpool      = nodeadmin.removepool:RemoveStoragePool',
> +            'startpool   = nodeadmin.startpool:StartStoragePool',
> +            'stoppool    = nodeadmin.stoppool:StopStoragePool',
> +            'addvolume   = nodeadmin.addvolume:AddStorageVolume',
> +            'rmvolume    = nodeadmin.removevolume:RemoveStorageVolume',
> +            'listpools   = nodeadmin.listpools:ListPools']
>          })
> diff --git a/nodeadmin/startpool.py b/nodeadmin/startpool.py
> new file mode 100644
> index 0000000..8a84512
> --- /dev/null
> +++ b/nodeadmin/startpool.py
> @@ -0,0 +1,62 @@
> +#!/usr/bin/env python
> +#
> +# startpool.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 *
> +
> +LIST_POOLS_PAGE    = 1
> +FINAL_PAGE         = 2
> +
> +class StartStoragePoolConfigScreen(StorageListConfigScreen):
> +    def __init__(self):
> +        StorageListConfigScreen.__init__(self, "Start A Storage Pool")
> +
> +    def get_elements_for_page(self, screen, page):
> +        if   page is LIST_POOLS_PAGE: return self.get_storage_pool_list_page(screen, created = False)
> +        elif page is FINAL_PAGE:      return self.get_final_page(screen)
> +
> +    def page_has_next(self, page):
> +        return page is LIST_POOLS_PAGE and self.has_selectable_pools()
> +
> +    def page_has_back(self, page):
> +        return False
> +
> +    def page_has_finish(self, page):
> +        return page is FINAL_PAGE
> +
> +    def validate_input(self, page, errors):
> +        if page is LIST_POOLS_PAGE:
> +            if self.get_selected_pool() is not None:
> +                return True
> +            else:
> +                errors.append("Please select a storage pool to be started.")
> +        return False
> +
> +    def process_input(self, page):
> +        if page is LIST_POOLS_PAGE:
> +            self.get_libvirt().create_storage_pool(self.get_selected_pool())
> +            self.set_finished()
> +
> +    def get_final_page(self, screen):
> +        return [Label("Storage pool started: %s" % self.get_selected_pool())]
> +
> +def StartStoragePool():
> +    screen = StartStoragePoolConfigScreen()
> +    screen.start()
> diff --git a/nodeadmin/stoppool.py b/nodeadmin/stoppool.py
> new file mode 100644
> index 0000000..0522b95
> --- /dev/null
> +++ b/nodeadmin/stoppool.py
> @@ -0,0 +1,62 @@
> +#!/usr/bin/env python
> +#
> +# stoppool.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 *
> +
> +LIST_POOLS_PAGE    = 1
> +FINAL_PAGE         = 2
> +
> +class StopStoragePoolConfigScreen(StorageListConfigScreen):
> +    def __init__(self):
> +        StorageListConfigScreen.__init__(self, "Stop A Storage Pool")
> +
> +    def get_elements_for_page(self, screen, page):
> +        if   page is LIST_POOLS_PAGE: return self.get_storage_pool_list_page(screen, defined = False)
> +        elif page is FINAL_PAGE:      return self.get_final_page(screen)
> +
> +    def page_has_next(self, page):
> +        return page is LIST_POOLS_PAGE and self.has_selectable_pools()
> +
> +    def page_has_back(self, page):
> +        return False
> +
> +    def page_has_finish(self, page):
> +        return page is FINAL_PAGE
> +
> +    def validate_input(self, page, errors):
> +        if page is LIST_POOLS_PAGE:
> +            if self.get_selected_pool() is not None:
> +                return True
> +            else:
> +                errors.append("Please select a storage pool to be stopped.")
> +        return False
> +
> +    def process_input(self, page):
> +        if page is LIST_POOLS_PAGE:
> +            self.get_libvirt().destroy_storage_pool(self.get_selected_pool())
> +            self.set_finished()
> +
> +    def get_final_page(self, screen):
> +        return [Label("Storage pool stopped: %s" % self.get_selected_pool())]
> +
> +def StopStoragePool():
> +    screen = StopStoragePoolConfigScreen()
> +    screen.start()
> diff --git a/nodeadmin/storagemenu.py b/nodeadmin/storagemenu.py
> new file mode 100644
> index 0000000..0b56dae
> --- /dev/null
> +++ b/nodeadmin/storagemenu.py
> @@ -0,0 +1,63 @@
> +# storagemenu.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 menuscreen    import MenuScreen
> +from addpool       import AddStoragePool
> +from startpool     import StartStoragePool
> +from stoppool      import StopStoragePool
> +from removepool    import RemoveStoragePool
> +from addvolume     import AddStorageVolume
> +from removevolume  import RemoveStorageVolume
> +from listpools     import ListStoragePools
> +
> +ADD_POOL      = 1
> +START_POOL    = 2
> +STOP_POOL     = 3
> +REMOVE_POOL   = 4
> +ADD_VOLUME    = 5
> +REMOVE_VOLUME = 6
> +LIST_POOLS    = 7
> +
> +class StoragePoolMenuScreen(MenuScreen):
> +    def __init__(self):
> +        MenuScreen.__init__(self, "Storage Pool Administration")
> +
> +    def get_menu_items(self):
> +        return (("Add A Storage Pool",      ADD_POOL),
> +                ("Start A Storage Pool",    START_POOL),
> +                ("Stop A Storage Pool",     STOP_POOL),
> +                ("Remove A Storage Pool",   REMOVE_POOL),
> +                ("Add A Storage Volume",    ADD_VOLUME),
> +                ("Remove A Storage Volume", REMOVE_VOLUME),
> +                ("List Storage Pools",      LIST_POOLS))
> +
> +    def handle_selection(self, item):
> +        if   item is ADD_POOL:      AddStoragePool()
> +        elif item is START_POOL:    StartStoragePool()
> +        elif item is STOP_POOL:     StopStoragePool()
> +        elif item is REMOVE_POOL:   RemoveStoragePool()
> +        elif item is ADD_VOLUME:    AddStorageVolume()
> +        elif item is REMOVE_VOLUME: RemoveStorageVolume()
> +        elif item is LIST_POOLS:    ListStoragePools()
> +
> +def StoragePoolMenu():
> +    screen = StoragePoolMenuScreen()
> +    screen.start()
> diff --git a/nodeadmin/utils.py b/nodeadmin/utils.py
> index 55a838c..28ccb8b 100644
> --- a/nodeadmin/utils.py
> +++ b/nodeadmin/utils.py
> @@ -17,9 +17,19 @@
>  # also available at http://www.gnu.org/copyleft/gpl.html.
>  
>  import logging
> +import re
>  
>  logging.basicConfig(level=logging.DEBUG,
>                      format='%(asctime)s %(levelname)-8s %(message)s',
>                      datefmt='%a, %d %b %Y %H:%M:%S',
>                      filename='/var/log/ovirt-nodeadmin.log',
>                      filemode='w')
> +
> +def string_is_not_blank(value):
> +    if len(value) > 0: return True
> +    return False
> +
> +def string_has_no_spaces(value):
> +    if re.match("^[a-zA-Z0-9_]*$", value):
> +        return True
> +    return False
> diff --git a/nodeadmin/volumeconfig.py b/nodeadmin/volumeconfig.py
> new file mode 100644
> index 0000000..86ada74
> --- /dev/null
> +++ b/nodeadmin/volumeconfig.py
> @@ -0,0 +1,83 @@
> +# volumeconfig.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 virtinst
> +from virtinst import Storage
> +
> +class StorageVolumeConfig:
> +    def __init__(self):
> +        self.__pool = None
> +        self.__name = ""
> +        self.__formats = None
> +        self.__format = None
> +        self.__max_capacity = 10000
> +        self.__allocation = 0
> +
> +    def set_pool(self, pool):
> +        self.__pool = pool
> +        self.__formats = None
> +        self.__pool_type = virtinst.util.get_xml_path(self.__pool.XMLDesc(0), '/pool/@type')
> +        self.__volume_class = Storage.StoragePool.get_volume_for_pool(self.__pool_type)
> +
> +    def get_pool(self):
> +        return self.__pool
> +
> +    def create_volume(self):
> +        volume = self.__volume_class(name       = self.__name + ".img",
> +                                     allocation = self.__allocation * 1024**2,
> +                                     capacity   = self.__max_capacity * 1024**2,
> +                                     pool       = self.__pool)
> +        volume.pool = self.__pool
> +        if self.needs_format():
> +            volume.format = self.__format
> +        return volume
> +
> +    def set_name(self, name):
> +        self.__name = name
> +
> +    def get_name(self):
> +        return self.__name
> +
> +    def needs_format(self):
> +        if self.__pool.__dict__.keys().count("get_formats_for_pool") > 0:
> +            return self.__pool.get_formats_for_pool() is not 0
> +        else:
> +            return False
> +
> +    def get_formats_for_pool(self):
> +        if self.__formats is None:
> +            self.__formats = self.__volume_class.formats
> +        return self.__formats
> +
> +    def set_format(self, format):
> +        self.__format = format
> +
> +    def get_format(self):
> +        return self.__format
> +
> +    def set_max_capacity(self, capacity):
> +        self.__max_capacity = capacity
> +
> +    def get_max_capacity(self):
> +        return self.__max_capacity
> +
> +    def set_allocation(self, allocation):
> +        self.__allocation = allocation
> +
> +    def get_allocation(self):
> +        return self.__allocation
> diff --git a/ovirt-node.spec.in b/ovirt-node.spec.in
> index d23a4ef..6509fa0 100644
> --- a/ovirt-node.spec.in
> +++ b/ovirt-node.spec.in
> @@ -369,11 +369,18 @@ fi
>  %{_initrddir}/ovirt-functions
>  %defattr(-,root,root,0644)
>  %{_bindir}/nodeadmin
> +%{_bindir}/addpool
>  %{_bindir}/addvm
> +%{_bindir}/addvolume
>  %{_bindir}/startvm
>  %{_bindir}/stopvm
>  %{_bindir}/rmvm
> +%{_bindir}/listpools
>  %{_bindir}/listvms
> +%{_bindir}/rmpool
> +%{_bindir}/rmvolume
> +%{_bindir}/startpool
> +%{_bindir}/stoppool
>  %{_bindir}/definenet
>  %{_bindir}/createnet
>  %{_bindir}/destroynet
>   
ACK pending below comments:

For lvm -It  creates a pool fine, when creating a volume its missing the 
volume group name when running lvcreate

+-----+ An Exception Has Occurred +-----+
Couldn't create storage volume '1.img': 'internal error  '/sbin/lvcreate 
--name 1.img -L 1024K /tmp/lvm' exited with non-zero status 3 and signal 0
Volume group name expected (no slash) Run `lvcreate --help' for more 
information.   

iscsi creates a pool fine but when you reach creating a volume it tells 
you it's no implemented, any way to block this option for even being 
available or alert the user ahead of time?


All the other options create pools/volumes fine




More information about the ovirt-devel mailing list