summaryrefslogtreecommitdiff
path: root/openstackclient
diff options
context:
space:
mode:
authorDean Troyer <dtroyer@gmail.com>2012-08-20 18:02:30 -0500
committerDean Troyer <dtroyer@gmail.com>2012-09-05 16:06:49 -0500
commit90a1c65f3ac90b1077eb3ea2f5fbe8a039ee9290 (patch)
tree1bdfc6c4b38660cc0297fa2d4f33cf66730ac808 /openstackclient
parent8010e773accce2d24c04659c88ac0c040c9a1932 (diff)
downloadpython-openstackclient-90a1c65f3ac90b1077eb3ea2f5fbe8a039ee9290.tar.gz
Update compute client bits
* add server create, delete, pause, reboot, rebuild resume, suspend, unpause commands Change-Id: I728ec199e4562bd621c3a73106c90d8b790b459a
Diffstat (limited to 'openstackclient')
-rw-r--r--openstackclient/compute/client.py17
-rw-r--r--openstackclient/compute/v2/server.py578
2 files changed, 559 insertions, 36 deletions
diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py
index a59b6e00..3c17b17a 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 e72c5735..e496bd45 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,
+ 'instance_name': parsed_args.instance_name,
'status': parsed_args.status,
+ 'flavor': parsed_args.flavor,
+ 'image': parsed_args.image,
'host': parsed_args.host,
- 'instance_name': parsed_args.instance_name,
+ '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)
+ compute_client = self.app.client_manager.compute
+ server = utils.find_resource(compute_client.servers,
+ parsed_args.server)
+
+ details = _prep_server_detail(compute_client, server)
+ return zip(*sorted(details.iteritems()))
+
+
+class SuspendServer(command.Command):
+ """Suspend server command"""
+
+ api = 'compute'
+ log = logging.getLogger(__name__ + '.SuspendServer')
+
+ 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)
- nova_client = self.app.client_manager.compute
- server = utils.find_resource(nova_client.servers, parsed_args.server)
-
- info = {}
- info.update(server._info)
-
- # 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
-
- # Format addresses in a useful way
- info['addresses'] = _format_servers_list_networks(server)
-
- # Remove a couple of values that are long and not too useful
- info.pop('links', None)
- return zip(*sorted(info.iteritems()))
+ compute_client = self.app.client_manager.compute
+ server = utils.find_resource(
+ compute_client.servers, parsed_args.server)
+ server.unpause()
+ return