# Copyright 2013 OpenStack Foundation # # 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. # """Flavor action implementations""" import logging from openstack import exceptions as sdk_exceptions from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common LOG = logging.getLogger(__name__) _formatters = { 'extra_specs': format_columns.DictColumn, 'properties': format_columns.DictColumn } def _get_flavor_columns(item): # To maintain backwards compatibility we need to rename sdk props to # whatever OSC was using before column_map = { 'extra_specs': 'properties', 'ephemeral': 'OS-FLV-EXT-DATA:ephemeral', 'is_disabled': 'OS-FLV-DISABLED:disabled', 'is_public': 'os-flavor-access:is_public' } hidden_columns = ['links', 'location'] return utils.get_osc_show_columns_for_sdk_resource( item, column_map, hidden_columns) class CreateFlavor(command.ShowOne): _description = _("Create new flavor") def get_parser(self, prog_name): parser = super(CreateFlavor, self).get_parser(prog_name) parser.add_argument( "name", metavar="", help=_("New flavor name") ) parser.add_argument( "--id", metavar="", help=_("Unique flavor ID") ) parser.add_argument( "--ram", type=int, metavar="", default=256, help=_("Memory size in MB (default 256M)") ) parser.add_argument( "--disk", type=int, metavar="", default=0, help=_("Disk size in GB (default 0G)") ) parser.add_argument( "--ephemeral", type=int, metavar="", default=0, help=_("Ephemeral disk size in GB (default 0G)") ) parser.add_argument( "--swap", type=int, metavar="", default=0, help=_("Additional swap space size in MB (default 0M)") ) parser.add_argument( "--vcpus", type=int, metavar="", default=1, help=_("Number of vcpus (default 1)") ) parser.add_argument( "--rxtx-factor", type=float, metavar="", default=1.0, help=_("RX/TX factor (default 1.0)") ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( "--public", dest="public", action="store_true", default=True, help=_("Flavor is available to other projects (default)") ) public_group.add_argument( "--private", dest="public", action="store_false", help=_("Flavor is not available to other projects") ) parser.add_argument( "--property", metavar="", action=parseractions.KeyValueAction, dest="properties", help=_("Property to add for this flavor " "(repeat option to set multiple properties)") ) parser.add_argument( '--project', metavar='', help=_("Allow to access private flavor (name or ID) " "(Must be used with --private option)"), ) parser.add_argument( '--description', metavar='', help=_("Description for the flavor.(Supported by API versions " "'2.55' - '2.latest'") ) identity_common.add_project_domain_option_to_parser(parser) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.sdk_connection.compute identity_client = self.app.client_manager.identity if parsed_args.project and parsed_args.public: msg = _("--project is only allowed with --private") raise exceptions.CommandError(msg) args = { 'name': parsed_args.name, 'ram': parsed_args.ram, 'vcpus': parsed_args.vcpus, 'disk': parsed_args.disk, 'id': parsed_args.id, 'ephemeral': parsed_args.ephemeral, 'swap': parsed_args.swap, 'rxtx_factor': parsed_args.rxtx_factor, 'is_public': parsed_args.public, } if parsed_args.description: if not sdk_utils.supports_microversion(compute_client, '2.55'): msg = _( 'The --description parameter requires server support for ' 'API microversion 2.55' ) raise exceptions.CommandError(msg) args['description'] = parsed_args.description flavor = compute_client.create_flavor(**args) if parsed_args.project: try: project_id = identity_common.find_project( identity_client, parsed_args.project, parsed_args.project_domain, ).id compute_client.flavor_add_tenant_access( flavor.id, project_id) except Exception as e: msg = _("Failed to add project %(project)s access to " "flavor: %(e)s") LOG.error(msg, {'project': parsed_args.project, 'e': e}) if parsed_args.properties: try: flavor = compute_client.create_flavor_extra_specs( flavor, parsed_args.properties) except Exception as e: LOG.error(_("Failed to set flavor properties: %s"), e) display_columns, columns = _get_flavor_columns(flavor) data = utils.get_dict_properties(flavor, columns, formatters=_formatters) return (display_columns, data) class DeleteFlavor(command.Command): _description = _("Delete flavor(s)") def get_parser(self, prog_name): parser = super(DeleteFlavor, self).get_parser(prog_name) parser.add_argument( "flavor", metavar="", nargs='+', help=_("Flavor(s) to delete (name or ID)") ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.sdk_connection.compute result = 0 for f in parsed_args.flavor: try: flavor = compute_client.find_flavor(f, ignore_missing=False) compute_client.delete_flavor(flavor.id) except Exception as e: result += 1 LOG.error(_("Failed to delete flavor with name or " "ID '%(flavor)s': %(e)s"), {'flavor': f, 'e': e}) if result > 0: total = len(parsed_args.flavor) msg = (_("%(result)s of %(total)s flavors failed " "to delete.") % {'result': result, 'total': total}) raise exceptions.CommandError(msg) class ListFlavor(command.Lister): _description = _("List flavors") def get_parser(self, prog_name): parser = super(ListFlavor, self).get_parser(prog_name) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( "--public", dest="public", action="store_true", default=True, help=_("List only public flavors (default)") ) public_group.add_argument( "--private", dest="public", action="store_false", help=_("List only private flavors") ) public_group.add_argument( "--all", dest="all", action="store_true", default=False, help=_("List all flavors, whether public or private") ) parser.add_argument( '--min-disk', type=int, metavar='', help=_('Filters the flavors by a minimum disk space, in GiB.'), ) parser.add_argument( '--min-ram', type=int, metavar='', help=_('Filters the flavors by a minimum RAM, in MiB.'), ) parser.add_argument( '--long', action='store_true', default=False, help=_("List additional fields in output") ) parser.add_argument( '--marker', metavar="", help=_("The last flavor ID of the previous page") ) parser.add_argument( '--limit', type=int, metavar='', help=_( 'Maximum number of flavors to display. This is also ' 'configurable on the server. The actual limit used will be ' 'the lower of the user-supplied value and the server ' 'configuration-derived value' ), ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.sdk_connection.compute # is_public is ternary - None means give all flavors, # True is public only and False is private only # By default Nova assumes True and gives admins public flavors # and flavors from their own projects only. is_public = None if parsed_args.all else parsed_args.public query_attrs = { 'is_public': is_public } if parsed_args.marker: query_attrs['marker'] = parsed_args.marker if parsed_args.limit: query_attrs['limit'] = parsed_args.limit if parsed_args.limit or parsed_args.marker: # User passed explicit pagination request, switch off SDK # pagination query_attrs['paginated'] = False if parsed_args.min_disk: query_attrs['min_disk'] = parsed_args.min_disk if parsed_args.min_ram: query_attrs['min_ram'] = parsed_args.min_ram data = list(compute_client.flavors(**query_attrs)) # Even if server supports 2.61 some policy might stop it sending us # extra_specs. So try to fetch them if they are absent for f in data: if not f.extra_specs: compute_client.fetch_flavor_extra_specs(f) columns = ( "id", "name", "ram", "disk", "ephemeral", "vcpus", "is_public" ) if parsed_args.long: columns += ( "swap", "rxtx_factor", "extra_specs", ) column_headers = ( "ID", "Name", "RAM", "Disk", "Ephemeral", "VCPUs", "Is Public" ) if parsed_args.long: column_headers += ( "Swap", "RXTX Factor", "Properties", ) return ( column_headers, ( utils.get_item_properties(s, columns, formatters=_formatters) for s in data ), ) class SetFlavor(command.Command): _description = _("Set flavor properties") def get_parser(self, prog_name): parser = super(SetFlavor, self).get_parser(prog_name) parser.add_argument( "flavor", metavar="", help=_("Flavor to modify (name or ID)") ) parser.add_argument( "--no-property", action="store_true", help=_("Remove all properties from this flavor " "(specify both --no-property and --property" " to remove the current properties before setting" " new properties.)"), ) parser.add_argument( "--property", metavar="", action=parseractions.KeyValueAction, dest="properties", help=_("Property to add or modify for this flavor " "(repeat option to set multiple properties)") ) parser.add_argument( '--project', metavar='', help=_('Set flavor access to project (name or ID) ' '(admin only)'), ) identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( '--description', metavar='', help=_("Set description for the flavor.(Supported by API " "versions '2.55' - '2.latest'") ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.sdk_connection.compute identity_client = self.app.client_manager.identity try: flavor = compute_client.find_flavor( parsed_args.flavor, get_extra_specs=True, ignore_missing=False) except sdk_exceptions.ResourceNotFound as e: raise exceptions.CommandError(e.message) if parsed_args.description: if not sdk_utils.supports_microversion(compute_client, '2.55'): msg = _( 'The --description parameter requires server support for ' 'API microversion 2.55' ) raise exceptions.CommandError(msg) compute_client.update_flavor( flavor=flavor.id, description=parsed_args.description) result = 0 if parsed_args.no_property: try: for key in flavor.extra_specs.keys(): compute_client.delete_flavor_extra_specs_property( flavor.id, key) except Exception as e: LOG.error(_("Failed to clear flavor properties: %s"), e) result += 1 if parsed_args.properties: try: compute_client.create_flavor_extra_specs( flavor.id, parsed_args.properties) except Exception as e: LOG.error(_("Failed to set flavor properties: %s"), e) result += 1 if parsed_args.project: try: if flavor.is_public: msg = _("Cannot set access for a public flavor") raise exceptions.CommandError(msg) else: project_id = identity_common.find_project( identity_client, parsed_args.project, parsed_args.project_domain, ).id compute_client.flavor_add_tenant_access( flavor.id, project_id) except Exception as e: LOG.error(_("Failed to set flavor access to project: %s"), e) result += 1 if result > 0: raise exceptions.CommandError(_("Command Failed: One or more of" " the operations failed")) class ShowFlavor(command.ShowOne): _description = _("Display flavor details") def get_parser(self, prog_name): parser = super(ShowFlavor, self).get_parser(prog_name) parser.add_argument( "flavor", metavar="", help=_("Flavor to display (name or ID)") ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.sdk_connection.compute flavor = compute_client.find_flavor( parsed_args.flavor, get_extra_specs=True, ignore_missing=False) access_projects = None # get access projects list of this flavor if not flavor.is_public: try: flavor_access = compute_client.get_flavor_access( flavor=flavor.id) access_projects = [ utils.get_field(access, 'tenant_id') for access in flavor_access] except Exception as e: msg = _("Failed to get access projects list " "for flavor '%(flavor)s': %(e)s") LOG.error(msg, {'flavor': parsed_args.flavor, 'e': e}) # Since we need to inject "access_project_id" into resource - convert # it to dict and treat it respectively flavor = flavor.to_dict() flavor['access_project_ids'] = access_projects display_columns, columns = _get_flavor_columns(flavor) data = utils.get_dict_properties( flavor, columns, formatters=_formatters) return (display_columns, data) class UnsetFlavor(command.Command): _description = _("Unset flavor properties") def get_parser(self, prog_name): parser = super(UnsetFlavor, self).get_parser(prog_name) parser.add_argument( "flavor", metavar="", help=_("Flavor to modify (name or ID)") ) parser.add_argument( "--property", metavar="", action='append', dest="properties", help=_("Property to remove from flavor " "(repeat option to unset multiple properties)") ) parser.add_argument( '--project', metavar='', help=_('Remove flavor access from project (name or ID) ' '(admin only)'), ) identity_common.add_project_domain_option_to_parser(parser) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.sdk_connection.compute identity_client = self.app.client_manager.identity try: flavor = compute_client.find_flavor( parsed_args.flavor, get_extra_specs=True, ignore_missing=False) except sdk_exceptions.ResourceNotFound as e: raise exceptions.CommandError(_(e.message)) result = 0 if parsed_args.properties: for key in parsed_args.properties: try: compute_client.delete_flavor_extra_specs_property( flavor.id, key) except sdk_exceptions.SDKException as e: LOG.error(_("Failed to unset flavor property: %s"), e) result += 1 if parsed_args.project: try: if flavor.is_public: msg = _("Cannot remove access for a public flavor") raise exceptions.CommandError(msg) project_id = identity_common.find_project( identity_client, parsed_args.project, parsed_args.project_domain, ).id compute_client.flavor_remove_tenant_access( flavor.id, project_id) except Exception as e: LOG.error(_("Failed to remove flavor access from project: %s"), e) result += 1 if result > 0: raise exceptions.CommandError(_("Command Failed: One or more of" " the operations failed"))