From 880168e5b840a23d841273f3a72fe53eab98c614 Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Thu, 17 Apr 2025 16:27:30 -0500 Subject: [PATCH 01/13] Add sorting based on predefined columns --- blazarclient/command.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/blazarclient/command.py b/blazarclient/command.py index 5a1e875..176fcfe 100644 --- a/blazarclient/command.py +++ b/blazarclient/command.py @@ -273,6 +273,11 @@ def setup_columns(self, info, parsed_args): columns = { col for col in self.list_columns if col in columns } | valid_parsed_columns + # sort the columns based on list_columns + sorting_map = {item: i for i, item in enumerate(self.list_columns)} + # sort key is either index of item in list_columns, or placed at end of list + columns = sorted(columns, key=lambda x: sorting_map.get(x, len(self.list_columns))) + return ( columns, (utils.get_item_properties(s, columns, formatters=self._formatters) From 5b130f71ad7f39da04ac242e960f14d16457d359 Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Fri, 18 Apr 2025 10:44:55 -0500 Subject: [PATCH 02/13] Remove custom formatter, add --long flag --- blazarclient/command.py | 51 +++++++------------ blazarclient/tests/test_command.py | 14 ----- blazarclient/v1/shell_commands/allocations.py | 1 - blazarclient/v1/shell_commands/devices.py | 3 +- blazarclient/v1/shell_commands/hosts.py | 1 + blazarclient/v1/shell_commands/leases.py | 1 + blazarclient/v1/shell_commands/networks.py | 1 + 7 files changed, 22 insertions(+), 50 deletions(-) diff --git a/blazarclient/command.py b/blazarclient/command.py index 176fcfe..23cbb18 100644 --- a/blazarclient/command.py +++ b/blazarclient/command.py @@ -92,31 +92,6 @@ def get_parser(self, prog_name): parser = super(BlazarCommand, self).get_parser(prog_name) return parser - def format_output_data(self, data): - for k, v in data.items(): - if isinstance(v, str): - try: - # Deserialize if possible into dict, lists, tuples... - v = ast.literal_eval(v) - except SyntaxError: - # NOTE(sbauza): This is probably a datetime string, we need - # to keep it unchanged. - pass - except ValueError: - # NOTE(sbauza): This is not something AST can evaluate, - # probably a string. - pass - if isinstance(v, list): - value = '\n'.join(utils.dumps( - i, indent=self.json_indent) if isinstance(i, dict) - else str(i) for i in v) - data[k] = value - elif isinstance(v, dict): - value = utils.dumps(v, indent=self.json_indent) - data[k] = value - elif v is None: - data[k] = '' - def add_known_arguments(self, parser): pass @@ -137,7 +112,6 @@ def get_data(self, parsed_args): body = self.args2body(parsed_args) resource_manager = getattr(blazar_client, self.resource) data = resource_manager.create(**body) - self.format_output_data(data) if data: print('Created a new %s:' % self.resource, file=self.app.stdout) @@ -229,6 +203,7 @@ class ListCommand(BlazarCommand, lister.Lister): log = None _formatters = {} list_columns = [] + long_columns = [] unknown_parts_flag = True def args2body(self, parsed_args): @@ -236,6 +211,8 @@ def args2body(self, parsed_args): if parsed_args.sort_by: if parsed_args.sort_by in self.list_columns: params['sort_by'] = parsed_args.sort_by + elif self.long_columns and parsed_args.sort_by in self.long_columns: + params['sort_by'] = parsed_args.sort_by else: msg = 'Invalid sort option %s' % parsed_args.sort_by raise exception.BlazarClientException(msg) @@ -243,6 +220,12 @@ def args2body(self, parsed_args): def get_parser(self, prog_name): parser = super(ListCommand, self).get_parser(prog_name) + if self.long_columns: + parser.add_argument( + '--long', + action='store_true', + help='Display detailed information for each item' + ) return parser def retrieve_list(self, parsed_args): @@ -269,14 +252,18 @@ def setup_columns(self, info, parsed_args): valid_parsed_columns = {col for col in parsed_args.columns if col in columns} else: valid_parsed_columns = set() - if self.list_columns: + + default_columns = self.list_columns + if self.long_columns and parsed_args.long: + default_columns += self.long_columns + if default_columns: columns = { - col for col in self.list_columns if col in columns + col for col in default_columns if col in columns } | valid_parsed_columns # sort the columns based on list_columns - sorting_map = {item: i for i, item in enumerate(self.list_columns)} + sorting_map = {item: i for i, item in enumerate(default_columns)} # sort key is either index of item in list_columns, or placed at end of list - columns = sorted(columns, key=lambda x: sorting_map.get(x, len(self.list_columns))) + columns = sorted(columns, key=lambda x: sorting_map.get(x, len(default_columns))) return ( columns, @@ -328,7 +315,6 @@ def get_data(self, parsed_args): resource_manager = getattr(blazar_client, self.resource) data = resource_manager.get(res_id) - self.format_output_data(data) return list(zip(*sorted(data.items()))) @@ -351,7 +337,6 @@ def get_data(self, parsed_args): blazar_client = self.get_client() resource_manager = getattr(blazar_client, self.resource) data = resource_manager.get_allocation(parsed_args.id) - self.format_output_data(data) return list(zip(*sorted(data.items()))) @@ -412,8 +397,6 @@ def get_data(self, parsed_args): blazar_client = self.get_client() resource_manager = getattr(blazar_client, self.resource) data = resource_manager.get_property(parsed_args.property_name) - if parsed_args.formatter == 'table': - self.format_output_data(data) return list(zip(*sorted(data.items()))) diff --git a/blazarclient/tests/test_command.py b/blazarclient/tests/test_command.py index 0b79371..9dd2f52 100644 --- a/blazarclient/tests/test_command.py +++ b/blazarclient/tests/test_command.py @@ -77,20 +77,6 @@ def test_get_parser(self): self.command.get_parser('TestCase') self.parser.assert_called_once_with('TestCase') - def test_format_output_data(self): - data_before = {'key_string': 'string_value', - 'key_dict': {'key': 'value'}, - 'key_list': ['1', '2', '3'], - 'key_none': None} - data_after = {'key_string': 'string_value', - 'key_dict': '{"key": "value"}', - 'key_list': '1\n2\n3', - 'key_none': ''} - - self.command.format_output_data(data_before) - - self.assertEqual(data_after, data_before) - class CreateCommandTestCase(tests.TestCase): def setUp(self): diff --git a/blazarclient/v1/shell_commands/allocations.py b/blazarclient/v1/shell_commands/allocations.py index 7ee195e..938e2e5 100644 --- a/blazarclient/v1/shell_commands/allocations.py +++ b/blazarclient/v1/shell_commands/allocations.py @@ -83,7 +83,6 @@ def get_data(self, parsed_args): filter(lambda d: d['id'] == parsed_args.reservation_id, data['reservations'])) - self.format_output_data(data) return list(zip(*sorted(data.items()))) def args2body(self, parsed_args): diff --git a/blazarclient/v1/shell_commands/devices.py b/blazarclient/v1/shell_commands/devices.py index f9b5f4e..f5577dd 100644 --- a/blazarclient/v1/shell_commands/devices.py +++ b/blazarclient/v1/shell_commands/devices.py @@ -22,7 +22,8 @@ class ListDevices(command.ListCommand): """Print a list of devices.""" resource = 'device' log = logging.getLogger(__name__ + '.ListDevices') - list_columns = ['id', 'name', 'device_type', 'device_driver'] + list_columns = ['id', 'name', 'device_type'] + long_columns = ['machine_name', 'model', 'device_name', 'device_driver', 'reservable'] def get_parser(self, prog_name): parser = super(ListDevices, self).get_parser(prog_name) diff --git a/blazarclient/v1/shell_commands/hosts.py b/blazarclient/v1/shell_commands/hosts.py index c61bcd4..3bb90b1 100644 --- a/blazarclient/v1/shell_commands/hosts.py +++ b/blazarclient/v1/shell_commands/hosts.py @@ -28,6 +28,7 @@ class ListHosts(command.ListCommand): log = logging.getLogger(__name__ + '.ListHosts') list_columns = ['id', 'hypervisor_hostname', 'vcpus', 'memory_mb', 'local_gb'] + long_columns = ['node_name', 'node_type', 'disabled', 'reservable'] def get_parser(self, prog_name): parser = super(ListHosts, self).get_parser(prog_name) diff --git a/blazarclient/v1/shell_commands/leases.py b/blazarclient/v1/shell_commands/leases.py index a3dec83..23173cf 100644 --- a/blazarclient/v1/shell_commands/leases.py +++ b/blazarclient/v1/shell_commands/leases.py @@ -87,6 +87,7 @@ class ListLeases(command.ListCommand): resource = 'lease' log = logging.getLogger(__name__ + '.ListLeases') list_columns = ['id', 'name', 'start_date', 'end_date'] + long_columns = ["status", "created_at", "degraded"] def get_parser(self, prog_name): parser = super(ListLeases, self).get_parser(prog_name) diff --git a/blazarclient/v1/shell_commands/networks.py b/blazarclient/v1/shell_commands/networks.py index a72aa87..f16bcf0 100644 --- a/blazarclient/v1/shell_commands/networks.py +++ b/blazarclient/v1/shell_commands/networks.py @@ -24,6 +24,7 @@ class ListNetworks(command.ListCommand): resource = 'network' log = logging.getLogger(__name__ + '.ListNetworks') list_columns = ['id', 'network_type', 'physical_network', 'segment_id'] + long_columns = ['stitch_provider'] def get_parser(self, prog_name): parser = super(ListNetworks, self).get_parser(prog_name) From 9693f1cb53d5f8263fcb8267925194416f8b47b1 Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Fri, 18 Apr 2025 13:37:53 -0500 Subject: [PATCH 03/13] Re-add json formatting for dict --- blazarclient/command.py | 26 ++++++++++++++++++++++++++ blazarclient/tests/test_command.py | 14 ++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/blazarclient/command.py b/blazarclient/command.py index 23cbb18..6896e3f 100644 --- a/blazarclient/command.py +++ b/blazarclient/command.py @@ -92,6 +92,29 @@ def get_parser(self, prog_name): parser = super(BlazarCommand, self).get_parser(prog_name) return parser + def format_output_data(self, data, parsed_args): + # Do not format output data if the formatter is not table + if parsed_args.formatter != 'table': + return + for k, v in data.items(): + if isinstance(v, str): + try: + # Deserialize if possible into dict, lists, tuples... + v = ast.literal_eval(v) + except SyntaxError: + # NOTE(sbauza): This is probably a datetime string, we need + # to keep it unchanged. + pass + except ValueError: + # NOTE(sbauza): This is not something AST can evaluate, + # probably a string. + pass + elif isinstance(v, list) or isinstance(v, dict): + value = utils.dumps(v, indent=self.json_indent) + data[k] = value + elif v is None: + data[k] = '' + def add_known_arguments(self, parser): pass @@ -112,6 +135,7 @@ def get_data(self, parsed_args): body = self.args2body(parsed_args) resource_manager = getattr(blazar_client, self.resource) data = resource_manager.create(**body) + self.format_output_data(data, parsed_args) if data: print('Created a new %s:' % self.resource, file=self.app.stdout) @@ -315,6 +339,7 @@ def get_data(self, parsed_args): resource_manager = getattr(blazar_client, self.resource) data = resource_manager.get(res_id) + self.format_output_data(data, parsed_args) return list(zip(*sorted(data.items()))) @@ -337,6 +362,7 @@ def get_data(self, parsed_args): blazar_client = self.get_client() resource_manager = getattr(blazar_client, self.resource) data = resource_manager.get_allocation(parsed_args.id) + self.format_output_data(data, parsed_args) return list(zip(*sorted(data.items()))) diff --git a/blazarclient/tests/test_command.py b/blazarclient/tests/test_command.py index 9dd2f52..0b79371 100644 --- a/blazarclient/tests/test_command.py +++ b/blazarclient/tests/test_command.py @@ -77,6 +77,20 @@ def test_get_parser(self): self.command.get_parser('TestCase') self.parser.assert_called_once_with('TestCase') + def test_format_output_data(self): + data_before = {'key_string': 'string_value', + 'key_dict': {'key': 'value'}, + 'key_list': ['1', '2', '3'], + 'key_none': None} + data_after = {'key_string': 'string_value', + 'key_dict': '{"key": "value"}', + 'key_list': '1\n2\n3', + 'key_none': ''} + + self.command.format_output_data(data_before) + + self.assertEqual(data_after, data_before) + class CreateCommandTestCase(tests.TestCase): def setUp(self): From 510e82f2bf85ff9841c9b92d927c60a8ae241f79 Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Fri, 18 Apr 2025 14:33:00 -0500 Subject: [PATCH 04/13] Add lease filters --- blazarclient/command.py | 7 ++++-- blazarclient/v1/shell_commands/leases.py | 27 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/blazarclient/command.py b/blazarclient/command.py index 6896e3f..f3c7bc1 100644 --- a/blazarclient/command.py +++ b/blazarclient/command.py @@ -229,6 +229,7 @@ class ListCommand(BlazarCommand, lister.Lister): list_columns = [] long_columns = [] unknown_parts_flag = True + _filters = [] def args2body(self, parsed_args): params = {} @@ -258,6 +259,8 @@ def retrieve_list(self, parsed_args): body = self.args2body(parsed_args) resource_manager = getattr(blazar_client, self.resource) data = resource_manager.list(**body) + for f in self._filters: + data = list(filter(f, data)) return data def setup_columns(self, info, parsed_args): @@ -282,8 +285,8 @@ def setup_columns(self, info, parsed_args): default_columns += self.long_columns if default_columns: columns = { - col for col in default_columns if col in columns - } | valid_parsed_columns + col for col in default_columns if col in columns + } | valid_parsed_columns # sort the columns based on list_columns sorting_map = {item: i for i, item in enumerate(default_columns)} # sort key is either index of item in list_columns, or placed at end of list diff --git a/blazarclient/v1/shell_commands/leases.py b/blazarclient/v1/shell_commands/leases.py index 23173cf..03aa264 100644 --- a/blazarclient/v1/shell_commands/leases.py +++ b/blazarclient/v1/shell_commands/leases.py @@ -96,8 +96,35 @@ def get_parser(self, prog_name): help='column name used to sort result', default='name' ) + parser.add_argument( + '--project-id', metavar="", + help='ID of the project to filter leases by', + ) + parser.add_argument( + '--status', metavar="", + help='status to filter leases by', + ) + parser.add_argument( + '--user', metavar="", + help='User ID to filter leases by', + ) return parser + def get_data(self, parsed_args): + if parsed_args.project_id: + self._filters.append( + lambda x: x['project_id'] == parsed_args.project_id + ) + if parsed_args.status: + self._filters.append( + lambda x: x['status'].lower() == parsed_args.status.lower() + ) + if parsed_args.user: + self._filters.append( + lambda x: x['user_id'] == parsed_args.user + ) + return super(ListLeases, self).get_data(parsed_args) + class ShowLease(command.ShowCommand): """Show details about the given lease.""" From 873b5fc69a51a45a6407ab75693d45207c92d1f0 Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Fri, 18 Apr 2025 15:04:02 -0500 Subject: [PATCH 05/13] Add host filters --- blazarclient/v1/shell_commands/hosts.py | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/blazarclient/v1/shell_commands/hosts.py b/blazarclient/v1/shell_commands/hosts.py index 3bb90b1..ecce92f 100644 --- a/blazarclient/v1/shell_commands/hosts.py +++ b/blazarclient/v1/shell_commands/hosts.py @@ -37,8 +37,35 @@ def get_parser(self, prog_name): help='column name used to sort result', default='hypervisor_hostname' ) + parser.add_argument( + '--node-type', metavar="", + help='Node type to filter leases by', + ) + parser.add_argument( + '--reservable', + help='List only reservable hosts', + action='store_true', + default=None, + ) + parser.add_argument( + '--unreservable', + help='List only unreservable hosts', + action='store_false', + dest='reservable', + ) return parser + def get_data(self, parsed_args): + if parsed_args.node_type: + self._filters.append( + lambda x: x['node_type'] == parsed_args.node_type + ) + if parsed_args.reservable is not None: + self._filters.append( + lambda x: x['reservable'] == parsed_args.reservable + ) + return super(ListHosts, self).get_data(parsed_args) + class ShowHost(command.ShowCommand): """Show host details.""" From eaad71fcef522c18b551c916245bde72f0f96196 Mon Sep 17 00:00:00 2001 From: Anish Reddy <2anishreddy@gmail.com> Date: Wed, 10 Apr 2024 08:28:23 -0500 Subject: [PATCH 06/13] fetching additional resources reserved in a lease (#31) * fetching additional resources reserved in a lease, such as hosts, networks, and devices command to accept a new --detail flag, enabling users to request all resources reserved in a lease * Parallelize calls to get allocations in lease detail get Parallelize the calls to hosts_in_lease, networks_in_lease, and devices_in_lease using Python's concurrent.futures module. By submitting these calls to a thread pool for concurrent execution and retrieving the results once the tasks are completed, the performance of the get method is potentially improved. --- blazarclient/command.py | 3 ++- blazarclient/v1/leases.py | 30 +++++++++++++++++++++++- blazarclient/v1/shell_commands/leases.py | 11 +++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/blazarclient/command.py b/blazarclient/command.py index f3c7bc1..a7edf8a 100644 --- a/blazarclient/command.py +++ b/blazarclient/command.py @@ -329,6 +329,7 @@ def get_parser(self, prog_name): def get_data(self, parsed_args): self.log.debug('get_data(%s)' % parsed_args) + body = self.args2body(parsed_args) blazar_client = self.get_client() if self.allow_names: @@ -341,7 +342,7 @@ def get_data(self, parsed_args): res_id = parsed_args.id resource_manager = getattr(blazar_client, self.resource) - data = resource_manager.get(res_id) + data = resource_manager.get(res_id, detail=body['detail']) self.format_output_data(data, parsed_args) return list(zip(*sorted(data.items()))) diff --git a/blazarclient/v1/leases.py b/blazarclient/v1/leases.py index 2fb59cf..1db10ad 100644 --- a/blazarclient/v1/leases.py +++ b/blazarclient/v1/leases.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from concurrent.futures import ThreadPoolExecutor + from oslo_utils import timeutils from blazarclient import base @@ -32,11 +34,22 @@ def create(self, name, start, end, reservations, events, before_end=None): resp, body = self.request_manager.post('/leases', body=values) return body['lease'] - def get(self, lease_id): + def get(self, lease_id, detail=False): """Describes lease specifications such as name, status and locked condition. """ resp, body = self.request_manager.get('/leases/%s' % lease_id) + if detail and body['lease']: + with ThreadPoolExecutor() as executor: + # Submit the calls + h_future = executor.submit(self.hosts_in_lease, lease_id) + n_future = executor.submit(self.networks_in_lease, lease_id) + d_future = executor.submit(self.devices_in_lease, lease_id) + + # Retrieve the results + body['lease']['hosts'] = h_future.result() + body['lease']['networks'] = n_future.result() + body['lease']['devices'] = d_future.result() return body['lease'] def update(self, lease_id, name=None, prolong_for=None, reduce_by=None, @@ -94,6 +107,21 @@ def list(self, sort_by=None): leases = sorted(leases, key=lambda l: l[sort_by]) return leases + def hosts_in_lease(self, lease_id): + """List all hosts in lease""" + resp, body = self.request_manager.get(f'/leases/{lease_id}/hosts') + return body['hosts'] + + def networks_in_lease(self, lease_id): + """List all networks in lease""" + resp, body = self.request_manager.get(f'/leases/{lease_id}/networks') + return body['networks'] + + def devices_in_lease(self, lease_id): + """List all devices in lease""" + resp, body = self.request_manager.get(f'/leases/{lease_id}/devices') + return body['devices'] + def _add_lease_date(self, values, lease, key, delta_date, positive_delta): delta_sec = utils.from_elapsed_time_to_delta( delta_date, diff --git a/blazarclient/v1/shell_commands/leases.py b/blazarclient/v1/shell_commands/leases.py index 03aa264..19d034f 100644 --- a/blazarclient/v1/shell_commands/leases.py +++ b/blazarclient/v1/shell_commands/leases.py @@ -135,6 +135,12 @@ class ShowLease(command.ShowCommand): def get_parser(self, prog_name): parser = super(ShowLease, self).get_parser(prog_name) + parser.add_argument( + '--detail', + action='store_true', + help='Return all resources reserved in lease.', + default=False + ) if self.allow_names: help_str = 'ID or name of %s to look up' else: @@ -143,6 +149,11 @@ def get_parser(self, prog_name): help=help_str % self.resource) return parser + def args2body(self, parsed_args): + params = {} + params['detail'] = parsed_args.detail + return params + class CreateLeaseBase(command.CreateCommand): """Create a lease.""" From 3ec65f9f4a9698eeb531a8840b4e1e37910a8da2 Mon Sep 17 00:00:00 2001 From: Anish Reddy Ravula <2anishreddy@gmail.com> Date: Mon, 18 Mar 2024 16:01:19 -0500 Subject: [PATCH 07/13] Add support for setting capability uniqueness in blazar client Updated the `UpdateCapabilityCommand` in the blazar client to include a new `--unique` flag, allowing users to set a capability as unique when updating or creating a capability. Also, modified the `set_capability` method in the `ComputeHostClientManager` to include the `is_unique` parameter when sending a PATCH request to update the capability. This enhancement enables users to specify whether a capability should be unique or not via the blazar client. --- blazarclient/command.py | 7 +++++++ blazarclient/v1/hosts.py | 6 +++--- blazarclient/v1/shell_commands/hosts.py | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/blazarclient/command.py b/blazarclient/command.py index a7edf8a..29f77b3 100644 --- a/blazarclient/command.py +++ b/blazarclient/command.py @@ -465,9 +465,16 @@ def get_parser(self, prog_name): default=False, help='Set property to public.' ) + parser.add_argument( + '--unique', + action='store_true', + default=False, + help='Set capability as unique.' + ) return parser def args2body(self, parsed_args): return dict( property_name=parsed_args.property_name, + is_unique=(parsed_args.unique is True), private=(parsed_args.private is True)) diff --git a/blazarclient/v1/hosts.py b/blazarclient/v1/hosts.py index 54f7c52..a3dff70 100644 --- a/blazarclient/v1/hosts.py +++ b/blazarclient/v1/hosts.py @@ -105,10 +105,10 @@ def get_property(self, property_name): if x['property'] == property_name] if not resource_property: raise exception.ResourcePropertyNotFound() - return resource_property[0] + return {} if not resource_property else resource_property[0] - def set_property(self, property_name, private): - data = {'private': private} + def set_property(self, property_name, private, is_unique=False): + data = {'private': private, 'is_unique': is_unique} resp, body = self.request_manager.patch( '/os-hosts/properties/%s' % property_name, body=data) diff --git a/blazarclient/v1/shell_commands/hosts.py b/blazarclient/v1/shell_commands/hosts.py index ecce92f..1cdc82f 100644 --- a/blazarclient/v1/shell_commands/hosts.py +++ b/blazarclient/v1/shell_commands/hosts.py @@ -260,7 +260,7 @@ class ListHostProperties(command.ListCommand): """List host properties.""" resource = 'host' log = logging.getLogger(__name__ + '.ListHostProperties') - list_columns = ['property', 'private', 'property_values'] + list_columns = ['property', 'private', 'property_values', 'is_unique'] def args2body(self, parsed_args): params = { From de843b7ef7fdfba118e685eab54b3bfe1025c751 Mon Sep 17 00:00:00 2001 From: Anish Reddy <2anishreddy@gmail.com> Date: Tue, 16 Apr 2024 11:22:07 -0500 Subject: [PATCH 08/13] Fix lease get detail (#33) * Fix 'detail' parameter usage in Blazar client show command This commit fixes the handling of the 'detail' parameter in the Blazar client show command. Since other resources like hosts and FIPs do not have a detail keyword in their manager get method, the previous implementation caused errors. This commit removes the unnecessary usage of 'detail' and instead calls the 'additional_details' method if the parsed args have 'detail' arg to fetch additional details Additionally, tests are added to cover the corrected behavior of the show command with and without the 'detail' parameter. * Add github actions to run unittests --- .github/workflows/test.yml | 31 +++++++++++ blazarclient/command.py | 4 +- .../tests/v1/shell_commands/test_leases.py | 52 ++++++++++++++++++- blazarclient/v1/leases.py | 25 +++++---- 4 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..af78130 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: Unit tests + +env: + # This should match the default python_version build arg + PYTHON_VERSION: 3.8 + TOX_ENV: py38 + +on: + push: + branches: + - "*" + pull_request: + types: [opened, reopened] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python 3.x + uses: actions/setup-python@v1 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install tox + run: pip install tox + + - name: Run tests + run: tox -e ${{ env.TOX_ENV }} \ No newline at end of file diff --git a/blazarclient/command.py b/blazarclient/command.py index 29f77b3..e8afeb5 100644 --- a/blazarclient/command.py +++ b/blazarclient/command.py @@ -342,7 +342,9 @@ def get_data(self, parsed_args): res_id = parsed_args.id resource_manager = getattr(blazar_client, self.resource) - data = resource_manager.get(res_id, detail=body['detail']) + data = resource_manager.get(res_id) + if body.get('detail'): + data.update(resource_manager.additional_details(res_id)) self.format_output_data(data, parsed_args) return list(zip(*sorted(data.items()))) diff --git a/blazarclient/tests/v1/shell_commands/test_leases.py b/blazarclient/tests/v1/shell_commands/test_leases.py index cdf1e8d..79b20b4 100644 --- a/blazarclient/tests/v1/shell_commands/test_leases.py +++ b/blazarclient/tests/v1/shell_commands/test_leases.py @@ -334,12 +334,33 @@ def test_show_lease(self): ] mock.seal(lease_manager) - args = argparse.Namespace(id=FIRST_LEASE) + args = argparse.Namespace(id=FIRST_LEASE, detail=False) expected = [('id',), (FIRST_LEASE,)] self.assertEqual(show_lease.get_data(args), expected) lease_manager.get.assert_called_once_with(FIRST_LEASE) + def test_show_lease_with_allocations(self): + show_lease, lease_manager = self.create_show_command() + lease_manager.get.return_value = {'id': FIRST_LEASE} + lease_manager.additional_details.return_value = { + 'hosts': [], + 'networks': [], + 'devices': [] + } + lease_manager.list.return_value = [ + {'id': FIRST_LEASE, 'name': 'first-lease'}, + {'id': SECOND_LEASE, 'name': 'second-lease'}, + ] + mock.seal(lease_manager) + + args = argparse.Namespace(id=FIRST_LEASE, detail=True) + expected = [('devices', 'hosts', 'id', 'networks',), ('', '', FIRST_LEASE, '',)] + + self.assertEqual(show_lease.get_data(args), expected) + lease_manager.get.assert_called_once_with(FIRST_LEASE) + lease_manager.additional_details.assert_called_once_with(FIRST_LEASE) + def test_show_lease_by_name(self): show_lease, lease_manager = self.create_show_command() lease_manager.list.return_value = [ @@ -349,13 +370,40 @@ def test_show_lease_by_name(self): lease_manager.get.return_value = {'id': SECOND_LEASE} mock.seal(lease_manager) - args = argparse.Namespace(id='second-lease') + args = argparse.Namespace(id='second-lease', detail=False) expected = [('id',), (SECOND_LEASE,)] self.assertEqual(show_lease.get_data(args), expected) lease_manager.list.assert_called_once_with() lease_manager.get.assert_called_once_with(SECOND_LEASE) + def test_show_lease_by_name_with_allocations(self): + show_lease, lease_manager = self.create_show_command() + lease_manager.list.return_value = [ + {'id': FIRST_LEASE, 'name': 'first-lease'}, + {'id': SECOND_LEASE, 'name': 'second-lease'}, + ] + lease_manager.get.return_value = {'id': SECOND_LEASE} + host1 = {'id': '101', 'hypervisor_hostname': 'host-1'} + lease_manager.additional_details.return_value = { + 'hosts': [host1], + 'networks': [], + 'devices': [] + } + import json + d = json.dumps(host1, indent=4) + mock.seal(lease_manager) + args = argparse.Namespace(id='second-lease', detail=True) + expected = [ + ('devices', 'hosts', 'id', 'networks',), + ('', d, SECOND_LEASE, '',) + ] + + self.assertEqual(show_lease.get_data(args), expected) + lease_manager.list.assert_called_once_with() + lease_manager.get.assert_called_once_with(SECOND_LEASE) + lease_manager.additional_details.assert_called_once_with(SECOND_LEASE) + class DeleteLeaseTestCase(tests.TestCase): diff --git a/blazarclient/v1/leases.py b/blazarclient/v1/leases.py index 1db10ad..791713e 100644 --- a/blazarclient/v1/leases.py +++ b/blazarclient/v1/leases.py @@ -39,17 +39,6 @@ def get(self, lease_id, detail=False): condition. """ resp, body = self.request_manager.get('/leases/%s' % lease_id) - if detail and body['lease']: - with ThreadPoolExecutor() as executor: - # Submit the calls - h_future = executor.submit(self.hosts_in_lease, lease_id) - n_future = executor.submit(self.networks_in_lease, lease_id) - d_future = executor.submit(self.devices_in_lease, lease_id) - - # Retrieve the results - body['lease']['hosts'] = h_future.result() - body['lease']['networks'] = n_future.result() - body['lease']['devices'] = d_future.result() return body['lease'] def update(self, lease_id, name=None, prolong_for=None, reduce_by=None, @@ -107,6 +96,20 @@ def list(self, sort_by=None): leases = sorted(leases, key=lambda l: l[sort_by]) return leases + def additional_details(self, lease_id): + allocations = {} + with ThreadPoolExecutor() as executor: + # Submit the calls + h_future = executor.submit(self.hosts_in_lease, lease_id) + n_future = executor.submit(self.networks_in_lease, lease_id) + d_future = executor.submit(self.devices_in_lease, lease_id) + + # Retrieve the results + allocations['hosts'] = h_future.result() + allocations['networks'] = n_future.result() + allocations['devices'] = d_future.result() + return allocations + def hosts_in_lease(self, lease_id): """List all hosts in lease""" resp, body = self.request_manager.get(f'/leases/{lease_id}/hosts') From 59c14add267bfd54924bbeab787de6f9e7aace82 Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Fri, 18 Apr 2025 17:06:42 -0500 Subject: [PATCH 09/13] Add username, lease_id filters to allocation --- blazarclient/command.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/blazarclient/command.py b/blazarclient/command.py index e8afeb5..f1c68cf 100644 --- a/blazarclient/command.py +++ b/blazarclient/command.py @@ -307,12 +307,43 @@ def get_data(self, parsed_args): class ListAllocationCommand(ListCommand, lister.Lister): """List allocations that belong to a given tenant.""" + def get_parser(self, prog_name): + parser = super(ListAllocationCommand, self).get_parser(prog_name) + parser.add_argument( + '--lease-id', metavar="", + help='Lease to filter allocations by', + ) + parser.add_argument( + '--username', metavar="", + help='Username to filter allocations by', + ) + return parser + def retrieve_list(self, parsed_args): """Retrieve a list of resources from Blazar server.""" blazar_client = self.get_client() body = self.args2body(parsed_args) resource_manager = getattr(blazar_client, self.resource) data = resource_manager.list_allocations(**body) + filters = [] + if parsed_args.lease_id: + filters.append(lambda x: x['lease_id'] == parsed_args.lease_id) + if parsed_args.username: + filters.append(lambda x: x.get("extras", {}).get('user_name') == parsed_args.username) + + if filters: + new_data = [] + for allocation in data: + new_reservations = [] + for reservation in allocation["reservations"]: + if all(f(reservation) for f in filters): + new_reservations.append(reservation) + if new_reservations: + new_data.append({ + "resource_id": allocation["resource_id"], + "reservations": new_reservations, + }) + data = new_data return data From 59b331c055cae8d357c4d108450ce925de7194e3 Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Mon, 21 Apr 2025 09:27:03 -0500 Subject: [PATCH 10/13] Fix GHA --- .github/workflows/test.yml | 44 +++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index af78130..1c2f635 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,19 +13,37 @@ on: types: [opened, reopened] jobs: - test: - runs-on: ubuntu-latest - + run_linting: + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v3 - - name: Set up Python 3.x - uses: actions/setup-python@v1 + run_tests: + strategy: + matrix: + include: + - python_version: "3.10" + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install tox - run: pip install tox - - - name: Run tests - run: tox -e ${{ env.TOX_ENV }} \ No newline at end of file + python-version: ${{ matrix.python_version }} + - name: install uv for speedup + run: pip install uv + - name: install test tools + run: | + uv pip install \ + --system \ + -r requirements.txt \ + -r test-requirements.txt \ + . + - name: init stestr repo + run: stestr init + - name: run stestr tests + run: stestr run + - name: list failing tests + run: stestr failing --list + if: always() #run even if tests fail \ No newline at end of file From 609be587955eb534753976a9bdb070e111153ba5 Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Mon, 21 Apr 2025 09:28:43 -0500 Subject: [PATCH 11/13] Fix GHA --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c2f635..124f63f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,6 @@ on: branches: - "*" pull_request: - types: [opened, reopened] jobs: run_linting: From ea533a710f3bae3f387976ffb418f5cbfc3d6861 Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Mon, 21 Apr 2025 09:52:40 -0500 Subject: [PATCH 12/13] Fix tests --- blazarclient/tests/test_command.py | 6 ++++-- .../tests/v1/shell_commands/test_floatingips.py | 2 +- blazarclient/tests/v1/shell_commands/test_hosts.py | 6 +++--- blazarclient/tests/v1/shell_commands/test_leases.py | 13 ++++++------- .../tests/v1/shell_commands/test_networks.py | 6 ++++-- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/blazarclient/tests/test_command.py b/blazarclient/tests/test_command.py index 0b79371..9f75b39 100644 --- a/blazarclient/tests/test_command.py +++ b/blazarclient/tests/test_command.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import argparse from unittest import mock import testtools @@ -84,10 +85,11 @@ def test_format_output_data(self): 'key_none': None} data_after = {'key_string': 'string_value', 'key_dict': '{"key": "value"}', - 'key_list': '1\n2\n3', + 'key_list': '["1", "2", "3"]', 'key_none': ''} - self.command.format_output_data(data_before) + args = argparse.Namespace(formatter="table") + self.command.format_output_data(data_before, args) self.assertEqual(data_after, data_before) diff --git a/blazarclient/tests/v1/shell_commands/test_floatingips.py b/blazarclient/tests/v1/shell_commands/test_floatingips.py index 9302574..5fe315b 100644 --- a/blazarclient/tests/v1/shell_commands/test_floatingips.py +++ b/blazarclient/tests/v1/shell_commands/test_floatingips.py @@ -104,7 +104,7 @@ def test_show_floatingip(self): show_floatingip, floatingip_manager = self.create_show_command( list_value, get_value) - args = argparse.Namespace(id='84c4d37e-1f8b-45ce-897b-16ad7f49b0e9') + args = argparse.Namespace(id='84c4d37e-1f8b-45ce-897b-16ad7f49b0e9', formatter="table") expected = [('id',), ('84c4d37e-1f8b-45ce-897b-16ad7f49b0e9',)] ret = show_floatingip.get_data(args) diff --git a/blazarclient/tests/v1/shell_commands/test_hosts.py b/blazarclient/tests/v1/shell_commands/test_hosts.py index 53c6df4..8552e84 100644 --- a/blazarclient/tests/v1/shell_commands/test_hosts.py +++ b/blazarclient/tests/v1/shell_commands/test_hosts.py @@ -178,7 +178,7 @@ def test_show_host(self): show_host, host_manager = self.create_show_command(list_value, get_value) - args = argparse.Namespace(id='101') + args = argparse.Namespace(id='101', formatter="table") expected = [('hypervisor_hostname', 'id'), ('host-1', '101')] ret = show_host.get_data(args) @@ -197,7 +197,7 @@ def test_show_host_with_name(self): show_host, host_manager = self.create_show_command(list_value, get_value) - args = argparse.Namespace(id='host-1') + args = argparse.Namespace(id='host-1', formatter="table") expected = [('hypervisor_hostname', 'id'), ('host-1', '101')] ret = show_host.get_data(args) @@ -215,7 +215,7 @@ def test_show_host_with_name_startwith_number(self): show_host, host_manager = self.create_show_command(list_value, get_value) - args = argparse.Namespace(id='1-host') + args = argparse.Namespace(id='1-host', formatter="table") expected = [('hypervisor_hostname', 'id'), ('1-host', '101')] ret = show_host.get_data(args) diff --git a/blazarclient/tests/v1/shell_commands/test_leases.py b/blazarclient/tests/v1/shell_commands/test_leases.py index 79b20b4..db2892a 100644 --- a/blazarclient/tests/v1/shell_commands/test_leases.py +++ b/blazarclient/tests/v1/shell_commands/test_leases.py @@ -334,7 +334,7 @@ def test_show_lease(self): ] mock.seal(lease_manager) - args = argparse.Namespace(id=FIRST_LEASE, detail=False) + args = argparse.Namespace(id=FIRST_LEASE, detail=False, formatter="table") expected = [('id',), (FIRST_LEASE,)] self.assertEqual(show_lease.get_data(args), expected) @@ -354,8 +354,8 @@ def test_show_lease_with_allocations(self): ] mock.seal(lease_manager) - args = argparse.Namespace(id=FIRST_LEASE, detail=True) - expected = [('devices', 'hosts', 'id', 'networks',), ('', '', FIRST_LEASE, '',)] + args = argparse.Namespace(id=FIRST_LEASE, detail=True, formatter="table") + expected = [('devices', 'hosts', 'id', 'networks',), ('[]', '[]', FIRST_LEASE, '[]',)] self.assertEqual(show_lease.get_data(args), expected) lease_manager.get.assert_called_once_with(FIRST_LEASE) @@ -370,7 +370,7 @@ def test_show_lease_by_name(self): lease_manager.get.return_value = {'id': SECOND_LEASE} mock.seal(lease_manager) - args = argparse.Namespace(id='second-lease', detail=False) + args = argparse.Namespace(id='second-lease', detail=False, formatter="table") expected = [('id',), (SECOND_LEASE,)] self.assertEqual(show_lease.get_data(args), expected) @@ -393,12 +393,11 @@ def test_show_lease_by_name_with_allocations(self): import json d = json.dumps(host1, indent=4) mock.seal(lease_manager) - args = argparse.Namespace(id='second-lease', detail=True) + args = argparse.Namespace(id='second-lease', detail=True, formatter="table") expected = [ ('devices', 'hosts', 'id', 'networks',), - ('', d, SECOND_LEASE, '',) + ('[]', '[\n {\n "id": "101",\n "hypervisor_hostname": "host-1"\n }\n]', '424d21c3-45a2-448a-81ad-32eddc888375', '[]') ] - self.assertEqual(show_lease.get_data(args), expected) lease_manager.list.assert_called_once_with() lease_manager.get.assert_called_once_with(SECOND_LEASE) diff --git a/blazarclient/tests/v1/shell_commands/test_networks.py b/blazarclient/tests/v1/shell_commands/test_networks.py index d931dba..0ca5ea2 100644 --- a/blazarclient/tests/v1/shell_commands/test_networks.py +++ b/blazarclient/tests/v1/shell_commands/test_networks.py @@ -143,7 +143,7 @@ def test_show_network(self): show_network, network_manager = self.create_show_command(list_value, get_value) - args = argparse.Namespace(id='072c58c0-64ac-467b-b040-9138771e146a') + args = argparse.Namespace(id='072c58c0-64ac-467b-b040-9138771e146a', formatter="table") expected = [('id',), ('072c58c0-64ac-467b-b040-9138771e146a',)] ret = show_network.get_data(args) @@ -232,7 +232,9 @@ def test_list_network_sort_by(self): list_network, network_manager = self.create_list_command() list_networks_args = argparse.Namespace( sort_by='segment_id', - columns=[] + columns=[], + formatter="table", + long=False, ) return_networks = list_network.get_data(list_networks_args) segment_id_index = list(return_networks[0]).index('segment_id') From 51e497915eff58b1371d690df81aa2b46c3a749a Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Mon, 21 Apr 2025 09:56:22 -0500 Subject: [PATCH 13/13] Fix linting --- blazarclient/tests/v1/shell_commands/test_leases.py | 2 -- blazarclient/v1/allocations.py | 2 +- blazarclient/v1/devices.py | 6 +++--- blazarclient/v1/floatingips.py | 2 +- blazarclient/v1/hosts.py | 6 +++--- blazarclient/v1/leases.py | 2 +- blazarclient/v1/networks.py | 6 +++--- 7 files changed, 12 insertions(+), 14 deletions(-) diff --git a/blazarclient/tests/v1/shell_commands/test_leases.py b/blazarclient/tests/v1/shell_commands/test_leases.py index db2892a..aa2a759 100644 --- a/blazarclient/tests/v1/shell_commands/test_leases.py +++ b/blazarclient/tests/v1/shell_commands/test_leases.py @@ -390,8 +390,6 @@ def test_show_lease_by_name_with_allocations(self): 'networks': [], 'devices': [] } - import json - d = json.dumps(host1, indent=4) mock.seal(lease_manager) args = argparse.Namespace(id='second-lease', detail=True, formatter="table") expected = [ diff --git a/blazarclient/v1/allocations.py b/blazarclient/v1/allocations.py index f1c4c1e..8d8a70a 100644 --- a/blazarclient/v1/allocations.py +++ b/blazarclient/v1/allocations.py @@ -30,5 +30,5 @@ def list(self, resource, sort_by=None): resp, body = self.request_manager.get('/%s/allocations' % resource) allocations = body['allocations'] if sort_by: - allocations = sorted(allocations, key=lambda l: l[sort_by]) + allocations = sorted(allocations, key=lambda alloc: alloc[sort_by]) return allocations diff --git a/blazarclient/v1/devices.py b/blazarclient/v1/devices.py index a2f319f..992169c 100644 --- a/blazarclient/v1/devices.py +++ b/blazarclient/v1/devices.py @@ -50,7 +50,7 @@ def list(self, sort_by=None): resp, body = self.request_manager.get('/devices') devices = body['devices'] if sort_by: - devices = sorted(devices, key=lambda l: l[sort_by]) + devices = sorted(devices, key=lambda dev: dev[sort_by]) return devices def get_allocation(self, device_id): @@ -64,7 +64,7 @@ def list_allocations(self, sort_by=None): resp, body = self.request_manager.get('/devices/allocations') allocations = body['allocations'] if sort_by: - allocations = sorted(allocations, key=lambda l: l[sort_by]) + allocations = sorted(allocations, key=lambda alloc: alloc[sort_by]) return allocations def reallocate(self, device_id, values): @@ -90,7 +90,7 @@ def list_properties(self, detail=False, all=False, sort_by=None): if sort_by: resource_properties = sorted(resource_properties, - key=lambda l: l[sort_by]) + key=lambda prop: prop[sort_by]) return resource_properties def get_property(self, property_name): diff --git a/blazarclient/v1/floatingips.py b/blazarclient/v1/floatingips.py index 029f079..2a65329 100644 --- a/blazarclient/v1/floatingips.py +++ b/blazarclient/v1/floatingips.py @@ -43,5 +43,5 @@ def list(self, sort_by=None): resp, body = self.request_manager.get('/floatingips') floatingips = body['floatingips'] if sort_by: - floatingips = sorted(floatingips, key=lambda l: l[sort_by]) + floatingips = sorted(floatingips, key=lambda ip: ip[sort_by]) return floatingips diff --git a/blazarclient/v1/hosts.py b/blazarclient/v1/hosts.py index a3dff70..0a1c102 100644 --- a/blazarclient/v1/hosts.py +++ b/blazarclient/v1/hosts.py @@ -51,7 +51,7 @@ def list(self, sort_by=None): resp, body = self.request_manager.get('/os-hosts') hosts = body['hosts'] if sort_by: - hosts = sorted(hosts, key=lambda l: l[sort_by]) + hosts = sorted(hosts, key=lambda host: host[sort_by]) return hosts def get_allocation(self, host_id): @@ -65,7 +65,7 @@ def list_allocations(self, sort_by=None): resp, body = self.request_manager.get('/os-hosts/allocations') allocations = body['allocations'] if sort_by: - allocations = sorted(allocations, key=lambda l: l[sort_by]) + allocations = sorted(allocations, key=lambda alloc: alloc[sort_by]) return allocations def reallocate(self, host_id, values): @@ -96,7 +96,7 @@ def list_properties(self, detail=False, all=False, sort_by=None): if sort_by: resource_properties = sorted(resource_properties, - key=lambda l: l[sort_by]) + key=lambda lease: lease[sort_by]) return resource_properties def get_property(self, property_name): diff --git a/blazarclient/v1/leases.py b/blazarclient/v1/leases.py index 791713e..b0d802f 100644 --- a/blazarclient/v1/leases.py +++ b/blazarclient/v1/leases.py @@ -93,7 +93,7 @@ def list(self, sort_by=None): resp, body = self.request_manager.get('/leases') leases = body['leases'] if sort_by: - leases = sorted(leases, key=lambda l: l[sort_by]) + leases = sorted(leases, key=lambda lease: lease[sort_by]) return leases def additional_details(self, lease_id): diff --git a/blazarclient/v1/networks.py b/blazarclient/v1/networks.py index 1e2334e..0136034 100644 --- a/blazarclient/v1/networks.py +++ b/blazarclient/v1/networks.py @@ -52,7 +52,7 @@ def list(self, sort_by=None): resp, body = self.request_manager.get('/networks') networks = body['networks'] if sort_by: - networks = sorted(networks, key=lambda l: l[sort_by]) + networks = sorted(networks, key=lambda network: network[sort_by]) return networks def get_allocation(self, network_id): @@ -66,7 +66,7 @@ def list_allocations(self, sort_by=None): resp, body = self.request_manager.get('/networks/allocations') allocations = body['allocations'] if sort_by: - allocations = sorted(allocations, key=lambda l: l[sort_by]) + allocations = sorted(allocations, key=lambda alloc: alloc[sort_by]) return allocations def list_properties(self, detail=False, all=False, sort_by=None): @@ -86,7 +86,7 @@ def list_properties(self, detail=False, all=False, sort_by=None): if sort_by: resource_properties = sorted(resource_properties, - key=lambda l: l[sort_by]) + key=lambda prop: prop[sort_by]) return resource_properties def get_property(self, property_name):