From 90a1c65f3ac90b1077eb3ea2f5fbe8a039ee9290 Mon Sep 17 00:00:00 2001
From: Dean Troyer <dtroyer@gmail.com>
Date: Mon, 20 Aug 2012 18:02:30 -0500
Subject: [PATCH] Update compute client bits

* add server create, delete, pause, reboot, rebuild
  resume, suspend, unpause commands

Change-Id: I728ec199e4562bd621c3a73106c90d8b790b459a
---
 openstackclient/compute/client.py    |  17 +-
 openstackclient/compute/v2/server.py | 570 +++++++++++++++++++++++++--
 setup.py                             |  29 +-
 3 files changed, 576 insertions(+), 40 deletions(-)

diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py
index a59b6e0063..3c17b17ad6 100644
--- a/openstackclient/compute/client.py
+++ b/openstackclient/compute/client.py
@@ -17,19 +17,28 @@
 
 import logging
 
-from novaclient import client as nova_client
+from openstackclient.common import exceptions as exc
+from openstackclient.common import utils
 
 LOG = logging.getLogger(__name__)
 
 API_NAME = 'compute'
+API_VERSIONS = {
+    '1.1': 'novaclient.v1_1.client.Client',
+    '2': 'novaclient.v1_1.client.Client',
+}
 
 
 def make_client(instance):
     """Returns a compute service client.
     """
