From 573de86f7f7496dc292d842eff97e93d116b7a40 Mon Sep 17 00:00:00 2001
From: gongysh <gong.yongsheng@99cloud.net>
Date: Sat, 20 Jan 2018 19:55:11 +0800
Subject: [PATCH] Complete VIM osc commands

Change-Id: I3cb21b0583020db8a91789e2168d2a51d45d0cf4
Implements: blueprint tacker-support-python-openstackclient
---
 setup.cfg                            |   4 +
 tackerclient/osc/sdk_utils.py        | 102 +++++++++++++
 tackerclient/osc/utils.py            |  13 ++
 tackerclient/osc/v1/nfvo/vim.py      | 213 ++++++++++++++++++++++++++-
 tackerclient/tacker/v1_0/nfvo/vim.py |   2 +-
 5 files changed, 332 insertions(+), 2 deletions(-)
 create mode 100644 tackerclient/osc/sdk_utils.py

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="<VIM>",
+            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="<VIM>",
+            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