diff --git a/setup.cfg b/setup.cfg index 24609cbc..4a37b104 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,11 @@ openstack.cli.extension = tackerclient = tackerclient.osc.plugin openstack.tackerclient.v1 = + vim_register = tackerclient.osc.v1.nfvo.vim:CreateVIM vim_list = tackerclient.osc.v1.nfvo.vim:ListVIM + vim_set = tackerclient.osc.v1.nfvo.vim:UpdateVIM + vim_delete = tackerclient.osc.v1.nfvo.vim:DeleteVIM + vim_show = tackerclient.osc.v1.nfvo.vim:ShowVIM [build_sphinx] diff --git a/tackerclient/osc/sdk_utils.py b/tackerclient/osc/sdk_utils.py new file mode 100644 index 00000000..5d969bde --- /dev/null +++ b/tackerclient/osc/sdk_utils.py @@ -0,0 +1,102 @@ +# 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. + + +def get_osc_show_columns_for_sdk_resource( + sdk_resource, + osc_column_map, + invisible_columns=None +): + """Get and filter the display and attribute columns for an SDK resource. + + Common utility function for preparing the output of an OSC show command. + Some of the columns may need to get renamed, others made invisible. + + :param sdk_resource: An SDK resource + :param osc_column_map: A hash of mappings for display column names + :param invisible_columns: A list of invisible column names + + :returns: Two tuples containing the names of the display and attribute + columns + """ + + if getattr(sdk_resource, 'allow_get', None) is not None: + resource_dict = sdk_resource.to_dict( + body=True, headers=False, ignore_none=False) + else: + resource_dict = sdk_resource + + # Build the OSC column names to display for the SDK resource. + attr_map = {} + display_columns = list(resource_dict.keys()) + invisible_columns = [] if invisible_columns is None else invisible_columns + for col_name in invisible_columns: + if col_name in display_columns: + display_columns.remove(col_name) + for sdk_attr, osc_attr in osc_column_map.items(): + if sdk_attr in display_columns: + attr_map[osc_attr] = sdk_attr + display_columns.remove(sdk_attr) + if osc_attr not in display_columns: + display_columns.append(osc_attr) + sorted_display_columns = sorted(display_columns) + + # Build the SDK attribute names for the OSC column names. + attr_columns = [] + for column in sorted_display_columns: + new_column = attr_map[column] if column in attr_map else column + attr_columns.append(new_column) + return tuple(sorted_display_columns), tuple(attr_columns) + + +class DictModel(dict): + """Convert dict into an object that provides attribute access to values.""" + + def __init__(self, *args, **kwargs): + """Convert dict values to DictModel values.""" + super(DictModel, self).__init__(*args, **kwargs) + + def needs_upgrade(item): + return isinstance(item, dict) and not isinstance(item, DictModel) + + def upgrade(item): + """Upgrade item if it needs to be upgraded.""" + if needs_upgrade(item): + return DictModel(item) + else: + return item + + for key, value in self.items(): + if isinstance(value, (list, tuple)): + # Keep the same type but convert dicts to DictModels + self[key] = type(value)( + (upgrade(item) for item in value) + ) + elif needs_upgrade(value): + # Change dict instance values to DictModel instance values + self[key] = DictModel(value) + + def __getattr__(self, name): + try: + return self[name] + except KeyError as e: + raise AttributeError(e) + + def __setattr__(self, name, value): + self[name] = value + + def __delattr__(self, name): + del self[name] + + def __str__(self): + pairs = ['%s=%s' % (k, v) for k, v in self.items()] + return ', '.join(sorted(pairs)) diff --git a/tackerclient/osc/utils.py b/tackerclient/osc/utils.py index 9b82f856..8c31051d 100644 --- a/tackerclient/osc/utils.py +++ b/tackerclient/osc/utils.py @@ -26,6 +26,7 @@ from keystoneclient import exceptions as identity_exc from keystoneclient.v3 import domains from keystoneclient.v3 import projects from osc_lib import utils +from oslo_serialization import jsonutils from tackerclient.i18n import _ @@ -35,6 +36,18 @@ LIST_SHORT_ONLY = 'short_only' LIST_LONG_ONLY = 'long_only' +def format_dict_with_indention(data): + """Return a formatted string of key value pairs + + :param data: a dict + :rtype: a string formatted to key='value' + """ + + if data is None: + return None + return jsonutils.dumps(data, indent=4) + + def get_column_definitions(attr_map, long_listing): """Return table headers and column names for a listing table. diff --git a/tackerclient/osc/v1/nfvo/vim.py b/tackerclient/osc/v1/nfvo/vim.py index 4a4e8f98..99cb99bd 100644 --- a/tackerclient/osc/v1/nfvo/vim.py +++ b/tackerclient/osc/v1/nfvo/vim.py @@ -14,11 +14,18 @@ # License for the specific language governing permissions and limitations # under the License. +import yaml + from osc_lib.command import command from osc_lib import utils +from oslo_utils import strutils +from tackerclient.common import exceptions from tackerclient.i18n import _ +from tackerclient.osc import sdk_utils from tackerclient.osc import utils as tacker_osc_utils +from tackerclient.tacker import v1_0 as tackerV10 +from tackerclient.tacker.v1_0.nfvo import vim_utils _attr_map = ( ('id', 'ID', tacker_osc_utils.LIST_BOTH), @@ -32,6 +39,8 @@ _attr_map = ( ('status', 'Status', tacker_osc_utils.LIST_BOTH), ) +_VIM = 'vim' + class ListVIM(command.Lister): _description = _("List VIMs that belong to a given tenant.") @@ -53,4 +62,206 @@ class ListVIM(command.Lister): return (headers, (utils.get_dict_properties( s, columns, - ) for s in data['vims'])) + ) for s in data[_VIM + 's'])) + + +class ShowVIM(command.ShowOne): + _description = _("Display VIM details") + + def get_parser(self, prog_name): + parser = super(ShowVIM, self).get_parser(prog_name) + parser.add_argument( + _VIM, + metavar="", + help=_("VIM to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.tackerclient + obj_id = tackerV10.find_resourceid_by_name_or_id( + client, _VIM, parsed_args.vim) + obj = client.show_vim(obj_id) + display_columns, columns = _get_columns(obj[_VIM]) + data = utils.get_item_properties( + sdk_utils.DictModel(obj[_VIM]), + columns, + formatters=_formatters) + return (display_columns, data) + + +class CreateVIM(command.ShowOne): + _description = _("Register a new VIM") + + def get_parser(self, prog_name): + parser = super(CreateVIM, self).get_parser(prog_name) + parser.add_argument( + 'name', metavar='NAME', + help=_('Set a name for the VIM')) + parser.add_argument( + '--config-file', + required=True, + help=_('YAML file with VIM configuration parameters')) + parser.add_argument( + '--description', + help=_('Set a description for the VIM')) + parser.add_argument( + '--is-default', + action='store_true', + default=False, + help=_('Set as default VIM')) + return parser + + def args2body(self, parsed_args): + body = {_VIM: {}} + if parsed_args.config_file: + with open(parsed_args.config_file) as f: + vim_config = f.read() + try: + config_param = yaml.load(vim_config, + Loader=yaml.SafeLoader) + except yaml.YAMLError as e: + raise exceptions.InvalidInput(e) + vim_obj = body[_VIM] + try: + auth_url = config_param.pop('auth_url') + except KeyError: + raise exceptions.TackerClientException(message='Auth URL must be ' + 'specified', + status_code=404) + vim_obj['auth_url'] = vim_utils.validate_auth_url(auth_url).geturl() + vim_utils.args2body_vim(config_param, vim_obj) + tackerV10.update_dict(parsed_args, body[_VIM], + ['tenant_id', 'name', 'description', + 'is_default']) + return body + + def take_action(self, parsed_args): + client = self.app.client_manager.tackerclient + vim = client.create_vim(self.args2body(parsed_args)) + display_columns, columns = _get_columns(vim[_VIM]) + data = utils.get_item_properties( + sdk_utils.DictModel(vim[_VIM]), + columns, formatters=_formatters) + return (display_columns, data) + + +class DeleteVIM(command.Command): + _description = _("Delete VIM(s).") + + def get_parser(self, prog_name): + parser = super(DeleteVIM, self).get_parser(prog_name) + parser.add_argument( + _VIM, + metavar="", + nargs="+", + help=_("VIM(s) to delete (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.tackerclient + failure = False + deleted_ids = [] + failed_items = {} + for resource_id in parsed_args.vim: + try: + obj = tackerV10.find_resourceid_by_name_or_id( + client, _VIM, resource_id) + client.delete_vim(obj) + deleted_ids.append(resource_id) + except Exception as e: + failure = True + failed_items[resource_id] = e + if failure: + msg = '' + if deleted_ids: + msg = (_('Successfully deleted %(resource)s(s):' + ' %(deleted_list)s') % {'deleted_list': + ', '.join(deleted_ids), + 'resource': _VIM}) + err_msg = _("\n\nUnable to delete the below" + " %s(s):") % _VIM + for failed_id, error in failed_items.iteritems(): + err_msg += (_('\n Cannot delete %(failed_id)s: %(error)s') + % {'failed_id': failed_id, + 'error': error}) + msg += err_msg + raise exceptions.CommandError(msg) + else: + print((_('All specified %(resource)s(s) deleted successfully') + % {'resource': _VIM})) + return + + +class UpdateVIM(command.ShowOne): + _description = _("Update VIM.") + + def get_parser(self, prog_name): + parser = super(UpdateVIM, self).get_parser(prog_name) + parser.add_argument( + 'id', metavar="VIM", + help=_('ID or name of %s to update') % _VIM) + parser.add_argument( + '--config-file', + required=False, + help=_('YAML file with VIM configuration parameters')) + parser.add_argument( + '--name', + help=_('New name for the VIM')) + parser.add_argument( + '--description', + help=_('New description for the VIM')) + parser.add_argument( + '--is-default', + type=strutils.bool_from_string, + metavar='{True,False}', + help=_('Indicate whether the VIM is used as default')) + return parser + + def args2body(self, parsed_args): + body = {_VIM: {}} + config_param = None + # config arg passed as data overrides config yaml when both args passed + if parsed_args.config_file: + with open(parsed_args.config_file) as f: + config_yaml = f.read() + try: + config_param = yaml.load(config_yaml) + except yaml.YAMLError as e: + raise exceptions.InvalidInput(e) + vim_obj = body[_VIM] + if config_param is not None: + vim_utils.args2body_vim(config_param, vim_obj) + tackerV10.update_dict(parsed_args, body[_VIM], + ['tenant_id', 'name', 'description', + 'is_default']) + # type attribute is read-only, it can't be updated, so remove it + # in update method + body[_VIM].pop('type', None) + return body + + def take_action(self, parsed_args): + client = self.app.client_manager.tackerclient + obj_id = tackerV10.find_resourceid_by_name_or_id( + client, _VIM, parsed_args.id) + vim = client.update_vim(obj_id, self.args2body(parsed_args)) + display_columns, columns = _get_columns(vim[_VIM]) + data = utils.get_item_properties( + sdk_utils.DictModel(vim[_VIM]), columns, + formatters=_formatters) + return (display_columns, data) + + +_formatters = { + 'auth_cred': tacker_osc_utils.format_dict_with_indention, + 'placement_attr': tacker_osc_utils.format_dict_with_indention, + 'vim_project': tacker_osc_utils.format_dict_with_indention, +} + + +def _get_columns(item): + column_map = { + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) diff --git a/tackerclient/tacker/v1_0/nfvo/vim.py b/tackerclient/tacker/v1_0/nfvo/vim.py index 1cb037af..0f924b83 100644 --- a/tackerclient/tacker/v1_0/nfvo/vim.py +++ b/tackerclient/tacker/v1_0/nfvo/vim.py @@ -128,7 +128,7 @@ class UpdateVIM(tackerV10.UpdateCommand): 'is_default']) # type attribute is read-only, it can't be updated, so remove it # in update method - body['vim'].pop('type') + body['vim'].pop('type', None) return body