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=' +