From 790303be76ca02b1b61b68ed303dce94a748aa14 Mon Sep 17 00:00:00 2001 From: Cole Walker Date: Fri, 15 Oct 2021 16:18:31 -0400 Subject: [PATCH] PTP interfaces: CLI, REST API New shell commands for managing PTP interfaces: - "system ptp-interface-add " - "system ptp-interface-delete " - "system ptp-interface-list " - "system ptp-interface-list " - "system ptp-interface-show " This commit also adds the REST API endpoints required to manage the PTP interface table contents. Test Plan: PASS: Added unit tests for PTP interface API Regression: PASS: Existing PTP implementation has not been changed yet and still functions as normal. Story: 2009248 Task: 43706 Signed-off-by: Cole Walker Change-Id: Ib6a5fc75ba19f11cf9d44b83a9847f8e0df9a8d6 --- .../cgts-client/cgtsclient/v1/client.py | 2 + .../cgtsclient/v1/ptp_instance_shell.py | 4 +- .../cgtsclient/v1/ptp_interface.py | 56 ++++ .../cgtsclient/v1/ptp_interface_shell.py | 110 ++++++++ .../cgts-client/cgtsclient/v1/shell.py | 2 + .../sysinv/api/controllers/v1/__init__.py | 13 + .../sysinv/sysinv/api/controllers/v1/host.py | 15 +- .../api/controllers/v1/ptp_interface.py | 207 ++++++++++++++ sysinv/sysinv/sysinv/sysinv/db/api.py | 36 +++ .../sysinv/sysinv/sysinv/db/sqlalchemy/api.py | 31 +- .../sysinv/sysinv/objects/ptp_interface.py | 17 +- .../sysinv/tests/api/test_ptp_interface.py | 267 ++++++++++++++++++ sysinv/sysinv/sysinv/sysinv/tests/db/utils.py | 39 +++ 13 files changed, 786 insertions(+), 13 deletions(-) create mode 100644 sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_interface.py create mode 100644 sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_interface_shell.py create mode 100644 sysinv/sysinv/sysinv/sysinv/api/controllers/v1/ptp_interface.py create mode 100644 sysinv/sysinv/sysinv/sysinv/tests/api/test_ptp_interface.py diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py index 094a33c65a..ef9d0bb2b3 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py @@ -70,6 +70,7 @@ from cgtsclient.v1 import pci_device from cgtsclient.v1 import port from cgtsclient.v1 import ptp from cgtsclient.v1 import ptp_instance +from cgtsclient.v1 import ptp_interface from cgtsclient.v1 import registry_image from cgtsclient.v1 import remotelogging from cgtsclient.v1 import restore @@ -120,6 +121,7 @@ class Client(http.HTTPClient): self.intp = intp.intpManager(self) self.ptp = ptp.ptpManager(self) self.ptp_instance = ptp_instance.PtpInstanceManager(self) + self.ptp_interface = ptp_interface.PtpInterfaceManager(self) self.iextoam = iextoam.iextoamManager(self) self.controller_fs = controller_fs.ControllerFsManager(self) self.storage_backend = storage_backend.StorageBackendManager(self) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_instance_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_instance_shell.py index 4fe0221784..deac5a2afb 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_instance_shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_instance_shell.py @@ -29,8 +29,8 @@ def do_ptp_instance_list(cc, args): ihost = ihost_utils._find_ihost(cc, instance.host_uuid) setattr(instance, 'hostname', ihost.hostname) - field_labels = ['name', 'service', 'hostname'] - fields = ['name', 'service', 'hostname'] + field_labels = ['uuid', 'name', 'service', 'hostname'] + fields = ['uuid', 'name', 'service', 'hostname'] utils.print_list(ptp_instances, fields, field_labels) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_interface.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_interface.py new file mode 100644 index 0000000000..92585057c3 --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_interface.py @@ -0,0 +1,56 @@ +######################################################################## +# +# Copyright (c) 2021 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +######################################################################## + +from cgtsclient.common import base +from cgtsclient import exc +from cgtsclient.v1 import options + +CREATION_ATTRIBUTES = ['interface_uuid', 'ptp_instance_uuid'] + + +class PtpInterface(base.Resource): + def __repr__(self): + return "" % self._info + + +class PtpInterfaceManager(base.Manager): + resource_class = PtpInterface + + @staticmethod + def _path(ptp_interface_id=None): + return 'v1/ptp_interfaces/%s' % ptp_interface_id if ptp_interface_id \ + else 'v1/ptp_interfaces' + + def list(self, q=None): + return self._list(options.build_url(self._path(), q), "ptp_interfaces") + + def list_by_host(self, host_id): + path = 'v1/ihosts/%s/ptp_interfaces' % host_id + return self._list(path, "ptp_interfaces") + + def list_by_interface(self, host_id, interface_id): + path = 'v1/ihosts/%s/ptp_interfaces?interface_uuid=%s' % (host_id, interface_id) + return self._list(path, "ptp_interfaces") + + def get(self, ptp_interface_id): + try: + return self._list(self._path(ptp_interface_id))[0] + except IndexError: + return None + + def create(self, **kwargs): + body = {} + for (key, value) in kwargs.items(): + if key in CREATION_ATTRIBUTES: + body[key] = value + else: + raise exc.InvalidAttribute('Invalid attribute: %s' % key) + return self._create(self._path(), body) + + def delete(self, ptp_interface_id): + return self._delete(self._path(ptp_interface_id)) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_interface_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_interface_shell.py new file mode 100644 index 0000000000..9bf6df367f --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_interface_shell.py @@ -0,0 +1,110 @@ +######################################################################## +# +# Copyright (c) 2021 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +######################################################################## + +from cgtsclient.common import utils +from cgtsclient import exc +from cgtsclient.v1 import ihost as ihost_utils +from cgtsclient.v1 import iinterface as iinterface_utils +from cgtsclient.v1 import ptp_instance as ptp_instance_utils + + +def _print_ptp_interface_show(ptp_interface_obj): + fields = ['uuid', 'ifname', 'ptp_instance_name', + 'hostname', 'created_at'] + data = [(f, getattr(ptp_interface_obj, f, '')) for f in fields] + utils.print_tuple_list(data) + + +@utils.arg('ptp_interface_uuid', + metavar='', + help="UUID of a PTP interface") +def do_ptp_interface_show(cc, args): + """Show PTP interface attributes.""" + ptp_interface = cc.ptp_interface.get(args.ptp_interface_uuid) + host_id = str(getattr(ptp_interface, 'forihostid', '')) + ihost = ihost_utils._find_ihost(cc, host_id) + setattr(ptp_interface, 'hostname', ihost.hostname) + _print_ptp_interface_show(ptp_interface) + + +@utils.arg('hostnameorid', + metavar='', + help="Hostname or ID of a host") +@utils.arg('ifnameorid', + metavar='', + nargs='?', + help="Interface name [OPTIONAL]") +def do_ptp_interface_list(cc, args): + """List PTP interfaces on the specified host, + or a subset of PTP interfaces associated + with a given underlying interface. + """ + ihost = ihost_utils._find_ihost(cc, args.hostnameorid) + if args.ifnameorid: + validate_interface = iinterface_utils._find_interface(cc, ihost, args.ifnameorid) + ptp_interfaces = cc.ptp_interface.list_by_interface(ihost.uuid, validate_interface.uuid) + else: + ptp_interfaces = cc.ptp_interface.list_by_host(ihost.uuid) + + # Add a hostname column using the forihostid field + for i in ptp_interfaces[:]: + host_id = str(getattr(i, 'forihostid', '')) + ihost = ihost_utils._find_ihost(cc, host_id) + setattr(i, 'hostname', ihost.hostname) + field_labels = ['uuid', 'hostname', 'ifname', 'ptp_instance_name'] + fields = ['uuid', 'hostname', 'ifname', 'ptp_instance_name'] + utils.print_list(ptp_interfaces, fields, field_labels) + + +@utils.arg('ptp_interface_uuid', + metavar='', + help="UUID of a PTP instance") +def do_ptp_interface_delete(cc, args): + """Delete a PTP interface""" + cc.ptp_interface.delete(args.ptp_interface_uuid) + print('Deleted PTP interface: %s' % (args.ptp_interface_uuid)) + + +@utils.arg('hostnameorid', + metavar='', + help="The hostname or id associated with the interface and ptp instance [REQUIRED]") +@utils.arg('ifnameorid', + metavar='', + help="Name or UUID of an interface [REQUIRED]") +@utils.arg('ptpinstancenameorid', + metavar='', + help="Name or UUID of a PTP instance [REQUIRED]") +def do_ptp_interface_add(cc, args): + """Add a PTP interface.""" + field_list = ['interface_uuid', 'ptp_instance_uuid'] + + validate_ihost = ihost_utils._find_ihost(cc, args.hostnameorid) + validate_ptp_instance = ptp_instance_utils._find_ptp_instance(cc, args.ptpinstancenameorid) + validate_interface = iinterface_utils._find_interface(cc, validate_ihost, args.ifnameorid) + + if validate_ihost.uuid != validate_ptp_instance.host_uuid: + raise exc.CommandError('PTP instance %s is not on host %s.' + % (validate_ptp_instance.uuid, validate_ihost.hostname)) + + # Prune input fields down to required/expected values + data = dict((k, v) for (k, v) in vars(args).items() + if k in field_list and not (v is None)) + + data["interface_uuid"] = validate_interface.uuid + data["ptp_instance_uuid"] = validate_ptp_instance.uuid + + ptp_interface = cc.ptp_interface.create(**data) + uuid = getattr(ptp_interface, 'uuid', '') + try: + ptp_interface = cc.ptp_interface.get(uuid) + except exc.HTTPNotFound: + raise exc.CommandError('Created PTP interface UUID not found: %s' + % uuid) + + setattr(ptp_interface, 'hostname', validate_ihost.hostname) + _print_ptp_interface_show(ptp_interface) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py index a787ba8295..c37d9ae37d 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py @@ -55,6 +55,7 @@ from cgtsclient.v1 import partition_shell from cgtsclient.v1 import pci_device_shell from cgtsclient.v1 import port_shell from cgtsclient.v1 import ptp_instance_shell +from cgtsclient.v1 import ptp_interface_shell from cgtsclient.v1 import ptp_shell from cgtsclient.v1 import registry_image_shell from cgtsclient.v1 import remotelogging_shell @@ -77,6 +78,7 @@ COMMAND_MODULES = [ intp_shell, ptp_shell, ptp_instance_shell, + ptp_interface_shell, iextoam_shell, controller_fs_shell, storage_backend_shell, diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py index 55bc53e4d7..e8934d3ca4 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py @@ -66,6 +66,7 @@ from sysinv.api.controllers.v1 import pci_device from sysinv.api.controllers.v1 import port from sysinv.api.controllers.v1 import ptp from sysinv.api.controllers.v1 import ptp_instance +from sysinv.api.controllers.v1 import ptp_interface from sysinv.api.controllers.v1 import pv from sysinv.api.controllers.v1 import registry_image from sysinv.api.controllers.v1 import remotelogging @@ -150,6 +151,9 @@ class V1(base.APIBase): ptp_instances = [link.Link] "Links to the ptp_instances resource" + ptp_interfaces = [link.Link] + "Links to the ptp_interfaces resource" + iextoam = [link.Link] "Links to the iextoam resource" @@ -457,6 +461,14 @@ class V1(base.APIBase): 'ptp_instances', '', bookmark=True)] + v1.ptp_interfaces = [link.Link.make_link('self', pecan.request.host_url, + 'ptp_interfaces', ''), + link.Link.make_link('bookmark', + pecan.request.host_url, + 'ptp_interfaces', '', + bookmark=True) + ] + v1.iextoam = [link.Link.make_link('self', pecan.request.host_url, 'iextoam', ''), link.Link.make_link('bookmark', @@ -900,6 +912,7 @@ class Controller(rest.RestController): intp = ntp.NTPController() ptp = ptp.PTPController() ptp_instances = ptp_instance.PtpInstanceController() + ptp_interfaces = ptp_interface.PtpInterfaceController() iextoam = network_oam.OAMNetworkController() controller_fs = controller_fs.ControllerFsController() storage_backend = storage_backend.StorageBackendController() diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py index fd1b8b33e4..09e89cd0a2 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py @@ -87,7 +87,7 @@ from sysinv.api.controllers.v1 import interface_datanetwork from sysinv.api.controllers.v1 import vim_api from sysinv.api.controllers.v1 import patch_api from sysinv.api.controllers.v1 import ptp_instance - +from sysinv.api.controllers.v1 import ptp_interface from sysinv.common import ceph from sysinv.common import constants from sysinv.common import device @@ -1131,6 +1131,9 @@ class HostController(rest.RestController): ptp_instances = ptp_instance.PtpInstanceController(from_ihosts=True) "Expose PTP instance as a sub-element of ihosts" + ptp_interfaces = ptp_interface.PtpInterfaceController(from_ihosts=True) + "Expose PTP interface as a sub-element of ihosts" + _custom_actions = { 'detail': ['GET'], 'bulk_add': ['POST'], @@ -6470,12 +6473,12 @@ class HostController(rest.RestController): if ihost['clock_synchronization'] == constants.PTP: # Ensure we have at least one PTP interface host_interfaces = pecan.request.dbapi.iinterface_get_by_ihost(host_uuid) - ptp_interfaces = [] + ptp_ifaces = [] for interface in host_interfaces: if interface.ptp_role != constants.INTERFACE_PTP_ROLE_NONE: - ptp_interfaces.append(interface) + ptp_ifaces.append(interface) - if not ptp_interfaces: + if not ptp_ifaces: raise wsme.exc.ClientSideError( _("Hosts with PTP clock synchronization must have at least one PTP interface configured")) @@ -6486,8 +6489,8 @@ class HostController(rest.RestController): address_interfaces = set() for address in addresses: address_interfaces.add(address.ifname) - for ptp_interface in ptp_interfaces: - if ptp_interface.ifname not in address_interfaces: + for ptp_if in ptp_ifaces: + if ptp_if.ifname not in address_interfaces: raise wsme.exc.ClientSideError( _("All PTP interfaces must have an associated address when PTP transport is UDP")) diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/ptp_interface.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/ptp_interface.py new file mode 100644 index 0000000000..f9977fe3f3 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/ptp_interface.py @@ -0,0 +1,207 @@ +######################################################################## +# +# Copyright (c) 2021 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +######################################################################## + +import pecan +from pecan import rest +from wsme import types as wtypes +import wsmeext.pecan as wsme_pecan + +from oslo_log import log +from sysinv.api.controllers.v1 import base +from sysinv.api.controllers.v1 import collection +from sysinv.api.controllers.v1 import link +from sysinv.api.controllers.v1 import types +from sysinv.api.controllers.v1 import utils +from sysinv.common import exception +from sysinv import objects + +LOG = log.getLogger(__name__) + + +class PtpInterfacePatchType(types.JsonPatchType): + + @staticmethod + def mandatory_attrs(): + return [] + + +class PtpInterface(base.APIBase): + """API representation of a PTP interface. + + This class enforces type checking and value constraints, and converts + between the interna object model and the API representation of a PTP + interface. + """ + + uuid = types.uuid + "Unique UUID for this PTP interface" + + interface_uuid = types.uuid + "ID for the interface associated with the PTP interface" + + ptp_instance_id = int + "ID for the PTP instance this interface is associated with" + + links = [link.Link] + "A list containing a self link and associated ptp interface links" + + ptp_instance_uuid = types.uuid + "The UUID of the host this PTP interface belongs to" + + ifname = wtypes.text + "The name of the underlying interface" + + forihostid = int + "The foreign key host id" + + ptp_instance_name = wtypes.text + "The name of the associated PTP instance" + + created_at = wtypes.datetime.datetime + + def __init__(self, **kwargs): + self.fields = list(objects.ptp_interface.fields.keys()) + for k in self.fields: + if not hasattr(self, k): + continue + setattr(self, k, kwargs.get(k, wtypes.Unset)) + + @classmethod + def convert_with_links(cls, rpc_ptp_interface, expand=True): + + ptp_interface = PtpInterface(**rpc_ptp_interface.as_dict()) + if not expand: + ptp_interface.unset_fields_except(['uuid', + 'ptp_instance_id', + 'forihostid', + 'ptp_instance_name', + 'ifname', + 'interface_uuid', + 'created_at']) + + return ptp_interface + + +class PtpInterfaceCollection(collection.Collection): + """API representation of a collection of PTP interfaces.""" + + ptp_interfaces = [PtpInterface] + "A list containing PtpInterface objects" + + def __init__(self, **kwargs): + self._type = 'ptp_interfaces' + + @classmethod + def convert_with_links(cls, rpc_ptp_interfaces, limit, url=None, + expand=False, **kwargs): + collection = PtpInterfaceCollection() + collection.ptp_interfaces = [PtpInterface.convert_with_links(p, expand) + for p in rpc_ptp_interfaces] + collection.next = collection.get_next(limit, url=url, **kwargs) + + return collection + + +LOCK_NAME = 'PtpInterfaceController' + + +class PtpInterfaceController(rest.RestController): + """REST controller for ptp interfaces.""" + + def __init__(self, from_ihosts=False): + self._from_ihosts = from_ihosts + + def _get_ptp_interfaces_collection(self, host_uuid=None, marker=None, + limit=None, sort_key=None, + sort_dir=None, expand=False, + resource_url=None, interface_uuid=None): + + limit = utils.validate_limit(limit) + sort_dir = utils.validate_sort_dir(sort_dir) + marker_obj = None + + if marker: + marker_obj = objects.ptp_interface.get_by_uuid(pecan.request.context, + marker) + if self._from_ihosts or host_uuid is not None: + if interface_uuid is not None: + ptp_interfaces = pecan.request.dbapi.ptp_interfaces_get_by_interface( + interface_uuid, limit, + marker_obj, + sort_key, + sort_dir) + else: + ptp_interfaces = pecan.request.dbapi.ptp_interfaces_get_by_host( + host_uuid, limit, + marker_obj, + sort_key, + sort_dir) + else: + ptp_interfaces = pecan.request.dbapi.ptp_interfaces_get_list() + return PtpInterfaceCollection.convert_with_links(ptp_interfaces, + limit, + url=resource_url, + expand=expand, + sort_key=sort_key, + sort_dir=sort_dir) + + @wsme_pecan.wsexpose(PtpInterfaceCollection, types.uuid, types.uuid, int, + wtypes.text, wtypes.text, types.uuid) + def get_all(self, host_uuid, marker=None, limit=None, + sort_key='id', sort_dir='asc', interface_uuid=None): + """Retrieve a list of PTP interfaces.""" + return self._get_ptp_interfaces_collection(host_uuid, marker, limit, + sort_key=sort_key, + sort_dir=sort_dir, + expand=False, + interface_uuid=interface_uuid) + + @wsme_pecan.wsexpose(PtpInterface, types.uuid) + def get_one(self, ptp_interface_uuid): + """Retrieve information about the given PTP interface""" + rpc_ptp_interface = objects.ptp_interface.get_by_uuid(pecan.request.context, + ptp_interface_uuid) + return PtpInterface.convert_with_links(rpc_ptp_interface) + + @wsme_pecan.wsexpose(PtpInterface, body=PtpInterface) + def post(self, ptp_interface): + """Create a new PTP interface""" + return self._create_ptp_interface(ptp_interface) + + def _create_ptp_interface(self, ptp_interface): + # Create a new PTP interface + ptp_interface_dict = ptp_interface.as_dict() + + instance_uuid = ptp_interface_dict.pop('ptp_instance_uuid', None) + instance = objects.ptp_instance.get_by_uuid(pecan.request.context, + instance_uuid) + + interface_uuid = ptp_interface_dict.pop('interface_uuid', None) + interface = pecan.request.dbapi.iinterface_get(interface_uuid) + + ptp_interface_dict['interface_id'] = interface['id'] + ptp_interface_dict['ptp_instance_id'] = instance['id'] + + check = pecan.request.dbapi.ptp_interfaces_get_by_instance_and_interface( + ptp_interface_dict["ptp_instance_id"], + ptp_interface_dict["interface_id"]) + if len(check) != 0: + raise exception.PtpInterfaceAlreadyExists() + + result = pecan.request.dbapi.ptp_interface_create(ptp_interface_dict) + return PtpInterface.convert_with_links(result) + + @wsme_pecan.wsexpose(None, types.uuid, status_code=204) + def delete(self, ptp_interface_uuid): + """Delete a PTP interface.""" + try: + ptp_interface = objects.ptp_interface.get_by_uuid(pecan.request.context, + ptp_interface_uuid) + except exception.PtpInterfaceNotFound: + raise + pecan.request.dbapi.ptp_interface_destroy(ptp_interface.uuid) diff --git a/sysinv/sysinv/sysinv/sysinv/db/api.py b/sysinv/sysinv/sysinv/sysinv/db/api.py index 4012cd4c83..41be4745c4 100644 --- a/sysinv/sysinv/sysinv/sysinv/db/api.py +++ b/sysinv/sysinv/sysinv/sysinv/db/api.py @@ -2034,6 +2034,23 @@ class Connection(object): :returns: A list of PTP interface associations. """ + @abc.abstractmethod + def ptp_interfaces_get_by_host(self, host_uuid, limit=None, + marker=None, sort_key=None, + sort_dir=None): + + """Returns a list of the PTP associations for a given host. + + :param host_uuid: The id or uuid of a host. + :param limit: Maximum number of PTP associations to return. + :param marker: The last item of the previous page; we return the next + result set. + :param sort_key: Attribute by which results should be sorted + :param sort_dir: direction in which results should be sorted + (asc, desc) + :returns: A list of PTP associations (instances) for the host + """ + @abc.abstractmethod def ptp_interfaces_get_by_interface(self, interface_id, limit=None, marker=None, sort_key=None, @@ -2066,6 +2083,25 @@ class Connection(object): :returns: A list of PTP associations (interfaces) for the PTP instance. """ + @abc.abstractmethod + def ptp_interfaces_get_by_instance_and_interface(self, ptp_instance_id, + interface_id, + limit=None, + marker=None, + sort_key=None, + sort_dir=None): + """Returns a list of one PTP interface for a given instance and interface. + + :param ptp_instance_id: The id of a PTP instance. + :param interface_id: The UUID of the underlying interface. + :param marker: The last item of the previous page; we return the next + result set. + :param sort_key: Attribute by which results should be sorted + :param sort_dir: direction in which results should be sorted + (asc, desc) + :returns: A list of PTP interfaces with the given instance and interface. + """ + @abc.abstractmethod def ptp_interface_destroy(self, ptp_interface_id): """Destroys a PTP interface association. diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py index d72362f7fe..fb8d2a00b1 100644 --- a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py @@ -3815,7 +3815,6 @@ class Connection(api.Connection): def _ptp_interface_get(self, ptp_interface_id): query = model_query(models.PtpInterfaces) query = add_identity_filter(query, ptp_interface_id) - try: return query.one() except NoResultFound: @@ -3825,7 +3824,8 @@ class Connection(api.Connection): def ptp_interface_create(self, values): if not values.get('uuid'): values['uuid'] = uuidutils.generate_uuid() - ptp_interface = models.PtpInterfaces(**values) + ptp_interface = models.PtpInterfaces() + ptp_interface.update(values) with _session_for_write() as session: try: session.add(ptp_interface) @@ -3853,6 +3853,18 @@ class Connection(api.Connection): return _paginate_query(models.PtpInterfaces, limit, marker, sort_key, sort_dir, query) + @objects.objectify(objects.ptp_interface) + def ptp_interfaces_get_by_host(self, host_uuid, limit=None, + marker=None, sort_key=None, + sort_dir=None): + ihost_obj = self.ihost_get(host_uuid) + query = model_query(models.PtpInterfaces) + query = (query.join(models.Interfaces). + join(models.ihost, + models.ihost.id == models.Interfaces.forihostid)) + query, field = add_filter_by_many_identities(query, models.ihost, [ihost_obj.uuid]) + return _paginate_query(models.PtpInterfaces, limit, marker, sort_key, sort_dir, query) + @objects.objectify(objects.ptp_interface) def ptp_interfaces_get_by_interface(self, interface_id, limit=None, marker=None, sort_key=None, @@ -3865,6 +3877,21 @@ class Connection(api.Connection): return _paginate_query(models.PtpInterfaces, limit, marker, sort_key, sort_dir, query) + @objects.objectify(objects.ptp_interface) + def ptp_interfaces_get_by_instance_and_interface(self, ptp_instance_id, + interface_id, + limit=None, + marker=None, + sort_key=None, + sort_dir=None): + ptp_instance_obj = self.ptp_instance_get(ptp_instance_id) + ptp_interface_obj = self.iinterface_get(interface_id) + query = model_query(models.PtpInterfaces) + query = query.filter_by(interface_id=ptp_interface_obj.id) + query = query.filter_by(ptp_instance_id=ptp_instance_obj.id) + return _paginate_query(models.PtpInterfaces, limit, marker, + sort_key, sort_dir, query) + @objects.objectify(objects.ptp_interface) def ptp_interfaces_get_by_instance(self, ptp_instance_id, limit=None, marker=None, sort_key=None, diff --git a/sysinv/sysinv/sysinv/sysinv/objects/ptp_interface.py b/sysinv/sysinv/sysinv/sysinv/objects/ptp_interface.py index a8663718ea..5dca22c66e 100644 --- a/sysinv/sysinv/sysinv/sysinv/objects/ptp_interface.py +++ b/sysinv/sysinv/sysinv/sysinv/objects/ptp_interface.py @@ -23,14 +23,25 @@ class PtpInterface(base.SysinvObject): 'interface_id': utils.int_or_none, 'ptp_instance_uuid': utils.str_or_none, - 'ptp_instance_id': utils.int_or_none + 'ptp_instance_id': utils.int_or_none, + 'ptp_instance_name': utils.str_or_none, + + 'ifname': utils.str_or_none, + 'forihostid': utils.int_or_none, + } _foreign_fields = { 'interface_uuid': 'interface:uuid', - 'ptp_instance_uuid': 'ptp_instance:uuid' + 'interface_id': 'interface:id', + 'ptp_instance_uuid': 'ptp_instance:uuid', + 'ptp_instance_name': 'ptp_instance:name', + 'ifname': 'interface:ifname', + 'forihostid': 'interface:forihostid', + 'ptp_instance_host': 'ptp_instance:host_id' + } @base.remotable_classmethod def get_by_uuid(cls, context, uuid): - return cls.dbapi.ptp_get_interface(uuid) + return cls.dbapi.ptp_interface_get(uuid) diff --git a/sysinv/sysinv/sysinv/sysinv/tests/api/test_ptp_interface.py b/sysinv/sysinv/sysinv/sysinv/tests/api/test_ptp_interface.py new file mode 100644 index 0000000000..b454c6cc37 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/tests/api/test_ptp_interface.py @@ -0,0 +1,267 @@ +######################################################################## +# +# Copyright (c) 2021 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +######################################################################## + +from oslo_utils import uuidutils +from six.moves import http_client +from sysinv.common import constants +from sysinv.tests.api import base +from sysinv.tests.db import base as dbbase +from sysinv.tests.db import utils as dbutils + + +class BasePtpInterfaceTestCase(base.FunctionalTest, dbbase.BaseHostTestCase): + # Generic header passed in most API calls + API_HEADERS = {'User-Agent': 'sysinv-test'} + + # Prefix for the URL + API_PREFIX = '/ptp_interfaces' + + # Python table key for the list of results + RESULT_KEY = 'ptp_interfaces' + + # Field that is known to exist for inputs and outputs + COMMON_FIELD = 'interface_uuid' + + # Can perform API operations on thie object at a sublevel of host + HOST_PREFIX = '/ihosts' + + # Attributes that should be populated by an API query + expected_api_fields = ['uuid', 'interface_id', 'ptp_instance_id'] + + # Attributes that should NOT be populated by an API query + hidden_api_fields = ['host_id'] + + def setUp(self): + super(BasePtpInterfaceTestCase, self).setUp() + self.controller = self._create_test_host(constants.CONTROLLER) + self.worker = self._create_test_host(constants.WORKER) + + def get_single_url(self, ptp_interface_uuid): + return '%s/%s' % (self.API_PREFIX, ptp_interface_uuid) + + def get_host_scoped_url(self, host_uuid): + return '%s/%s%s' % (self.HOST_PREFIX, host_uuid, self.API_PREFIX) + + def get_host_scoped_url_interface(self, host_uuid, interface_uuid): + return '%s/%s%s?interface_uuid=%s' % (self.HOST_PREFIX, + host_uuid, + self.API_PREFIX, + interface_uuid) + + def get_post_object(self, interface_uuid=None, ptp_instance_uuid=None): + ptp_interface_db = { + 'interface_uuid': interface_uuid, + 'ptp_instance_uuid': ptp_instance_uuid + } + return ptp_interface_db + + def assert_fields(self, api_object): + assert(uuidutils.is_uuid_like(api_object['uuid'])) + for field in self.expected_api_fields: + self.assertIn(field, api_object) + for field in self.hidden_api_fields: + self.assertNotIn(field, api_object) + + +class TestCreatePtpInterface(BasePtpInterfaceTestCase): + + def setUp(self): + super(TestCreatePtpInterface, self).setUp() + + self.test_interface = dbutils.create_test_interface( + ifname='ptp0', + ifclass=constants.INTERFACE_CLASS_PLATFORM, + forihostid=self.controller.id, + ihost_uuid=self.controller.uuid) + + self.test_instance = dbutils.create_test_ptp_instance( + name='testInstance', + service='ptp4l', + host_id=self.controller.id) + + def _create_ptp_interface_success(self, interface_uuid, ptp_instance_uuid): + ptp_interface_db = self.get_post_object(interface_uuid, + ptp_instance_uuid) + response = self.post_json(self.API_PREFIX, ptp_interface_db, + headers=self.API_HEADERS) + self.assertEqual('application/json', response.content_type) + self.assertEqual(response.status_code, http_client.OK) + self.assertEqual(response.json[self.COMMON_FIELD], + ptp_interface_db[self.COMMON_FIELD]) + + def _create_ptp_interface_failed(self, interface_uuid, ptp_instance_uuid, + status_code, error_message): + ptp_interface_db = self.get_post_object(interface_uuid, + ptp_instance_uuid) + response = self.post_json(self.API_PREFIX, ptp_interface_db, + headers=self.API_HEADERS, + expect_errors=True) + self.assertEqual('application/json', response.content_type) + self.assertEqual(response.status_code, status_code) + self.assertIn(error_message, response.json['error_message']) + + def test_create_ptp_interface_ok(self): + self._create_ptp_interface_success(self.test_interface.uuid, + self.test_instance.uuid) + + def test_create_ptp_interface_invalid_interface(self): + self._create_ptp_interface_failed( + '32dbb999-6c10-448d-aeca-964c50af6384', + self.test_instance.uuid, + status_code=http_client.BAD_REQUEST, + error_message='No entry found for interface 32dbb999-6c10-448d-aeca-964c50af6384') + + def test_create_ptp_interface_invalid_instance(self): + self._create_ptp_interface_failed( + self.test_interface.uuid, + '32dbb999-6c10-448d-aeca-964c50af6384', + status_code=http_client.NOT_FOUND, + error_message='No PTP instance with id 32dbb999-6c10-448d-aeca-964c50af6384 found.') + + def test_create_ptp_interface_duplicate(self): + self._create_ptp_interface_success(self.test_interface.uuid, + self.test_instance.uuid) + + self._create_ptp_interface_failed( + interface_uuid=self.test_interface.uuid, + ptp_instance_uuid=self.test_instance.uuid, + status_code=http_client.INTERNAL_SERVER_ERROR, + error_message='') + + +class TestGetPtpInterface(BasePtpInterfaceTestCase): + def setUp(self): + super(TestGetPtpInterface, self).setUp() + self.test_interface = dbutils.create_test_interface( + ifname='ptp0', + ifclass=constants.INTERFACE_CLASS_PLATFORM, + forihostid=self.controller.id, + ihost_uuid=self.controller.uuid) + + self.test_instance = dbutils.create_test_ptp_instance( + name='testInstance', + service='ptp4l', + host_id=self.controller.id) + + self.test_ptp_interface = dbutils.create_test_ptp_interface( + interface_id=self.test_interface.id, + ptp_instance_id=self.test_instance.id) + + def test_get_ptp_interface_found(self): + + response = self.get_json(self.get_single_url(self.test_ptp_interface.uuid)) + self.assertIn(self.COMMON_FIELD, response) + + def test_get_ptp_interface_not_found(self): + fake_uuid = 'f4c56ddf-aef3-46ed-b9aa-126a1faafd40' + error_message = 'No PTP interface with id %s found' % fake_uuid + + response = self.get_json(self.get_single_url(fake_uuid), + expect_errors=True) + self.assertEqual('application/json', response.content_type) + self.assertEqual(response.status_code, http_client.NOT_FOUND) + self.assertIn(error_message, response.json['error_message']) + + +class TestListPtpInterface(BasePtpInterfaceTestCase): + def setUp(self): + super(TestListPtpInterface, self).setUp() + self.test_interface = dbutils.create_test_interface( + ifname='ptp0', + ifclass=constants.INTERFACE_CLASS_PLATFORM, + forihostid=self.worker.id, + ihost_uuid=self.worker.uuid) + + self.dummy_interface = dbutils.create_test_interface( + ifname='ptp1', + ifclass=constants.INTERFACE_CLASS_PLATFORM, + forihostid=self.worker.id, + ihost_uuid=self.worker.uuid) + + self.test_instance_ptp4l = dbutils.create_test_ptp_instance( + name='ptp4lInstance', + service='ptp4l', + host_id=self.worker.id) + + self.test_instance_phc2sys = dbutils.create_test_ptp_instance( + name='phc2sysInstance', + service='phc2sys', + host_id=self.worker.id) + + self.ptp4l_ptp_interface = dbutils.create_test_ptp_interface( + interface_id=self.test_interface.id, + ptp_instance_id=self.test_instance_ptp4l.id) + self.phc2sys_ptp_interface = dbutils.create_test_ptp_interface( + interface_id=self.test_interface.id, + ptp_instance_id=self.test_instance_phc2sys.id) + self.dummy_ptp_interface = dbutils.create_test_ptp_interface( + interface_id=self.dummy_interface.id, + ptp_instance_id=self.test_instance_ptp4l.id) + + def test_list_ptp_interface_host(self): + response = self.get_json(self.get_host_scoped_url(self.worker.uuid)) + for result in response[self.RESULT_KEY]: + self.assertEqual(self.worker.id, result['forihostid']) + if result['uuid'] == self.ptp4l_ptp_interface.uuid \ + or result['uuid'] == self.dummy_interface.uuid: + self.assertEqual(self.test_instance_ptp4l.id, result['ptp_instance_id']) + elif result['uuid'] == self.phc2sys_ptp_interface.uuid: + self.assertEqual(self.test_instance_phc2sys.id, result['ptp_instance_id']) + + def test_list_ptp_interface_interface(self): + response = self.get_json(self.get_host_scoped_url_interface(self.worker.uuid, + self.test_interface.uuid)) + for result in response[self.RESULT_KEY]: + self.assertIn(self.COMMON_FIELD, result) + self.assertNotIn(self.dummy_interface.uuid, result) + + def test_list_ptp_interface_empty(self): + response = self.get_json(self.get_host_scoped_url(self.controller.uuid)) + self.assertEqual([], response[self.RESULT_KEY]) + + +class TestDeletePtpInterface(BasePtpInterfaceTestCase): + """ Tests deletion. + Typically delete APIs return NO CONTENT. + python2 and python3 libraries may return different + content_type (None, or empty json) when NO_CONTENT returned. + """ + + def setUp(self): + super(TestDeletePtpInterface, self).setUp() + + self.test_interface = dbutils.create_test_interface( + ifname='ptp0', + ifclass=constants.INTERFACE_CLASS_PLATFORM, + forihostid=self.worker.id, + ihost_uuid=self.worker.uuid) + + self.test_instance_ptp4l = dbutils.create_test_ptp_instance( + name='ptp4lInstance', + service='ptp4l', + host_id=self.worker.id) + + self.test_ptp_interface = dbutils.create_test_ptp_interface( + interface_id=self.test_interface.id, + ptp_instance_id=self.test_instance_ptp4l.id) + + def test_delete_ptp_interface(self): + response = self.delete(self.get_single_url(self.test_ptp_interface.uuid), + headers=self.API_HEADERS) + self.assertEqual(response.status_code, http_client.NO_CONTENT) + + error_message = 'No PTP interface with id %s found' % self.test_ptp_interface.uuid + response = self.get_json(self.get_single_url(self.test_ptp_interface.uuid), + expect_errors=True) + self.assertEqual('application/json', response.content_type) + self.assertEqual(response.status_code, http_client.NOT_FOUND) + self.assertIn(error_message, response.json['error_message']) + + def test_delete_ptp_interface_with_parameters_failed(self): + # TODO: implement when PTP parameters API is available + pass diff --git a/sysinv/sysinv/sysinv/sysinv/tests/db/utils.py b/sysinv/sysinv/sysinv/sysinv/tests/db/utils.py index 4a83f835cd..340cb09910 100644 --- a/sysinv/sysinv/sysinv/sysinv/tests/db/utils.py +++ b/sysinv/sysinv/sysinv/sysinv/tests/db/utils.py @@ -536,6 +536,45 @@ def create_test_ptp(**kw): return dbapi.ptp_create(ptp) +# Create test ptp_instance object +def get_test_ptp_instance(**kw): + ptp_instance = { + 'id': kw.get('id'), + 'uuid': kw.get('uuid'), + 'name': kw.get('name'), + 'service': kw.get('service'), + 'host_id': kw.get('host_id'), + } + return ptp_instance + + +def create_test_ptp_instance(**kw): + ptp_instance = get_test_ptp_instance(**kw) + # Let DB generate ID if it isn't specified explicitly + if 'id' not in kw: + del ptp_instance['id'] + dbapi = db_api.get_instance() + return dbapi.ptp_instance_create(ptp_instance) + + +# Create test ptp_interface object +def get_test_ptp_interface(**kw): + ptp_interface = { + 'uuid': kw.get('uuid'), + 'interface_id': kw.get('interface_id'), + 'ptp_instance_id': kw.get('ptp_instance_id') + } + return ptp_interface + + +def create_test_ptp_interface(**kw): + ptp_interface = get_test_ptp_interface(**kw) + if 'uuid' in kw: + del ptp_interface['uuid'] + dbapi = db_api.get_instance() + return dbapi.ptp_interface_create(ptp_interface) + + # Create test dns object def get_test_dns(**kw): dns = {