-    LOG.debug('instantiating compute client')
-    client = nova_client.Client(
-        version=instance._api_version[API_NAME],
+    compute_client = utils.get_client_class(
+        API_NAME,
+        instance._api_version[API_NAME],
+        API_VERSIONS,
+    )
+    LOG.debug('instantiating compute client: %s' % compute_client)
+    client = compute_client(
         username=instance._username,
         api_key=instance._password,
         project_id=instance._tenant_name,
diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py
index e72c5735ec..e496bd459a 100644
--- a/openstackclient/compute/v2/server.py
+++ b/openstackclient/compute/v2/server.py
@@ -21,11 +21,15 @@ Server action implementations
 
 import logging
 import os
+import sys
+import time
 
 from cliff import command
 from cliff import lister
 from cliff import show
 
+from novaclient.v1_1 import servers
+from openstackclient.common import exceptions
 from openstackclient.common import utils
 
 
@@ -33,6 +37,7 @@ def _format_servers_list_networks(server):
     """Return a string containing the networks a server is attached to.
 
     :param server: a single Server resource
+    :rtype: a string of formatted network addresses
     """
     output = []
     for (network, addresses) in server.networks.items():
@@ -44,6 +49,318 @@ def _format_servers_list_networks(server):
     return '; '.join(output)
 
 
+def _prep_server_detail(compute_client, server):
+    """Prepare the detailed server dict for printing
+
+    :param compute_client: a compute client instance
+    :param server: a Server resource
+    :rtype: a dict of server details
+    """
+    info = server._info.copy()
+
+    # Call .get() to retrieve all of the server information
+    # as findall(name=blah) and REST /details are not the same
+    # and do not return flavor and image information.
+    server = compute_client.servers.get(info['id'])
+    info.update(server._info)
+
+    # Convert the image blob to a name
+    image_info = info.get('image', {})
+    image_id = image_info.get('id', '')
+    image = utils.find_resource(compute_client.images, image_id)
+    info['image'] = "%s (%s)" % (image.name, image_id)
+
+    # Convert the flavor blob to a name
+    flavor_info = info.get('flavor', {})
+    flavor_id = flavor_info.get('id', '')
+    flavor = utils.find_resource(compute_client.flavors, flavor_id)
+    info['flavor'] = "%s (%s)" % (flavor.name, flavor_id)
+
+    # NOTE(dtroyer): novaclient splits these into separate entries...
+    # Format addresses in a useful way
+    info['addresses'] = _format_servers_list_networks(server)
+
+    # Remove values that are long and not too useful
+    info.pop('links', None)
+
+    return info
+
+
+def _wait_for_status(poll_fn, obj_id, final_ok_states, poll_period=5,
+                     status_field="status"):
+    """Block while an action is being performed
+
+    :param poll_fn: a function to retrieve the state of the object
+    :param obj_id: the id of the object
+    :param final_ok_states: a tuple of the states of the object that end the
+        wait as success, ex ['active']
+    :param poll_period: the wait time between checks of object status
+    :param status_field: field name containing the status to be checked
+    """
+    log = logging.getLogger(__name__ + '._wait_for_status')
+    while True:
+        obj = poll_fn(obj_id)
+
+        status = getattr(obj, status_field)
+
+        if status:
+            status = status.lower()
+
+        if status in final_ok_states:
+            log.debug('Wait terminated with success')
+            retval = True
+            break
+        elif status == "error":
+            log.error('Wait terminated with an error')
+            retval = False
+            break
+
+        time.sleep(poll_period)
+
+    return retval
+
+
+class CreateServer(show.ShowOne):
+    """Create server command"""
+
+    api = "compute"
+    log = logging.getLogger(__name__ + '.CreateServer')
+
+    def get_parser(self, prog_name):
+        parser = super(CreateServer, self).get_parser(prog_name)
+        parser.add_argument(
+            'server_name',
+            metavar='<server-name>',
+            help='New server name',
+        )
+        parser.add_argument(
+            '--image',
+            metavar='<image>',
+            required=True,
+            help='Create server from this image',
+            )
+        parser.add_argument(
+            '--flavor',
+            metavar='<flavor>',
+            required=True,
+            help='Create server with this flavor',
+            )
+        parser.add_argument(
+            '--security-group',
+            metavar='<security-group-name>',
+            action='append',
+            default=[],
+            help='Security group to assign to this server ' \
+                '(repeat for multiple groups)',
+            )
+        parser.add_argument(
+            '--key-name',
+            metavar='<key-name>',
+            help='Keypair to inject into this server (optional extension)',
+            )
+        parser.add_argument(
+            '--meta-data',
+            metavar='<key=value>',
+            action='append',
+            default=[],
+            help='Metadata to store for this server ' \
+                '(repeat for multiple values)',
+            )
+        parser.add_argument(
+            '--file',
+            metavar='<dest-filename=source-filename>',
+            action='append',
+            default=[],
+            help='File to inject into image before boot ' \
+                '(repeat for multiple files)',
+            )
+        parser.add_argument(
+            '--user-data',
+            metavar='<user-data>',
+            help='User data file to be serverd by the metadata server',
+            )
+        parser.add_argument(
+            '--availability-zone',
+            metavar='<zone-name>',
+            help='Keypair to inject into this server',
+            )
+        parser.add_argument(
+            '--block-device-mapping',
+            metavar='<dev-name=mapping>',
+            action='append',
+            default=[],
+            help='Map block devices; map is ' \
+                '<id>:<type>:<size(GB)>:<delete_on_terminate> ' \
+                '(optional extension)',
+            )
+        parser.add_argument(
+            '--nic',
+            metavar='<nic-config-string>',
+            action='append',
+            default=[],
+            help='Specify NIC configuration (optional extension)',
+            )
+        parser.add_argument(
+            '--hint',
+            metavar='<key=value>',
+            action='append',
+            default=[],
+            help='Hints for the scheduler (optional extension)',
+            )
+        parser.add_argument(
+            '--config-drive',
+            metavar='<config-drive-volume>|True',
+            default=False,
+            help='Use specified volume as the config drive, ' \
+                'or \'True\' to use an ephemeral drive',
+            )
+        parser.add_argument(
+            '--min',
+            metavar='<count>',
+            type=int,
+            default=1,
+            help='Minimum number of servers to launch (default=1)',
+            )
+        parser.add_argument(
+            '--max',
+            metavar='<count>',
+            type=int,
+            default=1,
+            help='Maximum number of servers to launch (default=1)',
+            )
+        parser.add_argument(
+            '--wait',
+            dest='wait',
+            action='store_true',
+            help='Wait for server to become active to return',
+            )
+        return parser
+
+    def take_action(self, parsed_args):
+        self.log.debug('take_action(%s)' % parsed_args)
+        compute_client = self.app.client_manager.compute
+
+        # Lookup parsed_args.image
+        image = utils.find_resource(compute_client.images,
+            parsed_args.image)
+
+        # Lookup parsed_args.flavor
+        flavor = utils.find_resource(compute_client.flavors,
+            parsed_args.flavor)
+
+        boot_args = [parsed_args.server_name, image, flavor]
+
+        meta = dict(v.split('=', 1) for v in parsed_args.meta_data)
+
+        files = {}
+        for f in parsed_args.file:
+            dst, src = f.split('=', 1)
+            try:
+                files[dst] = open(src)
+            except IOError, e:
+                raise exceptions.CommandError("Can't open '%s': %s" % (src, e))
+
+        if parsed_args.min > parsed_args.max:
+            raise exceptions.CommandError("min instances should be <= "
+                "max instances")
+        if parsed_args.min < 1:
+            raise exceptions.CommandError("min instances should be > 0")
+        if parsed_args.max < 1:
+            raise exceptions.CommandError("max instances should be > 0")
+
+        userdata = None
+        if parsed_args.user_data:
+            try:
+                userdata = open(parsed_args.user_data)
+            except IOError, e:
+                raise exceptions.CommandError("Can't open '%s': %s" % \
+                    (parsed_args.user_data, e))
+
+        block_device_mapping = dict(v.split('=', 1)
+            for v in parsed_args.block_device_mapping)
+
+        nics = []
+        for nic_str in parsed_args.nic:
+            nic_info = {"net-id": "", "v4-fixed-ip": ""}
+            nic_info.update(dict(kv_str.split("=", 1)
+                for kv_str in nic_str.split(",")))
+            nics.append(nic_info)
+
+        hints = {}
+        for hint in parsed_args.hint:
+            key, _sep, value = hint.partition('=')
+            # NOTE(vish): multiple copies of the same hint will
+            #             result in a list of values
+            if key in hints:
+                if isinstance(hints[key], basestring):
+                    hints[key] = [hints[key]]
+                hints[key] += [value]
+            else:
+                hints[key] = value
+
+        # What does a non-boolean value for config-drive do?
+        # --config-drive argument is either a volume id or
+        # 'True' (or '1') to use an ephemeral volume
+        if str(parsed_args.config_drive).lower() in ("true", "1"):
+            config_drive = True
+        elif str(parsed_args.config_drive).lower() in ("false", "0",
+            "", "none"):
+            config_drive = None
+        else:
+            config_drive = parsed_args.config_drive
+
+        boot_kwargs = dict(
+            meta=meta,
+            files=files,
+            reservation_id=None,
+            min_count=parsed_args.min,
+            max_count=parsed_args.max,
+            security_groups=parsed_args.security_group,
+            userdata=userdata,
+            key_name=parsed_args.key_name,
+            availability_zone=parsed_args.availability_zone,
+            block_device_mapping=block_device_mapping,
+            nics=nics,
+            scheduler_hints=hints,
+            config_drive=config_drive,
+        )
+
+        self.log.debug('boot_args: %s' % boot_args)
+        self.log.debug('boot_kwargs: %s' % boot_kwargs)
+        server = compute_client.servers.create(*boot_args, **boot_kwargs)
+
+        if parsed_args.wait:
+            _wait_for_status(compute_client.servers.get, server._info['id'],
+                ['active'])
+
+        details = _prep_server_detail(compute_client, server)
+        return zip(*sorted(details.iteritems()))
+
+
+class DeleteServer(command.Command):
+    """Delete server command"""
+
+    api = 'compute'
+    log = logging.getLogger(__name__ + '.DeleteServer')
+
+    def get_parser(self, prog_name):
+        parser = super(DeleteServer, self).get_parser(prog_name)
+        parser.add_argument(
+            'server',
+            metavar='<server>',
+            help='Name or ID of server to delete',
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        self.log.debug('take_action(%s)' % parsed_args)
+        compute_client = self.app.client_manager.compute
+        server = utils.find_resource(
+            compute_client.servers, parsed_args.server)
+        compute_client.servers.delete(server.id)
+        return
+
+
 class ListServer(lister.Lister):
     """List server command"""
 
@@ -54,40 +371,48 @@ class ListServer(lister.Lister):
         parser = super(ListServer, self).get_parser(prog_name)
         parser.add_argument(
             '--reservation-id',
+            metavar='<reservation-id>',
             help='only return instances that match the reservation',
             )
         parser.add_argument(
             '--ip',
+            metavar='<ip-address-regex>',
             help='regular expression to match IP address',
             )
         parser.add_argument(
             '--ip6',
+            metavar='<ip-address-regex>',
             help='regular expression to match IPv6 address',
             )
         parser.add_argument(
             '--name',
+            metavar='<name>',
             help='regular expression to match name',
             )
         parser.add_argument(
             '--instance-name',
+            metavar='<server-name>',
             help='regular expression to match instance name',
             )
         parser.add_argument(
             '--status',
+            metavar='<status>',
             help='search by server status',
             # FIXME(dhellmann): Add choices?
             )
         parser.add_argument(
             '--flavor',
+            metavar='<flavor>',
             help='search by flavor ID',
             )
         parser.add_argument(
             '--image',
+            metavar='<image>',
             help='search by image ID',
             )
         parser.add_argument(
             '--host',
-            metavar='HOSTNAME',
+            metavar='<hostname>',
             help='search by hostname',
             )
         parser.add_argument(
@@ -100,23 +425,23 @@ class ListServer(lister.Lister):
 
     def take_action(self, parsed_args):
         self.log.debug('take_action(%s)' % parsed_args)
-        nova_client = self.app.client_manager.compute
+        compute_client = self.app.client_manager.compute
         search_opts = {
-            'all_tenants': parsed_args.all_tenants,
             'reservation_id': parsed_args.reservation_id,
             'ip': parsed_args.ip,
             'ip6': parsed_args.ip6,
             'name': parsed_args.name,
-            'image': parsed_args.image,
-            'flavor': parsed_args.flavor,
-            'status': parsed_args.status,
-            'host': parsed_args.host,
             'instance_name': parsed_args.instance_name,
+            'status': parsed_args.status,
+            'flavor': parsed_args.flavor,
+            'image': parsed_args.image,
+            'host': parsed_args.host,
+            'all_tenants': parsed_args.all_tenants,
             }
         self.log.debug('search options: %s', search_opts)
         # FIXME(dhellmann): Consider adding other columns
         columns = ('ID', 'Name', 'Status', 'Networks')
-        data = nova_client.servers.list(search_opts=search_opts)
+        data = compute_client.servers.list(search_opts=search_opts)
         return (columns,
                 (utils.get_item_properties(
                     s, columns,
@@ -125,6 +450,165 @@ class ListServer(lister.Lister):
                 )
 
 
+class PauseServer(command.Command):
+    """Pause server command"""
+
+    api = 'compute'
+    log = logging.getLogger(__name__ + '.PauseServer')
+
+    def get_parser(self, prog_name):
+        parser = super(PauseServer, self).get_parser(prog_name)
+        parser.add_argument(
+            'server',
+            metavar='<server>',
+            help='Name or ID of server to pause',
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        self.log.debug('take_action(%s)' % parsed_args)
+        compute_client = self.app.client_manager.compute
+        server = utils.find_resource(
+            compute_client.servers, parsed_args.server)
+        server.pause()
+        return
+
+
+class RebootServer(command.Command):
+    """Reboot server command"""
+
+    api = 'compute'
+    log = logging.getLogger(__name__ + '.RebootServer')
+
+    def get_parser(self, prog_name):
+        parser = super(RebootServer, self).get_parser(prog_name)
+        parser.add_argument(
+            'server',
+            metavar='<server>',
+            help='Name or ID of server to reboot',
+            )
+        group = parser.add_mutually_exclusive_group()
+        group.add_argument(
+            '--hard',
+            dest='reboot_type',
+            action='store_const',
+            const=servers.REBOOT_HARD,
+            default=servers.REBOOT_SOFT,
+            help='Perform a hard reboot',
+            )
+        group.add_argument(
+            '--soft',
+            dest='reboot_type',
+            action='store_const',
+            const=servers.REBOOT_SOFT,
+            default=servers.REBOOT_SOFT,
+            help='Perform a soft reboot',
+            )
+        parser.add_argument(
+            '--wait',
+            dest='wait',
+            action='store_true',
+            help='Wait for server to become active to return',
+            )
+        return parser
+
+    def take_action(self, parsed_args):
+        self.log.debug('take_action(%s)' % parsed_args)
+        compute_client = self.app.client_manager.compute
+        server = utils.find_resource(
+            compute_client.servers, parsed_args.server)
+        server.reboot(parsed_args.reboot_type)
+
+        if parsed_args.wait:
+            _wait_for_status(compute_client.servers.get, server.id,
+                ['active'])
+
+        return
+
+
+class RebuildServer(show.ShowOne):
+    """Rebuild server command"""
+
+    api = "compute"
+    log = logging.getLogger(__name__ + '.RebuildServer')
+
+    def get_parser(self, prog_name):
+        parser = super(RebuildServer, self).get_parser(prog_name)
+        parser.add_argument(
+            'server',
+            metavar='<server>',
+            help='Server name or ID',
+        )
+        parser.add_argument(
+            '--image',
+            metavar='<image>',
+            required=True,
+            help='Recreate server from this image',
+            )
+        parser.add_argument(
+            '--rebuild-password',
+            metavar='<rebuild_password>',
+            default=False,
+            help="Set the provided password on the rebuild instance",
+            )
+        parser.add_argument(
+            '--wait',
+            dest='wait',
+            action='store_true',
+            help='Wait for server to become active to return',
+            )
+        return parser
+
+    def take_action(self, parsed_args):
+        self.log.debug('take_action(%s)' % parsed_args)
+        compute_client = self.app.client_manager.compute
+
+        # Lookup parsed_args.image
+        image = utils.find_resource(compute_client.images, parsed_args.image)
+
+        server = utils.find_resource(
+            compute_client.servers, parsed_args.server)
+
+        _password = None
+        if parsed_args.rebuild_password is not False:
+            _password = args.rebuild_password
+
+        kwargs = {}
+        server = server.rebuild(image, _password, **kwargs)
+
+        # TODO(dtroyer): force silent=True if output filter != table
+        if parsed_args.wait:
+            _wait_for_status(compute_client.servers.get, server._info['id'],
+                ['active'])
+
+        details = _prep_server_detail(compute_client, server)
+        return zip(*sorted(details.iteritems()))
+
+
+class ResumeServer(command.Command):
+    """Resume server command"""
+
+    api = 'compute'
+    log = logging.getLogger(__name__ + '.ResumeServer')
+
+    def get_parser(self, prog_name):
+        parser = super(ResumeServer, self).get_parser(prog_name)
+        parser.add_argument(
+            'server',
+            metavar='<server>',
+            help='Name or ID of server to resume',
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        self.log.debug('take_action(%s)' % parsed_args)
+        compute_client = self.app.client_manager.compute
+        server = utils.find_resource(
+            compute_client.servers, parsed_args.server)
+        server.resume()
+        return
+
+
 class ShowServer(show.ShowOne):
     """Show server command"""
 
@@ -136,32 +620,62 @@ class ShowServer(show.ShowOne):
         parser.add_argument(
             'server',
             metavar='<server>',
-            help='Name or ID of server to display')
+            help='Name or ID of server to display'),
         return parser
 
     def take_action(self, parsed_args):
         self.log.debug('take_action(%s)' % parsed_args)
-        nova_client = self.app.client_manager.compute
-        server = utils.find_resource(nova_client.servers, parsed_args.server)
+        compute_client = self.app.client_manager.compute
+        server = utils.find_resource(compute_client.servers,
+            parsed_args.server)
 
-        info = {}
-        info.update(server._info)
+        details = _prep_server_detail(compute_client, server)
+        return zip(*sorted(details.iteritems()))
 
-        # Convert the flavor blob to a name
-        flavor_info = info.get('flavor', {})
-        flavor_id = flavor_info.get('id', '')
-        flavor = utils.find_resource(nova_client.flavors, flavor_id)
-        info['flavor'] = flavor.name
 
-        # Convert the image blob to a name
-        image_info = info.get('image', {})
-        image_id = image_info.get('id', '')
-        image = utils.find_resource(nova_client.images, image_id)
-        info['image'] = image.name
+class SuspendServer(command.Command):
+    """Suspend server command"""
 
-        # Format addresses in a useful way
-        info['addresses'] = _format_servers_list_networks(server)
+    api = 'compute'
+    log = logging.getLogger(__name__ + '.SuspendServer')
 
-        # Remove a couple of values that are long and not too useful
-        info.pop('links', None)
-        return zip(*sorted(info.iteritems()))
+    def get_parser(self, prog_name):
+        parser = super(SuspendServer, self).get_parser(prog_name)
+        parser.add_argument(
+            'server',
+            metavar='<server>',
+            help='Name or ID of server to suspend',
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        self.log.debug('take_action(%s)' % parsed_args)
+        compute_client = self.app.client_manager.compute
+        server = utils.find_resource(
+            compute_client.servers, parsed_args.server)
+        server.suspend()
+        return
+
+
+class UnpauseServer(command.Command):
+    """Unpause server command"""
+
+    api = 'compute'
+    log = logging.getLogger(__name__ + '.UnpauseServer')
+
+    def get_parser(self, prog_name):
+        parser = super(UnpauseServer, self).get_parser(prog_name)
+        parser.add_argument(
+            'server',
+            metavar='<server>',
+            help='Name or ID of server to unpause',
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        self.log.debug('take_action(%s)' % parsed_args)
+        compute_client = self.app.client_manager.compute
+        server = utils.find_resource(
+            compute_client.servers, parsed_args.server)
+        server.unpause()
+        return
diff --git a/setup.py b/setup.py
index 9a6b5b199d..728a062e36 100644
--- a/setup.py
+++ b/setup.py
@@ -55,6 +55,15 @@ setuptools.setup(
     entry_points={
         'console_scripts': ['openstack=openstackclient.shell:main'],
         'openstack.cli': [
+            'create_endpoint=' +
+                'openstackclient.identity.v2_0.endpoint:CreateEndpoint',
+            'delete_endpoint=' +
+                'openstackclient.identity.v2_0.endpoint:DeleteEndpoint',
+            'list_endpoint=' +
+                'openstackclient.identity.v2_0.endpoint:ListEndpoint',
+            'show_endpoint=' +
+                'openstackclient.identity.v2_0.endpoint:ShowEndpoint',
+
             'add_role=' +
                 'openstackclient.identity.v2_0.role:AddRole',
             'create_role=' +
@@ -65,22 +74,25 @@ setuptools.setup(
             'remove_role=' +
                 'openstackclient.identity.v2_0.role:RemoveRole',
             'show_role=openstackclient.identity.v2_0.role:ShowRole',
+
+            'create_server=openstackclient.compute.v2.server:CreateServer',
+            'delete_server=openstackclient.compute.v2.server:DeleteServer',
             'list_server=openstackclient.compute.v2.server:ListServer',
+            'pause_server=openstackclient.compute.v2.server:PauseServer',
+            'reboot_server=openstackclient.compute.v2.server:RebootServer',
+            'rebuild_server=openstackclient.compute.v2.server:RebuildServer',
+            'resume_server=openstackclient.compute.v2.server:ResumeServer',
             'show_server=openstackclient.compute.v2.server:ShowServer',
-            'create_endpoint=' +
-                'openstackclient.identity.v2_0.endpoint:CreateEndpoint',
-            'delete_endpoint=' +
-                'openstackclient.identity.v2_0.endpoint:DeleteEndpoint',
-            'list_endpoint=' +
-                'openstackclient.identity.v2_0.endpoint:ListEndpoint',
-            'show_endpoint=' +
-                'openstackclient.identity.v2_0.endpoint:ShowEndpoint',
+            'suspend_server=openstackclient.compute.v2.server:SuspendServer',
+            'unpause_server=openstackclient.compute.v2.server:UnpauseServer',
+
             'create_service=' +
                 'openstackclient.identity.v2_0.service:CreateService',
             'delete_service=' +
                 'openstackclient.identity.v2_0.service:DeleteService',
             'list_service=openstackclient.identity.v2_0.service:ListService',
             'show_service=openstackclient.identity.v2_0.service:ShowService',
+
             'create_tenant=' +
                 'openstackclient.identity.v2_0.tenant:CreateTenant',
             'delete_tenant=' +
@@ -88,6 +100,7 @@ setuptools.setup(
             'list_tenant=openstackclient.identity.v2_0.tenant:ListTenant',
             'set_tenant=openstackclient.identity.v2_0.tenant:SetTenant',
             'show_tenant=openstackclient.identity.v2_0.tenant:ShowTenant',
+
             'create_user=' +
                 'openstackclient.identity.v2_0.user:CreateUser',
             'delete_user=' +