diff --git a/blazarclient/command.py b/blazarclient/command.py index 50b7e78..39d3654 100644 --- a/blazarclient/command.py +++ b/blazarclient/command.py @@ -28,6 +28,8 @@ HEX_ELEM + '{4}', HEX_ELEM + '{4}', HEX_ELEM + '{12}']) +import logging +LOG = logging.getLogger(__name__) class OpenStackCommand(command.Command): """Base class for OpenStack commands.""" @@ -88,6 +90,31 @@ def get_client(self): else: return self.app.client + def get_manager_and_args( + self, parsed_args, args, add_to_body=False, blazar_client=None + ): + if not blazar_client: + blazar_client = self.get_client() + + if hasattr(parsed_args, "resource"): # Passed in via --resource flag + if hasattr(blazar_client, parsed_args.resource): # A built-in type + resource_manager = getattr(blazar_client, parsed_args.resource) + if add_to_body: + args["resource_type"] = parsed_args.resource + else: + args.insert(0, parsed_args.resource) + else: # A built-in resource type + resource_manager = blazar_client.resource + if add_to_body: + args["resource_type"] = self.resource + else: + args.insert(0, parsed_args.resource) + else: # Else, no --resource, normal usage + resource_manager = getattr(blazar_client, self.resource) + # Update resource type depending on calling method + return resource_manager, args + + def get_parser(self, prog_name): parser = super(BlazarCommand, self).get_parser(prog_name) return parser @@ -135,7 +162,7 @@ def get_data(self, parsed_args): self.log.debug('get_data(%s)' % parsed_args) blazar_client = self.get_client() body = self.args2body(parsed_args) - resource_manager = getattr(blazar_client, self.resource) + resource_manager, body = self.get_manager_and_args(parsed_args, body, add_to_body=True, blazar_client=blazar_client) data = resource_manager.create(**body) self.format_output_data(data) @@ -178,8 +205,8 @@ def run(self, parsed_args): self.id_pattern) else: res_id = parsed_args.id - resource_manager = getattr(blazar_client, self.resource) - resource_manager.update(res_id, **body) + resource_manager, args = self.get_manager_and_args(parsed_args, [res_id], add_to_body=False, blazar_client=blazar_client) + resource_manager.update(*args, **body) print('Updated %s: %s' % (self.resource, parsed_args.id), file=self.app.stdout) return @@ -189,7 +216,7 @@ class DeleteCommand(BlazarCommand): """Delete a given resource.""" api = 'reservation' - resource = None + resource = "resource" log = None def get_parser(self, prog_name): @@ -206,7 +233,6 @@ def get_parser(self, prog_name): def run(self, parsed_args): self.log.debug('run(%s)' % parsed_args) blazar_client = self.get_client() - resource_manager = getattr(blazar_client, self.resource) if self.allow_names: res_id = utils.find_resource_id_by_name_or_id(blazar_client, self.resource, @@ -215,7 +241,8 @@ def run(self, parsed_args): self.id_pattern) else: res_id = parsed_args.id - resource_manager.delete(res_id) + resource_manager, args = self.get_manager_and_args(parsed_args, [res_id], add_to_body=False, blazar_client=blazar_client) + resource_manager.delete(*args) print('Deleted %s: %s' % (self.resource, parsed_args.id), file=self.app.stdout) return @@ -231,6 +258,8 @@ class ListCommand(BlazarCommand, lister.Lister): list_columns = [] unknown_parts_flag = True + list_fn_name = "list" + def args2body(self, parsed_args): params = {} if parsed_args.sort_by: @@ -249,8 +278,8 @@ 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(**body) + resource_manager, body = self.get_manager_and_args(parsed_args, body, add_to_body=True, blazar_client=blazar_client) + data = getattr(resource_manager, self.list_fn_name)(**body) return data def setup_columns(self, info, parsed_args): @@ -292,7 +321,7 @@ 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) + resource_manager, body = self.get_manager_and_args(parsed_args, body, add_to_body=True, blazar_client=blazar_client) data = resource_manager.list_allocations(**body) return data @@ -317,18 +346,15 @@ def get_parser(self, prog_name): def get_data(self, parsed_args): self.log.debug('get_data(%s)' % parsed_args) blazar_client = self.get_client() - + res_id = parsed_args.id if self.allow_names: res_id = utils.find_resource_id_by_name_or_id(blazar_client, self.resource, parsed_args.id, self.name_key, self.id_pattern) - else: - res_id = parsed_args.id - - resource_manager = getattr(blazar_client, self.resource) - data = resource_manager.get(res_id) + resource_manager, args = self.get_manager_and_args(parsed_args, [res_id], add_to_body=False, blazar_client=blazar_client) + data = resource_manager.get(*args) self.format_output_data(data) return list(zip(*sorted(data.items()))) @@ -339,8 +365,8 @@ class ShowAllocationCommand(ShowCommand, show.ShowOne): def get_data(self, parsed_args): self.log.debug('get_data(%s)' % parsed_args) blazar_client = self.get_client() - resource_manager = getattr(blazar_client, self.resource) - data = resource_manager.get_allocation(parsed_args.id) + resource_manager, args = self.get_manager_and_args(parsed_args, [parsed_args.id], add_to_body=False, blazar_client=blazar_client) + data = resource_manager.get_allocation(*args) self.format_output_data(data) return list(zip(*sorted(data.items()))) @@ -377,8 +403,8 @@ def run(self, parsed_args): self.id_pattern) else: res_id = parsed_args.id - resource_manager = getattr(blazar_client, self.resource) - resource_manager.reallocate(res_id, body) + resource_manager, args = self.get_manager_and_args(parsed_args, [res_id, body], add_to_body=False, blazar_client=blazar_client) + resource_manager.reallocate(*args) print('Reallocated %s: %s' % (self.resource, parsed_args.id), file=self.app.stdout) return @@ -400,8 +426,9 @@ def get_parser(self, prog_name): def get_data(self, parsed_args): self.log.debug('get_data(%s)' % parsed_args) blazar_client = self.get_client() - resource_manager = getattr(blazar_client, self.resource) - data = resource_manager.get_capability(parsed_args.capability_name) + resource_manager, args = self.get_manager_and_args(parsed_args, [parsed_args.capability_name], add_to_body=False, blazar_client=blazar_client) + LOG.info(args) + data = resource_manager.get_capability(*args) self.format_output_data(data) return list(zip(*sorted(data.items()))) @@ -415,7 +442,7 @@ def run(self, parsed_args): self.log.debug('run(%s)' % parsed_args) blazar_client = self.get_client() body = self.args2body(parsed_args) - resource_manager = getattr(blazar_client, self.resource) + resource_manager, body = self.get_manager_and_args(parsed_args, body, add_to_body=True, blazar_client=blazar_client) resource_manager.set_capability(**body) print( 'Updated %s extra capability: %s' % ( diff --git a/blazarclient/utils.py b/blazarclient/utils.py index 57adb46..1ccadcc 100644 --- a/blazarclient/utils.py +++ b/blazarclient/utils.py @@ -120,8 +120,12 @@ def find_resource_id_by_name_or_id(client, resource_type, name_or_id, def _find_resource_id_by_name(client, resource_type, name, name_key): - resource_manager = getattr(client, resource_type) - resources = resource_manager.list() + if hasattr(client, resource_type): + resource_manager = getattr(client, resource_type) + resources = resource_manager.list() + else: # If third party resource + resource_manager = client.resource + resources = resource_manager.list(resource_type) named_resources = [] key = name_key if name_key else 'name' diff --git a/blazarclient/v1/client.py b/blazarclient/v1/client.py index 0313a19..3ff8d2a 100644 --- a/blazarclient/v1/client.py +++ b/blazarclient/v1/client.py @@ -20,6 +20,7 @@ from blazarclient.v1 import hosts from blazarclient.v1 import leases from blazarclient.v1 import networks +from blazarclient.v1 import resources class Client(object): @@ -76,3 +77,9 @@ def __init__(self, blazar_url=None, auth_token=None, session=None, session=self.session, version=self.version, **kwargs) + self.resource = resources.ResourceClientManager( + blazar_url=self.blazar_url, + auth_token=self.auth_token, + session=self.session, + version=self.version, + **kwargs) diff --git a/blazarclient/v1/hosts.py b/blazarclient/v1/hosts.py index b403249..72ebad1 100644 --- a/blazarclient/v1/hosts.py +++ b/blazarclient/v1/hosts.py @@ -43,7 +43,7 @@ def update(self, host_id, values): def delete(self, host_id): """Delete host with specified ID.""" - resp, body = self.request_manager.delete('/os-hosts/%s' % host_id) + self.request_manager.delete('/os-hosts/%s' % host_id) def list(self, sort_by=None): """List all hosts.""" diff --git a/blazarclient/v1/resources.py b/blazarclient/v1/resources.py new file mode 100644 index 0000000..485c391 --- /dev/null +++ b/blazarclient/v1/resources.py @@ -0,0 +1,109 @@ +# Copyright (c) 2019 StackHPC Ltd. +# +# 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 blazarclient import base +from blazarclient.i18n import _ + +import logging +LOG = logging.getLogger(__name__) + +class ResourceClientManager(base.BaseClientManager): + def list_resources(self, sort_by=None): + resp, body = self.request_manager.get('/resources') + if sort_by: + body = sorted(body, key=lambda l: l[sort_by]) + return body + + def create(self, resource_type, data, **kwargs): + values = {'data': data} + values.update(**kwargs) + resp, body = self.request_manager.post(f'/{resource_type}', body=values) + return body['resource'] + + def get(self, resource_type, resource_id): + resp, body = self.request_manager.get( + f'/{resource_type}/{resource_id}') + return body['resource'] + + def update(self, resource_type, res_id, data, extras): + if not data and not extras: + return _('No information to update passed.') + body = {"data": data, "extras": extras} + resp, body = self.request_manager.put( + f'/{resource_type}/{res_id}', body=body + ) + return body['resource'] + + def delete(self, resource_type, resource_id): + resp, body = self.request_manager.delete( + f'/{resource_type}/{resource_id}') + + def list(self, resource_type, sort_by=None): + resp, body = self.request_manager.get(f'/{resource_type}') + resources = body['resources'] + if sort_by: + resources = sorted(resources, key=lambda l: l[sort_by]) + return resources + + def get_allocation(self, resource_type, resource_id): + resp, body = self.request_manager.get( + f'/{resource_type}/{resource_id}/allocation') + return body['allocation'] + + def list_allocations(self, resource_type, sort_by=None): + resp, body = self.request_manager.get(f'/{resource_type}/allocations') + allocations = body['allocations'] + if sort_by: + allocations = sorted(allocations, key=lambda l: l[sort_by]) + return allocations + + def reallocate(self, resource_type, resource_id, values): + resp, body = self.request_manager.put( + f'/{resource_type}/{resource_id}/allocation', body=values) + return body['allocation'] + + def list_capabilities(self, resource_type, detail=False, sort_by=None): + url = f'/{resource_type}/properties' + + if detail: + url += '?detail=True' + + resp, body = self.request_manager.get(url) + resource_properties = body['resource_properties'] + + # Values is a reserved word in cliff so need to rename values column. + if detail: + for p in resource_properties: + p['capability_values'] = p['values'] + del p['values'] + + if sort_by: + resource_properties = sorted(resource_properties, + key=lambda l: l[sort_by]) + return resource_properties + + def get_capability(self, resource_type, capability_name): + resource_property = [ + x for x in self.list_capabilities(resource_type, detail=True) + if x['property'] == capability_name] + + return {} if not resource_property else resource_property[0] + + def set_capability(self, resource_type, capability_name, private): + data = {'private': private} + resp, body = self.request_manager.patch( + f'/{resource_type}/properties/{capability_name}', body=data) + + return body['resource_property'] diff --git a/blazarclient/v1/shell_commands/leases.py b/blazarclient/v1/shell_commands/leases.py index 02b13b7..b9074ef 100644 --- a/blazarclient/v1/shell_commands/leases.py +++ b/blazarclient/v1/shell_commands/leases.py @@ -67,7 +67,10 @@ "resource_type": 'device' }, "others": { - ".*": None + "min": "", + "max": "", + "resource_properties": {}, + "resource_type": "", } } @@ -280,6 +283,8 @@ def _parse_params(self, str_params, default, err_msg): prog = re.compile('^(?:(.*),)?(%s)=(.*)$' % "|".join(default.keys())) + self.log.info("%s", str_params) + self.log.info("%s", default) while str_params != "": match = prog.search(str_params) diff --git a/blazarclient/v1/shell_commands/resources.py b/blazarclient/v1/shell_commands/resources.py new file mode 100644 index 0000000..076394d --- /dev/null +++ b/blazarclient/v1/shell_commands/resources.py @@ -0,0 +1,352 @@ +# Copyright (c) 2019 StackHPC Ltd. +# +# 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. + +import logging + +from oslo_serialization import jsonutils + +from blazarclient import command +from blazarclient import exception + +RESOURCE_ID_PATTERN = '^[0-9]+$' + + +class ListResourceTypes(command.ListCommand): + resource = "resource" + log = logging.getLogger(__name__ + '.ListResourceTypes') + list_columns = ['name', 'prefix'] + list_fn_name = 'list_resources' + + def get_parser(self, prog_name): + parser = super(ListResourceTypes, self).get_parser(prog_name) + parser.add_argument( + '--sort-by', metavar="", + help='column name used to sort result', + default='name' + ) + return parser + +class ListResources(command.ListCommand): + resource = "resource" + log = logging.getLogger(__name__ + '.ListResources') + list_columns = ['id', 'data'] + + def get_parser(self, prog_name): + parser = super(ListResources, self).get_parser(prog_name) + parser.add_argument( + '--resource', metavar="", + help='resource to list', + ) + parser.add_argument( + '--sort-by', metavar="", + help='column name used to sort result', + default='id' + ) + return parser + + def args2body(self, parsed_args): + params = super(ListResources, self).args2body(parsed_args) + if parsed_args.resource: + self.resource = parsed_args.resource + else: + raise exception.BlazarClientException("Resource not specified") + return params + + +class ShowResource(command.ShowCommand): + resource = "resource" + allow_names = False + json_indent = 4 + log = logging.getLogger(__name__ + '.ShowResource') + + def get_parser(self, prog_name): + parser = super(ShowResource, self).get_parser(prog_name) + parser.add_argument( + '--resource', metavar="", + help='resource type to show', + ) + return parser + + def args2body(self, parsed_args): + params = super(ShowResource, self).args2body(parsed_args) + if parsed_args.resource: + self.resource = parsed_args.resource + else: + raise exception.BlazarClientException("Resource not specified") + return params + + +class CreateResource(command.CreateCommand): + resource = "resource" + json_indent = 4 + log = logging.getLogger(__name__ + '.CreateResource') + + def get_parser(self, prog_name): + parser = super(CreateResource, self).get_parser(prog_name) + parser.add_argument( + '--resource', metavar="", + help='resource type to show', + ) + parser.add_argument( + 'data', metavar='DATA', + help='json data for the resource' + ) + return parser + + def args2body(self, parsed_args): + params = super(CreateResource, self).args2body(parsed_args) + if parsed_args.resource: + self.resource = parsed_args.resource + else: + raise exception.BlazarClientException("Resource not specified") + if parsed_args.data: + params['data'] = jsonutils.loads(parsed_args.data) + return params + + +class UpdateResource(command.UpdateCommand): + resource = 'resource' + json_indent = 4 + log = logging.getLogger(__name__ + '.UpdateResource') + + def get_parser(self, prog_name): + parser = super(UpdateResource, self).get_parser(prog_name) + parser.add_argument( + '--extra', metavar='=', + action='append', + dest='extra_capabilities', + default=[], + help='Extra capabilities key/value pairs to update for the resource' + ) + parser.add_argument( + '--data', metavar='', dest='data', default=None, + help='New data JSON object for resource' + ) + parser.add_argument( + '--resource', metavar="", + help='resource type to show', + ) + return parser + + def args2body(self, parsed_args): + params = {} + extras = {} + if parsed_args.resource: + self.resource = parsed_args.resource + else: + raise exception.BlazarClientException("Resource not specified") + params['data'] = parsed_args.data + if parsed_args.extra_capabilities: + for capa in parsed_args.extra_capabilities: + key, _sep, value = capa.partition('=') + # NOTE(sbauza): multiple copies of the same capability will + # result in only the last value to be stored + extras[key] = value + params['extras'] = extras + return params + + +class DeleteResource(command.DeleteCommand): + resource = "resource" + allow_names = False + log = logging.getLogger(__name__ + '.DeleteResource') + + def get_parser(self, prog_name): + parser = super(DeleteResource, self).get_parser(prog_name) + parser.add_argument( + '--resource', metavar="", + help='resource type to show', + ) + return parser + + def args2body(self, parsed_args): + params = super(DeleteResource, self).args2body(parsed_args) + if parsed_args.resource: + self.resource = parsed_args.resource + else: + raise exception.BlazarClientException("Resource not specified") + return params + + +class ShowResourceAllocation(command.ShowAllocationCommand): + resource = 'resource' + json_indent = 4 + log = logging.getLogger(__name__ + '.ShowResourceAllocation') + + def get_parser(self, prog_name): + parser = super(ShowResourceAllocation, self).get_parser(prog_name) + parser.add_argument( + '--resource', metavar="", + help='resource type to show', + ) + return parser + + def args2body(self, parsed_args): + params = super(ShowResourceAllocation, self).args2body(parsed_args) + if parsed_args.resource: + self.resource = parsed_args.resource + else: + raise exception.BlazarClientException("Resource not specified") + return params + + +class ListResourceAllocations(command.ListAllocationCommand): + resource = None + log = logging.getLogger(__name__ + '.ListResourceAllocations') + list_columns = ['resource_id', 'reservations'] + + def get_parser(self, prog_name): + parser = super(ListResourceAllocations, self).get_parser(prog_name) + parser.add_argument( + '--sort-by', metavar="", + help='column name used to sort result', + default='resource_id' + ) + parser.add_argument( + '--resource', metavar="", + help='resource type to show', + ) + return parser + + def args2body(self, parsed_args): + params = super(ListResourceAllocations, self).args2body(parsed_args) + if parsed_args.resource: + self.resource = parsed_args.resource + else: + raise exception.BlazarClientException("Resource not specified") + return params + + +class ReallocateResource(command.ReallocateCommand): + resource = "resource" + json_indent = 4 + log = logging.getLogger(__name__ + '.ReallocateResource') + id_pattern = RESOURCE_ID_PATTERN + allow_names = False + + def get_parser(self, prog_name): + parser = super(ReallocateResource, self).get_parser(prog_name) + parser.add_argument( + '--lease-id', + help='Lease ID to reallocate resource from.') + parser.add_argument( + '--reservation-id', + help='Reservation ID to reallocate resource from') + parser.add_argument( + '--resource', metavar="", + help='resource type to show', + ) + return parser + + def args2body(self, parsed_args): + params = {} + + if parsed_args.reservation_id: + params['reservation_id'] = parsed_args.reservation_id + elif parsed_args.lease_id: + params['lease_id'] = parsed_args.lease_id + if parsed_args.resource: + self.resource = parsed_args.resource + else: + raise exception.BlazarClientException("Resource not specified") + + return params + + +class ShowResourceCapability(command.ShowCapabilityCommand): + resource = 'resource' + json_indent = 4 + log = logging.getLogger(__name__ + '.ShowResourceCapability') + + def get_parser(self, prog_name): + parser = super(ShowResourceCapability, self).get_parser(prog_name) + parser.add_argument( + '--resource', metavar="", + help='resource type to show', + ) + return parser + + def args2body(self, parsed_args): + params = super(ShowResourceCapability, self).args2body(parsed_args) + if parsed_args.resource: + self.resource = parsed_args.resource + else: + raise exception.BlazarClientException("Resource not specified") + return params + + +class ListResourceCapabilities(command.ListCommand): + resource = 'resource' + log = logging.getLogger(__name__ + '.ListResourceCapabilities') + list_columns = ['property', 'private', 'capability_values'] + list_fn_name = "list_capabilities" + + def args2body(self, parsed_args): + params = {'detail': parsed_args.detail} + if parsed_args.sort_by: + if parsed_args.sort_by in self.list_columns: + params['sort_by'] = parsed_args.sort_by + else: + msg = 'Invalid sort option %s' % parsed_args.sort_by + raise exception.BlazarClientException(msg) + if parsed_args.resource: + self.resource = parsed_args.resource + else: + raise exception.BlazarClientException("Resource not specified") + + return params + + def get_parser(self, prog_name): + parser = super(ListResourceCapabilities, self).get_parser(prog_name) + parser.add_argument( + '--detail', + action='store_true', + help='Return capabilities with values and attributes.', + default=False + ) + parser.add_argument( + '--sort-by', metavar="", + help='column name used to sort result', + default='property' + ) + parser.add_argument( + '--resource', metavar="", + help='resource type to show', + ) + return parser + + +class UpdateResourceCapability(command.UpdateCapabilityCommand): + resource = 'resource' + json_indent = 4 + log = logging.getLogger(__name__ + '.UpdateResourceCapability') + name_key = 'capability_name' + + def get_parser(self, prog_name): + parser = super(UpdateResourceCapability, self).get_parser(prog_name) + parser.add_argument( + '--resource', metavar="", + help='resource type to show', + ) + return parser + + def args2body(self, parsed_args): + params = super(UpdateResourceCapability, self).args2body(parsed_args) + if parsed_args.resource: + self.resource = parsed_args.resource + else: + raise exception.BlazarClientException("Resource not specified") + return params + diff --git a/setup.cfg b/setup.cfg index 430270d..5b0d474 100644 --- a/setup.cfg +++ b/setup.cfg @@ -79,4 +79,15 @@ openstack.reservation.v1 = reservation_network_set = blazarclient.v1.shell_commands.networks:UpdateNetwork reservation_network_unset = blazarclient.v1.shell_commands.networks:UnsetAttributeNetwork reservation_network_show = blazarclient.v1.shell_commands.networks:ShowNetwork - + reservation_resource_create = blazarclient.v1.shell_commands.resources:CreateResource + reservation_resource_delete = blazarclient.v1.shell_commands.resources:DeleteResource + reservation_resource_list = blazarclient.v1.shell_commands.resources:ListResources + reservation_resource_set = blazarclient.v1.shell_commands.resources:UpdateResource + reservation_resource_show = blazarclient.v1.shell_commands.resources:ShowResource + reservation_resource_allocation_list = blazarclient.v1.shell_commands.resources:ListResourceAllocations + reservation_resource_allocation_show = blazarclient.v1.shell_commands.resources:ShowResourceAllocation + reservation_resource_capability_show = blazarclient.v1.shell_commands.resources:ShowResourceCapability + reservation_resource_capability_list = blazarclient.v1.shell_commands.resources:ListResourceCapabilities + reservation_resource_capability_update = blazarclient.v1.shell_commands.resources:UpdateResourceCapability + reservation_resource_reallocate = blazarclient.v1.shell_commands.resources:ReallocateResource + reservation_resources_list=blazarclient.v1.shell_commands.resources:ListResourceTypes