diff --git a/releasenotes/notes/new-commmand-vnf-resource-list-d5422ab917f0892f.yaml b/releasenotes/notes/new-commmand-vnf-resource-list-d5422ab917f0892f.yaml new file mode 100644 index 00000000..be72aa00 --- /dev/null +++ b/releasenotes/notes/new-commmand-vnf-resource-list-d5422ab917f0892f.yaml @@ -0,0 +1,4 @@ +--- +features: + - Adds new CLI command 'vnf-resource-list' to view VNF + resources, such as VDU, CP, etc. diff --git a/tackerclient/shell.py b/tackerclient/shell.py index 5f57d092..f1b72195 100644 --- a/tackerclient/shell.py +++ b/tackerclient/shell.py @@ -117,6 +117,7 @@ COMMAND_V1 = { 'vnf-list': vnf.ListVNF, 'vnf-show': vnf.ShowVNF, 'vnf-scale': vnf.ScaleVNF, + 'vnf-resource-list': vnf.ListVNFResources, # 'vnf-config-create' # 'vnf-config-push' diff --git a/tackerclient/tacker/v1_0/vm/vnf.py b/tackerclient/tacker/v1_0/vm/vnf.py index 9d6d6bd3..f7e7f54a 100644 --- a/tackerclient/tacker/v1_0/vm/vnf.py +++ b/tackerclient/tacker/v1_0/vm/vnf.py @@ -15,10 +15,12 @@ # License for the specific language governing permissions and limitations # under the License. +from tackerclient.i18n import _ from tackerclient.tacker import v1_0 as tackerV10 _VNF = 'vnf' +_RESOURCE = 'resource' class ListVNF(tackerV10.ListCommand): @@ -149,6 +151,73 @@ class DeleteVNF(tackerV10.DeleteCommand): resource = _VNF +class ListVNFResources(tackerV10.ListCommand): + """List resources of a VNF like VDU, CP, etc.""" + + list_columns = ['name', 'id', 'type'] + allow_names = True + resource = _VNF + + def get_id(self): + if self.resource: + return self.resource.upper() + + def get_parser(self, prog_name): + parser = super(ListVNFResources, self).get_parser(prog_name) + if self.allow_names: + help_str = _('ID or name of %s to look up') + else: + help_str = _('ID of %s to look up') + parser.add_argument( + 'id', metavar=self.get_id(), + help=help_str % self.resource) + return parser + + def get_data(self, parsed_args): + self.log.debug('get_data(%s)', parsed_args) + tacker_client = self.get_client() + tacker_client.format = parsed_args.request_format + if self.allow_names: + _id = tackerV10.find_resourceid_by_name_or_id(tacker_client, + self.resource, + parsed_args.id) + else: + _id = parsed_args.id + + data = self.retrieve_list_by_id(_id, parsed_args) + self.extend_list(data, parsed_args) + return self.setup_columns(data, parsed_args) + + def retrieve_list_by_id(self, id, parsed_args): + """Retrieve a list of sub resources from Tacker server""" + tacker_client = self.get_client() + tacker_client.format = parsed_args.request_format + _extra_values = tackerV10.parse_args_to_dict(self.values_specs) + tackerV10._merge_args(self, parsed_args, _extra_values, + self.values_specs) + search_opts = self.args2search_opts(parsed_args) + search_opts.update(_extra_values) + if self.pagination_support: + page_size = parsed_args.page_size + if page_size: + search_opts.update({'limit': page_size}) + if self.sorting_support: + keys = parsed_args.sort_key + if keys: + search_opts.update({'sort_key': keys}) + dirs = parsed_args.sort_dir + len_diff = len(keys) - len(dirs) + if len_diff > 0: + dirs += ['asc'] * len_diff + elif len_diff < 0: + dirs = dirs[:len(keys)] + if dirs: + search_opts.update({'sort_dir': dirs}) + obj_lister = getattr(tacker_client, "list_vnf_resources") + data = obj_lister(id, **search_opts) + return data.get('resources', []) + + class ScaleVNF(tackerV10.TackerCommand): """Scale a VNF.""" diff --git a/tackerclient/tests/unit/test_cli10.py b/tackerclient/tests/unit/test_cli10.py index 9bf99aa1..28b9c662 100644 --- a/tackerclient/tests/unit/test_cli10.py +++ b/tackerclient/tests/unit/test_cli10.py @@ -369,6 +369,139 @@ class CLITestV10Base(testtools.TestCase): self.assertIn('myid1', _str) return _str + def _test_list_sub_resources(self, resources, api_resource, cmd, myid, + detail=False, + tags=[], fields_1=[], fields_2=[], + page_size=None, sort_key=[], sort_dir=[], + response_contents=None, base_args=None, + path=None): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + if response_contents is None: + contents = [{self.id_field: 'myid1', }, + {self.id_field: 'myid2', }, ] + else: + contents = response_contents + reses = {api_resource: contents} + self.client.format = self.format + resstr = self.client.serialize(reses) + # url method body + query = "" + args = base_args if base_args is not None else [] + if detail: + args.append('-D') + args.extend(['--request-format', self.format]) + if fields_1: + for field in fields_1: + args.append('--fields') + args.append(field) + + if tags: + args.append('--') + args.append("--tag") + for tag in tags: + args.append(tag) + if isinstance(tag, six.string_types): + tag = urllib.quote(tag.encode('utf-8')) + if query: + query += "&tag=" + tag + else: + query = "tag=" + tag + if (not tags) and fields_2: + args.append('--') + if fields_2: + args.append("--fields") + for field in fields_2: + args.append(field) + if detail: + query = query and query + '&verbose=True' or 'verbose=True' + fields_1.extend(fields_2) + for field in fields_1: + if query: + query += "&fields=" + field + else: + query = "fields=" + field + if page_size: + args.append("--page-size") + args.append(str(page_size)) + if query: + query += "&limit=%s" % page_size + else: + query = "limit=%s" % page_size + if sort_key: + for key in sort_key: + args.append('--sort-key') + args.append(key) + if query: + query += '&' + query += 'sort_key=%s' % key + if sort_dir: + len_diff = len(sort_key) - len(sort_dir) + if len_diff > 0: + sort_dir += ['asc'] * len_diff + elif len_diff < 0: + sort_dir = sort_dir[:len(sort_key)] + for dir in sort_dir: + args.append('--sort-dir') + args.append(dir) + if query: + query += '&' + query += 'sort_dir=%s' % dir + if path is None: + path = getattr(self.client, resources + "_path") + self.client.httpclient.request( + MyUrlComparator(end_url(path % myid, query, format=self.format), + self.client), + 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + resources) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + if response_contents is None: + self.assertIn('myid1', _str) + return _str + + def _test_list_sub_resources_with_pagination(self, resources, api_resource, + cmd, myid): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + path = getattr(self.client, resources + "_path") + fake_query = "marker=myid2&limit=2" + reses1 = {api_resource: [{'id': 'myid1', }, + {'id': 'myid2', }], + '%s_links' % api_resource: [ + {'href': end_url(path % myid, fake_query), + 'rel': 'next'}] + } + reses2 = {api_resource: [{'id': 'myid3', }, + {'id': 'myid4', }]} + self.client.format = self.format + resstr1 = self.client.serialize(reses1) + resstr2 = self.client.serialize(reses2) + self.client.httpclient.request( + end_url(path % myid, "", format=self.format), 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr1)) + self.client.httpclient.request( + end_url(path % myid, fake_query, format=self.format), 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr2)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + resources) + args = [myid, '--request-format', self.format] + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + def _test_list_resources_with_pagination(self, resources, cmd): self.mox.StubOutWithMock(cmd, "get_client") self.mox.StubOutWithMock(self.client.httpclient, "request") diff --git a/tackerclient/tests/unit/vm/test_cli10_vnf.py b/tackerclient/tests/unit/vm/test_cli10_vnf.py index 5fc2712c..1b44bb87 100644 --- a/tackerclient/tests/unit/vm/test_cli10_vnf.py +++ b/tackerclient/tests/unit/vm/test_cli10_vnf.py @@ -32,9 +32,11 @@ ENDURL = 'localurl' class CLITestV10VmVNFJSON(test_cli10.CLITestV10Base): _RESOURCE = 'vnf' _RESOURCES = 'vnfs' + _VNF_RESOURCES = 'vnf_resources' def setUp(self): - plurals = {'vnfs': 'vnf'} + plurals = {'vnfs': 'vnf', + 'resources': 'resource'} super(CLITestV10VmVNFJSON, self).setUp(plurals=plurals) def _test_create_resource(self, resource, cmd, @@ -192,3 +194,22 @@ class CLITestV10VmVNFJSON(test_cli10.CLITestV10Base): my_id = 'my-id' args = [my_id] self._test_delete_resource(self._RESOURCE, cmd, my_id, args) + + def test_list_vnf_resources(self): + cmd = vnf.ListVNFResources(test_cli10.MyApp(sys.stdout), None) + base_args = [self.test_id] + response = [{'name': 'CP11', 'id': 'id1', 'type': 'NeutronPort'}, + {'name': 'CP12', 'id': 'id2', 'type': 'NeutronPort'}] + val = self._test_list_sub_resources(self._VNF_RESOURCES, 'resources', + cmd, self.test_id, + response_contents=response, + detail=True, base_args=base_args) + self.assertIn('id1', val) + self.assertIn('NeutronPort', val) + self.assertIn('CP11', val) + + def test_list_vnf_resources_pagination(self): + cmd = vnf.ListVNFResources(test_cli10.MyApp(sys.stdout), None) + self._test_list_sub_resources_with_pagination(self._VNF_RESOURCES, + 'resources', cmd, + self.test_id) diff --git a/tackerclient/v1_0/client.py b/tackerclient/v1_0/client.py index bc193258..38d81cd1 100644 --- a/tackerclient/v1_0/client.py +++ b/tackerclient/v1_0/client.py @@ -339,6 +339,7 @@ class Client(ClientBase): vnfs_path = '/vnfs' vnf_path = '/vnfs/%s' vnf_scale_path = '/vnfs/%s/actions' + vnf_resources_path = '/vnfs/%s/resources' vims_path = '/vims' vim_path = '/vims/%s' @@ -425,6 +426,11 @@ class Client(ClientBase): def update_vnf(self, vnf, body=None): return self.put(self.vnf_path % vnf, body=body) + @APIParamsCall + def list_vnf_resources(self, vnf, retrieve_all=True, **_params): + return self.list('resources', self.vnf_resources_path % vnf, + retrieve_all, **_params) + @APIParamsCall def scale_vnf(self, vnf, body=None): return self.post(self.vnf_scale_path % vnf, body=body)