# Copyright 2016 Huawei, Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # """Mogan v1 Baremetal server action implementations""" import io import json import logging import os from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils from moganclient.common.i18n import _ LOG = logging.getLogger(__name__) def _addresses_formatter(network_client, networks): output = [] for (network, addresses) in networks.items(): if not addresses: continue addrs = [addr['addr'] for addr in addresses] network_data = network_client.find_network( network, ignore_missing=False) net_ident = network_data.name or network_data.id addresses_csv = ', '.join(addrs) group = "%s=%s" % (net_ident, addresses_csv) output.append(group) return '; '.join(output) class ServersActionBase(command.Command): def _get_parser_with_action(self, prog_name, action): parser = super(ServersActionBase, self).get_parser(prog_name) parser.add_argument( 'server', metavar='', nargs='+', help=_("Baremetal server(s) to %s (name or UUID)") % action ) return parser def _action_multiple_items(self, parsed_args, action, method_name, **kwargs): bc_client = self.app.client_manager.baremetal_compute result = 0 for one_server in parsed_args.server: try: data = utils.find_resource( bc_client.server, one_server) method = getattr(bc_client.server, method_name) method(data.uuid, **kwargs) except Exception as e: result += 1 LOG.error("Failed to %(action)s server with name or UUID " "'%(server)s': %(e)s", {'action': action, 'server': one_server, 'e': e}) if result > 0: total = len(parsed_args.server) msg = (_("%(result)s of %(total)s baremetal servers failed " "to %(action)s.") % {'result': result, 'total': total, 'action': action}) raise exceptions.CommandError(msg) class CreateServer(command.ShowOne): """Create a new baremetal server""" def get_parser(self, prog_name): parser = super(CreateServer, self).get_parser(prog_name) parser.add_argument( "name", metavar="", help=_("New baremetal server name") ) parser.add_argument( "--flavor", metavar="", required=True, help=_('Create server with this flavor (name or ID)'), ) parser.add_argument( "--image", metavar="", required=True, help=_('Create server boot disk from this image (name or ID)'), ) parser.add_argument( "--nic", metavar="", required=True, optional_keys=['net-id', 'port-id'], action=parseractions.MultiKeyValueAction, help=_("Create a NIC on the server. " "Specify option multiple times to create multiple NICs."), ) parser.add_argument( "--description", metavar="", help=_("Baremetal server description"), ) parser.add_argument( "--availability-zone", metavar="", help=_('Select an availability zone for the server'), ) parser.add_argument( '--file', metavar='', action='append', default=[], help=_('File to inject into image before boot ' '(repeat option to set multiple files)'), ) parser.add_argument( '--user-data', metavar='', help=_('User data file to inject into the server'), ) parser.add_argument( '--key-name', metavar='', help=_('Keypair to inject into this server (optional extension)'), ) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, help=_('Set a property on this server ' '(repeat option to set multiple values)'), ) parser.add_argument( '--partition', metavar='', action=parseractions.KeyValueAction, help=_('Create a partition on the root disk of this server, ' 'only root_gb(required), ephemeral_gb, and swap_mb allowed ' '(repeat option to set multiple partitions)'), ) parser.add_argument( "--min", metavar='', type=int, default=1, help=_('Minimum number of servers to launch (default=1)'), ) parser.add_argument( '--max', metavar='', type=int, default=1, help=_('Maximum number of servers to launch (default=1)'), ) parser.add_argument( "--hint", metavar="", action=parseractions.KeyValueAction, help=_("Hints for the Mogan scheduler (optional extension)") ) return parser def take_action(self, parsed_args): bc_client = self.app.client_manager.baremetal_compute flavor_data = utils.find_resource( bc_client.flavor, parsed_args.flavor) image_data = utils.find_resource( self.app.client_manager.image.images, parsed_args.image) for nic in parsed_args.nic: if 'net-id' in nic: nic['net_id'] = nic['net-id'] del nic['net-id'] if 'port-id' in nic: nic['port_id'] = nic['port-id'] del nic['port-id'] files = {} for f in parsed_args.file: dst, src = f.split('=', 1) try: files[dst] = io.open(src, 'rb') except IOError as e: msg = _("Can't open '%(source)s': %(exception)s") raise exceptions.CommandError( msg % {"source": src, "exception": e} ) if parsed_args.min > parsed_args.max: msg = _("min servers should be <= max servers") raise exceptions.CommandError(msg) if parsed_args.min < 1: msg = _("min servers should be > 0") raise exceptions.CommandError(msg) if parsed_args.max < 1: msg = _("max servers should be > 0") raise exceptions.CommandError(msg) userdata = None if parsed_args.user_data: try: userdata = io.open(parsed_args.user_data) except IOError as e: msg = _("Can't open '%(data)s': %(exception)s") raise exceptions.CommandError( msg % {"data": parsed_args.user_data, "exception": e} ) boot_kwargs = dict( name=parsed_args.name, image_uuid=image_data.id, flavor_uuid=flavor_data.uuid, description=parsed_args.description, networks=parsed_args.nic, availability_zone=parsed_args.availability_zone, userdata=userdata, files=files, key_name=parsed_args.key_name, metadata=parsed_args.property, partitions=parsed_args.partition, min_count=parsed_args.min, max_count=parsed_args.max, hint=parsed_args.hint ) try: data = bc_client.server.create(**boot_kwargs) finally: # Clean up open files - make sure they are not strings for f in files: if hasattr(f, 'close'): f.close() if hasattr(userdata, 'close'): userdata.close() # Special mapping for columns to make the output easier to read: # 'metadata' --> 'properties' # 'image_uuid' --> ' ()' # 'flavor_uuid' --> ' ()' data._info.update( { 'properties': utils.format_dict(data._info.pop('metadata')), 'image': '%s (%s)' % (image_data.name, image_data.id), 'flavor': '%s (%s)' % (flavor_data.name, flavor_data.uuid) }, ) data._info.pop('flavor_uuid') data._info.pop('image_uuid') info = {} info.update(data._info) return zip(*sorted(info.items())) class DeleteServer(command.Command): """Delete existing baremetal erver(s)""" def get_parser(self, prog_name): parser = super(DeleteServer, self).get_parser(prog_name) parser.add_argument( 'server', metavar='', nargs='+', help=_("Baremetal server(s) to delete (name or UUID)") ) return parser def take_action(self, parsed_args): bc_client = self.app.client_manager.baremetal_compute result = 0 for one_server in parsed_args.server: try: data = utils.find_resource( bc_client.server, one_server) bc_client.server.delete(data.uuid) except Exception as e: result += 1 LOG.error("Failed to delete server with name or UUID " "'%(server)s': %(e)s", {'server': one_server, 'e': e}) if result > 0: total = len(parsed_args.server) msg = (_("%(result)s of %(total)s baremetal servers failed " "to delete.") % {'result': result, 'total': total}) raise exceptions.CommandError(msg) class ListServer(command.Lister): """List all baremetal servers""" def get_parser(self, prog_name): parser = super(ListServer, self).get_parser(prog_name) parser.add_argument( '--long', action='store_true', default=False, help=_("List additional fields in output") ) parser.add_argument( '-n', '--no-name-lookup', action='store_true', default=False, help=_('Skip flavor and image name lookup.'), ) parser.add_argument( '--all-projects', action='store_true', default=bool(int(os.environ.get("ALL_PROJECTS", 0))), help=_('Include all projects (admin only)'), ) return parser def _addresses_formatter(self, networks): output = [] network_client = self.app.client_manager.network for (network, addresses) in networks.items(): if not addresses: continue addrs = [addr['addr'] for addr in addresses] network_data = network_client.find_network( network, ignore_missing=False) net_ident = network_data.name or network_data.id addresses_csv = ', '.join(addrs) group = "%s=%s" % (net_ident, addresses_csv) output.append(group) return '; '.join(output) def take_action(self, parsed_args): bc_client = self.app.client_manager.baremetal_compute if parsed_args.long: # This is the easiest way to change column headers column_headers = ( "UUID", "Name", "Status", "Power State", "Networks", "Image Name", "Image Id", "Flavor Name", "Flavor Id", "Availability Zone", 'Properties', ) columns = ( "uuid", "name", "status", "power_state", "addresses", "image_name", "image_uuid", "flavor_name", "flavor_uuid", "availability_zone", 'metadata', ) else: column_headers = ( "UUID", "Name", "Status", "Networks", "Image", "Flavor", ) if parsed_args.no_name_lookup: columns = ( "uuid", "name", "status", "addresses", "image_uuid", "flavor_uuid", ) else: columns = ( "uuid", "name", "status", "addresses", "image_name", "flavor_name", ) data = bc_client.server.list(detailed=True, all_projects=parsed_args.all_projects) formatters = {'addresses': self._addresses_formatter, 'metadata': utils.format_dict } images = {} # Create a dict that maps image_id to image object. # Needed so that we can display the "Image Name" column. # "Image Name" is not crucial, so we swallow any exceptions. if not parsed_args.no_name_lookup: try: images_list = self.app.client_manager.image.images.list() for i in images_list: images[i.id] = i except Exception: pass flavors = {} # Create a dict that maps flavor_id to flavor object. # Needed so that we can display the "Flavor Name" column. # "Flavor Name" is not crucial, so we swallow any exceptions. if not parsed_args.no_name_lookup: try: flavors_list = bc_client.flavor.list() for i in flavors_list: flavors[i.uuid] = i except Exception: pass # Populate image_name, image_id, flavor_name and flavor_id attributes # of server objects so that we can display those columns. for s in data: image = images.get(s.image_uuid) if image: s.image_name = image.name else: s.image_name = '' flavor = flavors.get(s.flavor_uuid) if flavor: s.flavor_name = flavor.name else: s.flavor_name = '' return (column_headers, (utils.get_item_properties( s, columns, formatters=formatters ) for s in data)) class ShowServer(command.ShowOne): """Display baremetal server details""" def get_parser(self, prog_name): parser = super(ShowServer, self).get_parser(prog_name) parser.add_argument( 'server', metavar='', help=_("Baremetal server to display (name or UUID)") ) return parser def _format_image_field(self, data): image_client = self.app.client_manager.image image_uuid = data._info.pop('image_uuid') image = image_client.images.get(image_uuid) return '%s (%s)' % (image.name, image_uuid) def _format_flavor_field(self, data): bc_client = self.app.client_manager.baremetal_compute flavor_uuid = data._info.pop('flavor_uuid') flavor = bc_client.flavor.get(flavor_uuid) return '%s (%s)' % (flavor.name, flavor_uuid) def take_action(self, parsed_args): bc_client = self.app.client_manager.baremetal_compute data = utils.find_resource( bc_client.server, parsed_args.server, ) # Special mapping for columns to make the output easier to read: # 'metadata' --> 'properties' network_client = self.app.client_manager.network data._info.update( { 'properties': utils.format_dict(data._info.pop('metadata')), 'addresses': _addresses_formatter( network_client, data._info.pop('addresses')), 'image': self._format_image_field(data), 'flavor': self._format_flavor_field(data) }, ) info = {} info.update(data._info) return zip(*sorted(info.items())) class SetServer(command.Command): """Set properties for a baremetal server""" def get_parser(self, prog_name): parser = super(SetServer, self).get_parser(prog_name) parser.add_argument( 'server', metavar='', help=_("Baremetal server to update (name or UUID)") ) parser.add_argument( "--description", metavar="", help=_("Baremetal Server description"), ) parser.add_argument( "--name", metavar="", help=_("Baremetal server description"), ) parser.add_argument( "--property", metavar="", action=parseractions.KeyValueAction, help=_("Property to set on this server " "(repeat option to set multiple properties)") ) return parser def take_action(self, parsed_args): bc_client = self.app.client_manager.baremetal_compute server = utils.find_resource( bc_client.server, parsed_args.server, ) updates = [] if parsed_args.description: updates.append({"op": "replace", "path": "/description", "value": parsed_args.description}) if parsed_args.name: updates.append({"op": "replace", "path": "/name", "value": parsed_args.name}) for key, value in (parsed_args.property or {}).items(): updates.append({"op": "add", "path": "/metadata/%s" % key, "value": value}) if updates: bc_client.server.update(server.uuid, updates) class UnsetServer(command.Command): """Unset properties for a baremetal server""" def get_parser(self, prog_name): parser = super(UnsetServer, self).get_parser(prog_name) parser.add_argument( 'server', metavar='', help=_("Baremetal server to unset its properties (name or UUID)") ) parser.add_argument( "--property", metavar="", action='append', help=_("Property to remove from this server " "(repeat option to remove multiple properties)") ) return parser def take_action(self, parsed_args): bc_client = self.app.client_manager.baremetal_compute server = utils.find_resource( bc_client.server, parsed_args.server, ) updates = [] for key in parsed_args.property or []: updates.append({"op": "remove", "path": "/metadata/%s" % key}) if updates: bc_client.server.update(server.uuid, updates) class StartServer(ServersActionBase): """Start a baremetal server.""" def get_parser(self, prog_name): return self._get_parser_with_action(prog_name, 'start') def take_action(self, parsed_args): self._action_multiple_items(parsed_args, 'start', 'set_power_state', power_state='on') class StopServer(ServersActionBase): """Stop baremetal server(s).""" def get_parser(self, prog_name): return self._get_parser_with_action(prog_name, 'stop') def take_action(self, parsed_args): self._action_multiple_items(parsed_args, 'stop', 'set_power_state', power_state='off') class RebootServer(ServersActionBase): """Reboot baremetal server(s).""" def get_parser(self, prog_name): return self._get_parser_with_action(prog_name, 'reboot') def take_action(self, parsed_args): self._action_multiple_items(parsed_args, 'reboot', 'set_power_state', power_state='reboot') class LockServer(ServersActionBase): """Lock baremetal server(s).""" def get_parser(self, prog_name): return self._get_parser_with_action(prog_name, 'lock') def take_action(self, parsed_args): self._action_multiple_items(parsed_args, 'lock', 'set_lock_state', lock_state=True) class UnLockServer(ServersActionBase): """UnLock baremetal server(s).""" def get_parser(self, prog_name): return self._get_parser_with_action(prog_name, 'unlock') def take_action(self, parsed_args): self._action_multiple_items(parsed_args, 'unlock', 'set_lock_state', lock_state=False) class ShowServerNetworkInfo(command.Lister): """Display baremetal server's network info""" def get_parser(self, prog_name): parser = super(ShowServerNetworkInfo, self).get_parser(prog_name) parser.add_argument( 'server', metavar='', help=_("Baremetal server to display its network information (name " "or UUID)") ) return parser def take_action(self, parsed_args): bc_client = self.app.client_manager.baremetal_compute server = utils.find_resource( bc_client.server, parsed_args.server, ) data = bc_client.server.get_server_nics(server.uuid) columns = ('network_id', 'port_id', 'mac_address', 'fixed_ips', 'floating_ip') formatters = {'fixed_ips': lambda s: json.dumps(s, indent=4)} return (columns, (utils.get_item_properties( nic, columns, formatters=formatters) for nic in data)) class AddFloatingIP(command.Command): _description = _("Add floating IP address to server") def get_parser(self, prog_name): parser = super(AddFloatingIP, self).get_parser(prog_name) parser.add_argument( "server", metavar="", help=_("Server to receive the floating IP address (name or ID)"), ) parser.add_argument( "ip_address", metavar="", help=_("Floating IP address to assign to server (IP only)"), ) parser.add_argument( "--fixed-ip-address", metavar="", help=_("Fixed IP address to associate with this floating IP " "address"), ) return parser def take_action(self, parsed_args): bc_client = self.app.client_manager.baremetal_compute server = utils.find_resource( bc_client.server, parsed_args.server, ) bc_client.server.add_floating_ip(server.uuid, parsed_args.ip_address, parsed_args.fixed_ip_address) class RemoveFloatingIP(command.Command): _description = _("Remove floating IP address from server") def get_parser(self, prog_name): parser = super(RemoveFloatingIP, self).get_parser(prog_name) parser.add_argument( "server", metavar="", help=_( "Server to remove the floating IP address from (name or ID)" ), ) parser.add_argument( "ip_address", metavar="", help=_("Floating IP address to remove from server (IP only)"), ) return parser def take_action(self, parsed_args): bc_client = self.app.client_manager.baremetal_compute server = utils.find_resource( bc_client.server, parsed_args.server, ) bc_client.server.remove_floating_ip(server.uuid, parsed_args.ip_address) class AddInterface(command.Command): _description = _("Add interface to server") def get_parser(self, prog_name): parser = super(AddInterface, self).get_parser(prog_name) excluded_group = parser.add_mutually_exclusive_group(required=True) excluded_group.add_argument( "--net-id", metavar="", help=_("Network to link to server"), ) excluded_group.add_argument( "--port-id", metavar="", help=_("Port to link to server"), ) parser.add_argument( "server", metavar="", help=_("Server to attach interface for"), ) return parser def take_action(self, parsed_args): bc_client = self.app.client_manager.baremetal_compute server = utils.find_resource( bc_client.server, parsed_args.server, ) bc_client.server.add_interface(server.uuid, parsed_args.net_id, parsed_args.port_id) class RemoveInterface(command.Command): _description = _("Remove interface from server") def get_parser(self, prog_name): parser = super(RemoveInterface, self).get_parser(prog_name) parser.add_argument( "port_id", metavar="", help=_("Interface to remove from server"), ) parser.add_argument( "server", metavar="", help=_( "Server to remove the interface from" ), ) return parser def take_action(self, parsed_args): bc_client = self.app.client_manager.baremetal_compute server = utils.find_resource( bc_client.server, parsed_args.server, ) bc_client.server.remove_interface(server.uuid, parsed_args.port_id) class ShowConsoleURL(command.ShowOne): _description = _("Show server's remote console URL") def get_parser(self, prog_name): parser = super(ShowConsoleURL, self).get_parser(prog_name) parser.add_argument( 'server', metavar='', help=_("Server to show URL (name or ID)") ) type_group = parser.add_mutually_exclusive_group() type_group.add_argument( '--shellinabox', dest='url_type', action='store_const', const='shellinabox', help=_("Show shellinabox serial console URL"), ) type_group.add_argument( '--socat', dest='url_type', action='store_const', const='socat', help=_("Show socat serial console URL"), ) return parser def take_action(self, parsed_args): bc_client = self.app.client_manager.baremetal_compute server = utils.find_resource( bc_client.server, parsed_args.server, ) data = bc_client.server.get_remote_console( server.uuid, parsed_args.url_type) if not data: return ({}, {}) info = {} info.update(data._info) return zip(*sorted(info.items()))