diff --git a/docs/vcd_search.md b/docs/vcd_search.md index 5aa9024d..06490e68 100644 --- a/docs/vcd_search.md +++ b/docs/vcd_search.md @@ -7,7 +7,7 @@ Usage: vcd search [OPTIONS] [resource-type] Search for resources of the provided type. Resource type is not case sensitive. When invoked without a resource type, list the available types to search for. Admin types are only allowed when the user is - the system administrator. + the system administrator. By default result is order by id.  Filters can be applied to the search.  @@ -44,7 +44,19 @@ Usage: vcd search [OPTIONS] [resource-type]  vcd search vm Search for virtual machines. - +  + vcd search vm --fields 'name,vdcName,status' + Search for virtual machines and show only some fields. +  + vcd search vm --fields 'name,vdcName,status' --hide-id --sort-asc vdcName + Search for virtual machines and show only some fields order y vdcName. +  + vcd search adminOrgVdc --fields 'name,orgName,providerVdcName' --hide-id --sort-asc name + Search all vdc and show only some fields order y name. +  + vcd search vm --fields 'containerName as containerName(vapp),name,ownerName as owner,isAutoNature as standalone' \ + --sort-asc containerName --filter 'isVAppTemplate==false' --hide-id + Search for virtual machines, show only some fields, use 'as' to customize field name Options: -f, --filter [query-filter] query filter diff --git a/system_tests/search_tests.py b/system_tests/search_tests.py new file mode 100644 index 00000000..093bf470 --- /dev/null +++ b/system_tests/search_tests.py @@ -0,0 +1,244 @@ +# VMware vCloud Director vCD CLI +# Copyright (c) 2018 VMware, 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. + +from click.testing import CliRunner + +import os +import yaml + +from pyvcloud.system_test_framework.base_test import BaseTestCase +from pyvcloud.system_test_framework.environment import Environment +from vcd_cli.login import login, logout +from vcd_cli.search import search + + +class SearchTest(BaseTestCase): + """Test search-related commands + + Tests cases in this module do not have ordering dependencies, + so setup is accomplished using Python unittest setUp and tearDown + methods. + + Be aware that this test will delete existing vcd-cli sessions. + """ + + _sys_admin_id = None + + @classmethod + def setUpClass(cls): + if 'VCD_TEST_BASE_CONFIG_FILE' in os.environ: + cls._config_file = os.environ['VCD_TEST_BASE_CONFIG_FILE'] + with open(cls._config_file, 'r') as f: + cls._config_yaml = yaml.safe_load(f) + + Environment.init(cls._config_yaml) + # We Don't need to setup further our Cloud Director so we skip attach vc, create pvcd ... + + @classmethod + def tearDownClass(cls): + Environment.cleanup() + + def setUp(self): + """Load configuration , get sys_admin ID and create a click runner to invoke CLI.""" + self._config = Environment.get_config() + self._logger = Environment.get_default_logger() + + client = Environment.get_sys_admin_client() + org = client.get_org() + org_href = org.get('href') + admin_user = self._config['vcd']['sys_admin_username'] + user = client.get_user_in_org(admin_user, org_href) + urn = user.get('id') + self._sys_admin_id = urn.split(":").pop() + self._logger.debug("sys_admin id: {0}".format(self._sys_admin_id)) + client.logout() + + self._runner = CliRunner() + self._login() + + def tearDown(self): + """Logout ignoring any errors to ensure test session is gone.""" + self._logout() + + def _login(self): + """Logs in using admin credentials""" + host = self._config['vcd']['host'] + org = self._config['vcd']['sys_org_name'] + admin_user = self._config['vcd']['sys_admin_username'] + admin_pass = self._config['vcd']['sys_admin_pass'] + login_args = [ + host, org, admin_user, "-i", "-w", + "--password={0}".format(admin_pass) + ] + result = self._runner.invoke(login, args=login_args) + self.assertEqual(0, result.exit_code) + self.assertTrue("logged in" in result.output) + + def _logout(self): + """Logs out current session, ignoring errors""" + self._runner.invoke(logout) + + def test_0010_search_without_arg(self): + """Search command is going to output help and valid resources type + """ + result = self._runner.invoke(search) + self._logger.debug("vcd search: {0}".format(result.output)) + self.assertEqual(0, result.exit_code) + self.assertTrue("user" in result.output) + self.assertTrue("cell" in result.output) + self.assertTrue("virtualCenter" in result.output) + self.assertTrue("organization" in result.output) + self.assertTrue("adminOrgVdc" in result.output) + self.assertTrue("vApp" in result.output) + self.assertTrue("adminVM" in result.output) + + def test_0020_search_valid_resource_type(self): + """Search a valid resource (ex: user) + """ + result = self._runner.invoke(search, args=['user']) + self._logger.debug("vcd search user: {0}".format(result.output)) + self.assertEqual(0, result.exit_code) + self.assertTrue("name" in result.output) + self.assertTrue("fullName" in result.output) + self.assertTrue("numberOfDeployedVMs" in result.output) + + def test_0030_search_with_filter(self): + """Search with a filter (in 'user' ressources we are going to find our user) + """ + admin_user = self._config['vcd']['sys_admin_username'] + filter = 'name==%s' % (admin_user) + result = self._runner.invoke(search, args=['user', '--filter', filter]) + self._logger.debug( + "vcd search user --filter {1}: {0}".format(result.output, filter)) + self.assertEqual(0, result.exit_code) + self.assertTrue(self._sys_admin_id in result.output) + self.assertTrue(admin_user in result.output) + self.assertTrue("name" in result.output) + self.assertTrue("fullName" in result.output) + self.assertTrue("numberOfDeployedVMs" in result.output) + + def test_0031_search_with_filter_and_no_match(self): + """Search with a filter on an unexpected user + """ + unexpected_user = 'xxxClark_kent_is_not_zorro_but_he_is_an_hero_xxx' + filter = 'name==%s' % (unexpected_user) + result = self._runner.invoke(search, args=['user', '--filter', filter]) + self._logger.debug( + "vcd search user --filter {1}: {0}".format(result.output, filter)) + self.assertEqual(0, result.exit_code) + self.assertTrue("not found" in result.output) + + def test_0040_search_with_fields(self): + """Search with fields: name, fullName and isEnable + """ + admin_user = self._config['vcd']['sys_admin_username'] + fields = 'name,fullName,isEnabled' + result = self._runner.invoke(search, args=['user', '--fields', fields]) + self._logger.debug( + "vcd search user --fields {1}: {0}".format(result.output, fields)) + self.assertEqual(0, result.exit_code) + self.assertTrue(self._sys_admin_id in result.output) + self.assertTrue(admin_user in result.output) + self.assertTrue("name" in result.output) + self.assertTrue("fullName" in result.output) + self.assertTrue("isEnabled" in result.output) + self.assertFalse("numberOfDeployedVMs" in result.output) + + def test_0041_search_with_fields_as_label(self): + """Search with fields: name as username, fullName as 'the full name' and isEnable as is-enable + """ + admin_user = self._config['vcd']['sys_admin_username'] + fields = 'name as username,fullName as the full name,isEnabled as is-enabled' + result = self._runner.invoke(search, args=['user', '--fields', fields]) + self._logger.debug( + "vcd search user --fields {1}: {0}".format(result.output, fields)) + self.assertEqual(0, result.exit_code) + self.assertTrue(self._sys_admin_id in result.output) + self.assertTrue(admin_user in result.output) + self.assertTrue("username" in result.output) + self.assertTrue("the full name" in result.output) + self.assertTrue("is-enabled" in result.output) + self.assertFalse("isEnabled" in result.output) + self.assertFalse("fullName" in result.output) + self.assertFalse("numberOfDeployedVMs" in result.output) + + def test_0050_search_and_hide_id(self): + """Search and hide id. The session user id should not be in the result. + """ + admin_user = self._config['vcd']['sys_admin_username'] + result = self._runner.invoke(search, args=['user', '--hide-id']) + self._logger.debug( + "vcd search user --hide-id: {0}".format(result.output)) + self.assertEqual(0, result.exit_code) + self.assertFalse(self._sys_admin_id in result.output) + self.assertTrue("name" in result.output) + self.assertTrue("fullName" in result.output) + self.assertTrue(admin_user in result.output) + + def test_0060_search_sort_asc(self): + """Search and sort asc on isEnabled. + """ + admin_user = self._config['vcd']['sys_admin_username'] + result = self._runner.invoke( + search, args=['user', '--sort-asc', 'isEnabled']) + self._logger.debug( + "vcd search user --sort-asc isEnabled: {0}".format(result.output)) + self.assertEqual(0, result.exit_code) + self.assertTrue(self._sys_admin_id in result.output) + self.assertTrue("name" in result.output) + self.assertTrue("fullName" in result.output) + self.assertTrue(admin_user in result.output) + + def test_0061_search_sort_asc_on_two_fields(self): + """Search and sort asc on isEnabled and then on fullName. + """ + admin_user = self._config['vcd']['sys_admin_username'] + result = self._runner.invoke( + search, args=['user', '--sort-asc', 'isEnabled', '--sort-next', 'fullName']) + self._logger.debug( + "vcd search user --sort-asc isEnabled: {0}".format(result.output)) + self.assertEqual(0, result.exit_code) + self.assertTrue(self._sys_admin_id in result.output) + self.assertTrue("name" in result.output) + self.assertTrue("fullName" in result.output) + self.assertTrue(admin_user in result.output) + + def test_0070_search_sort_desc(self): + """Search and sort desc on isEnabled. + """ + admin_user = self._config['vcd']['sys_admin_username'] + result = self._runner.invoke( + search, args=['user', '--sort-desc', 'isEnabled']) + self._logger.debug( + "vcd search user --sort-desc isEnabled: {0}".format(result.output)) + self.assertEqual(0, result.exit_code) + self.assertTrue(self._sys_admin_id in result.output) + self.assertTrue("name" in result.output) + self.assertTrue("fullName" in result.output) + self.assertTrue(admin_user in result.output) + + def test_0071_search_sort_desc_on_two_fields(self): + """Search and sort desc on isEnabled and then on fullName. + """ + admin_user = self._config['vcd']['sys_admin_username'] + result = self._runner.invoke( + search, args=['user', '--sort-desc', 'isEnabled', '--sort-next', 'fullName']) + self._logger.debug( + "vcd search user --sort-desc isEnabled: {0}".format(result.output)) + self.assertEqual(0, result.exit_code) + self.assertTrue(self._sys_admin_id in result.output) + self.assertTrue("name" in result.output) + self.assertTrue("fullName" in result.output) + self.assertTrue(admin_user in result.output) diff --git a/vcd_cli/search.py b/vcd_cli/search.py index 1fb8f026..6b3f5508 100644 --- a/vcd_cli/search.py +++ b/vcd_cli/search.py @@ -36,7 +36,40 @@ required=False, metavar='[query-filter]', help='query filter') -def search(ctx, resource_type, query_filter): +@click.option( + '-t', + '--fields', + 'fields', + required=False, + metavar='[fields]', + help='fields to show') +@click.option( + '--show-id/--hide-id', + 'show_id', + required=False, + is_flag=True, + show_default=True, + default=True, + help='show id') +@click.option( + '--sort-asc', + 'sort_asc', + required=False, + metavar='[field]', + help='sort in ascending order on a field') +@click.option( + '--sort-desc', + 'sort_desc', + required=False, + metavar='[field]', + help='sort in descending order on a field') +@click.option( + '--sort-next', + 'sort_next', + required=False, + metavar='[field]', + help='--sort_asc or --sort-desc on a second field') +def search(ctx, resource_type, query_filter, fields, show_id, sort_asc, sort_desc, sort_next): """Search for resources in vCloud Director. \b @@ -44,7 +77,7 @@ def search(ctx, resource_type, query_filter): Search for resources of the provided type. Resource type is not case sensitive. When invoked without a resource type, list the available types to search for. Admin types are only allowed when the user is - the system administrator. + the system administrator. By default result is order by id. \b Filters can be applied to the search. \b @@ -81,28 +114,93 @@ def search(ctx, resource_type, query_filter): \b vcd search vm Search for virtual machines. +\b + vcd search vm --fields 'name,vdcName,status' + Search for virtual machines and show only some fields. +\b + vcd search vm --fields 'name,vdcName,status' --hide-id --sort-asc vdcName + Search for virtual machines and show only some fields order y vdcName. +\b + vcd search adminOrgVdc --fields 'name,orgName,providerVdcName' --hide-id --sort-asc name + Search all vdc and show only some fields order y name. +\b + vcd search vm --fields 'containerName as containerName(vapp),name,ownerName as owner,isAutoNature as standalone' \\ + --sort-asc containerName --filter 'isVAppTemplate==false' --hide-id + Search for virtual machines, show only some fields, use 'as' to customize field name. +\b + vcd search vm --fields 'containerName as vapp,name' --sort-asc containerName --sort-next name --hide-id + Search for virtual machines and show only some fields. """ - try: if resource_type is None: click.secho(ctx.get_help()) click.echo('\nAvailable resource types:') click.echo(tabulate(tabulate_names(RESOURCE_TYPES, 4))) return + result = query(ctx, resource_type, query_filter, fields, sort_asc, sort_desc, sort_next) + if not result: + result = 'not found' + stdout(result, ctx, show_id=show_id, sort_headers=False) + except Exception as e: + stderr(e, ctx) + +def query(ctx, resource_type=None, query_filter=None, fields=None, sort_asc=None, sort_desc=None, sort_next=None): + try: + if resource_type is None: + raise Exception('resource_type can\'t be None') restore_session(ctx) client = ctx.obj['client'] result = [] resource_type_cc = to_camel_case(resource_type, RESOURCE_TYPES) + headers={} + if fields: + for f in fields.split(','): + field, *label = f.split(' as ') + headers[field] = field + if label: + label = label.pop() + headers[field] = label + fields = ','.join(headers.keys()) q = client.get_typed_query( resource_type_cc, query_result_format=QueryResultFormat.ID_RECORDS, - qfilter=query_filter) + qfilter=query_filter, + fields=fields, + sort_asc=sort_asc, + sort_desc=sort_desc) records = list(q.execute()) if len(records) == 0: - result = 'not found' - else: - for r in records: - result.append(to_dict(r, resource_type=resource_type_cc)) - stdout(result, ctx, show_id=True) + return [] + for r in records: + d = to_dict(r, resource_type=resource_type_cc) + if headers: + d_with_custom_header = { 'id': d.pop('id') } + for field, label in headers.items(): + d_with_custom_header[label] = None + if field in d: + d_with_custom_header[label] = d.pop(field) + d = d_with_custom_header + result.append(d) + if sort_next and (sort_asc or sort_desc): + if sort_asc: + reverse=False + sort_key1 = sort_asc + if sort_desc: + reverse=True + sort_key1 = sort_desc + sort_key2=sort_next + if sort_key1 in headers: + sort_key1 = headers[sort_key1] + if sort_key2 in headers: + sort_key2 = headers[sort_key2] + keys = list(result[0].keys()) + if sort_key1 not in keys: + raise Exception('sort key \'%s\' not in %s' % (sort_key1, keys)) + if sort_key2 not in keys: + raise Exception('sort_next \'%s\' not in %s' % (sort_key2, keys)) + result=sorted(result, key=lambda d: (d[sort_key1], d[sort_key2]), reverse=reverse) + elif sort_next: + raise Exception('sort_next must be used with sort_asc or sort_desc') + return result except Exception as e: stderr(e, ctx) diff --git a/vcd_cli/utils.py b/vcd_cli/utils.py index 95f93f83..ab9b6f60 100644 --- a/vcd_cli/utils.py +++ b/vcd_cli/utils.py @@ -59,7 +59,7 @@ def as_table(obj_list, if sort_headers: headers = sorted(obj_list[0].keys()) else: - headers = obj_list[0].keys() + headers = list(obj_list[0].keys()) if not show_id and 'id' in headers: headers.remove('id') for field in hide_fields: