diff --git a/ironic/objects/__init__.py b/ironic/objects/__init__.py index 72fe9d9862d..6e62da2f6e4 100644 --- a/ironic/objects/__init__.py +++ b/ironic/objects/__init__.py @@ -28,3 +28,4 @@ def register_all(): __import__('ironic.objects.conductor') __import__('ironic.objects.node') __import__('ironic.objects.port') + __import__('ironic.objects.portgroup') diff --git a/ironic/objects/port.py b/ironic/objects/port.py index b1adf144fc5..a93365b9a56 100644 --- a/ironic/objects/port.py +++ b/ironic/objects/port.py @@ -32,7 +32,9 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat): # Version 1.2: Add create() and destroy() # Version 1.3: Add list() # Version 1.4: Add list_by_node_id() - VERSION = '1.4' + # Version 1.5: Add list_by_portgroup_id() and new fields + # local_link_connection, portgroup_id and pxe_enabled + VERSION = '1.5' dbapi = dbapi.get_instance() @@ -42,6 +44,10 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat): 'node_id': object_fields.IntegerField(nullable=True), 'address': object_fields.MACAddressField(nullable=True), 'extra': object_fields.FlexibleDictField(nullable=True), + 'local_link_connection': object_fields.FlexibleDictField( + nullable=True), + 'portgroup_id': object_fields.IntegerField(nullable=True), + 'pxe_enabled': object_fields.BooleanField() } @staticmethod @@ -184,6 +190,31 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat): sort_dir=sort_dir) return Port._from_db_object_list(db_ports, cls, context) + # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable + # methods can be used in the future to replace current explicit RPC calls. + # Implications of calling new remote procedures should be thought through. + # @object_base.remotable_classmethod + @classmethod + def list_by_portgroup_id(cls, context, portgroup_id, limit=None, + marker=None, sort_key=None, sort_dir=None): + """Return a list of Port objects associated with a given portgroup ID. + + :param context: Security context. + :param portgroup_id: the ID of the portgroup. + :param limit: maximum number of resources to return in a single result. + :param marker: pagination marker for large data sets. + :param sort_key: column to sort results by. + :param sort_dir: direction to sort. "asc" or "desc". + :returns: a list of :class:`Port` object. + + """ + db_ports = cls.dbapi.get_ports_by_portgroup_id(portgroup_id, + limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir) + return Port._from_db_object_list(db_ports, cls, context) + # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. diff --git a/ironic/objects/portgroup.py b/ironic/objects/portgroup.py new file mode 100644 index 00000000000..b3d83a53145 --- /dev/null +++ b/ironic/objects/portgroup.py @@ -0,0 +1,288 @@ +# coding=utf-8 +# +# +# 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 oslo_utils import strutils +from oslo_utils import uuidutils +from oslo_versionedobjects import base as object_base + +from ironic.common import exception +from ironic.common import utils +from ironic.db import api as dbapi +from ironic.objects import base +from ironic.objects import fields as object_fields + + +@base.IronicObjectRegistry.register +class Portgroup(base.IronicObject, object_base.VersionedObjectDictCompat): + # Version 1.0: Initial version + VERSION = '1.0' + + dbapi = dbapi.get_instance() + + fields = { + 'id': object_fields.IntegerField(), + 'uuid': object_fields.UUIDField(nullable=True), + 'name': object_fields.StringField(nullable=True), + 'node_id': object_fields.IntegerField(nullable=True), + 'address': object_fields.MACAddressField(nullable=True), + 'extra': object_fields.FlexibleDictField(nullable=True), + } + + @staticmethod + def _from_db_object(portgroup, db_portgroup): + """Converts a database entity to a formal object.""" + for field in portgroup.fields: + portgroup[field] = db_portgroup[field] + + portgroup.obj_reset_changes() + return portgroup + + @staticmethod + def _from_db_object_list(db_objects, cls, context): + """Converts a list of database entities to a list of formal objects.""" + return [Portgroup._from_db_object(cls(context), obj) for obj in + db_objects] + + # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable + # methods can be used in the future to replace current explicit RPC calls. + # Implications of calling new remote procedures should be thought through. + # @object_base.remotable_classmethod + @classmethod + def get(cls, context, portgroup_ident): + """Find a portgroup based on its id, uuid, name or address. + + :param portgroup_ident: The id, uuid, name or address of a portgroup. + :param context: Security context + :returns: A :class:`Portgroup` object. + :raises: InvalidIdentity + + """ + if strutils.is_int_like(portgroup_ident): + return cls.get_by_id(context, portgroup_ident) + elif uuidutils.is_uuid_like(portgroup_ident): + return cls.get_by_uuid(context, portgroup_ident) + elif utils.is_valid_mac(portgroup_ident): + return cls.get_by_address(context, portgroup_ident) + elif utils.is_valid_logical_name(portgroup_ident): + return cls.get_by_name(context, portgroup_ident) + else: + raise exception.InvalidIdentity(identity=portgroup_ident) + + # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable + # methods can be used in the future to replace current explicit RPC calls. + # Implications of calling new remote procedures should be thought through. + # @object_base.remotable_classmethod + @classmethod + def get_by_id(cls, context, portgroup_id): + """Find a portgroup based on its integer id and return a Portgroup object. + + :param portgroup id: The id of a portgroup. + :param context: Security context + :returns: A :class:`Portgroup` object. + :raises: PortgroupNotFound + + """ + db_portgroup = cls.dbapi.get_portgroup_by_id(portgroup_id) + portgroup = Portgroup._from_db_object(cls(context), db_portgroup) + return portgroup + + # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable + # methods can be used in the future to replace current explicit RPC calls. + # Implications of calling new remote procedures should be thought through. + # @object_base.remotable_classmethod + @classmethod + def get_by_uuid(cls, context, uuid): + """Find a portgroup based on uuid and return a :class:`Portgroup` object. + + :param uuid: The uuid of a portgroup. + :param context: Security context + :returns: A :class:`Portgroup` object. + :raises: PortgroupNotFound + + """ + db_portgroup = cls.dbapi.get_portgroup_by_uuid(uuid) + portgroup = Portgroup._from_db_object(cls(context), db_portgroup) + return portgroup + + # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable + # methods can be used in the future to replace current explicit RPC calls. + # Implications of calling new remote procedures should be thought through. + # @object_base.remotable_classmethod + @classmethod + def get_by_address(cls, context, address): + """Find a portgroup based on address and return a :class:`Portgroup` object. + + :param address: The MAC address of a portgroup. + :param context: Security context + :returns: A :class:`Portgroup` object. + :raises: PortgroupNotFound + + """ + db_portgroup = cls.dbapi.get_portgroup_by_address(address) + portgroup = Portgroup._from_db_object(cls(context), db_portgroup) + return portgroup + + # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable + # methods can be used in the future to replace current explicit RPC calls. + # Implications of calling new remote procedures should be thought through. + # @object_base.remotable_classmethod + @classmethod + def get_by_name(cls, context, name): + """Find a portgroup based on name and return a :class:`Portgroup` object. + + :param name: The name of a portgroup. + :param context: Security context + :returns: A :class:`Portgroup` object. + :raises: PortgroupNotFound + + """ + db_portgroup = cls.dbapi.get_portgroup_by_name(name) + portgroup = Portgroup._from_db_object(cls(context), db_portgroup) + return portgroup + + # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable + # methods can be used in the future to replace current explicit RPC calls. + # Implications of calling new remote procedures should be thought through. + # @object_base.remotable_classmethod + @classmethod + def list(cls, context, limit=None, marker=None, + sort_key=None, sort_dir=None): + """Return a list of Portgroup objects. + + :param context: Security context. + :param limit: Maximum number of resources to return in a single result. + :param marker: Pagination marker for large data sets. + :param sort_key: Column to sort results by. + :param sort_dir: Direction to sort. "asc" or "desc". + :returns: A list of :class:`Portgroup` object. + :raises: InvalidParameterValue + + """ + db_portgroups = cls.dbapi.get_portgroup_list(limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir) + return Portgroup._from_db_object_list(db_portgroups, cls, context) + + # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable + # methods can be used in the future to replace current explicit RPC calls. + # Implications of calling new remote procedures should be thought through. + # @object_base.remotable_classmethod + @classmethod + def list_by_node_id(cls, context, node_id, limit=None, marker=None, + sort_key=None, sort_dir=None): + """Return a list of Portgroup objects associated with a given node ID. + + :param context: Security context. + :param node_id: The ID of the node. + :param limit: Maximum number of resources to return in a single result. + :param marker: Pagination marker for large data sets. + :param sort_key: Column to sort results by. + :param sort_dir: Direction to sort. "asc" or "desc". + :returns: A list of :class:`Portgroup` object. + :raises: InvalidParameterValue + + """ + db_portgroups = cls.dbapi.get_portgroups_by_node_id(node_id, + limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir) + return Portgroup._from_db_object_list(db_portgroups, cls, context) + + # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable + # methods can be used in the future to replace current explicit RPC calls. + # Implications of calling new remote procedures should be thought through. + # @object_base.remotable + def create(self, context=None): + """Create a Portgroup record in the DB. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Portgroup(context) + :raises: DuplicateName, MACAlreadyExists, PortgroupAlreadyExists + + """ + values = self.obj_get_changes() + db_portgroup = self.dbapi.create_portgroup(values) + self._from_db_object(self, db_portgroup) + + # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable + # methods can be used in the future to replace current explicit RPC calls. + # Implications of calling new remote procedures should be thought through. + # @object_base.remotable + def destroy(self, context=None): + """Delete the Portgroup from the DB. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Portgroup(context) + :raises: PortgroupNotEmpty, PortgroupNotFound + + """ + self.dbapi.destroy_portgroup(self.uuid) + self.obj_reset_changes() + + # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable + # methods can be used in the future to replace current explicit RPC calls. + # Implications of calling new remote procedures should be thought through. + # @object_base.remotable + def save(self, context=None): + """Save updates to this Portgroup. + + Updates will be made column by column based on the result + of self.what_changed(). + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Portgroup(context) + :raises: PortgroupNotFound, DuplicateName, MACAlreadyExists + + """ + updates = self.obj_get_changes() + updated_portgroup = self.dbapi.update_portgroup(self.uuid, updates) + self._from_db_object(self, updated_portgroup) + + # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable + # methods can be used in the future to replace current explicit RPC calls. + # Implications of calling new remote procedures should be thought through. + # @object_base.remotable + def refresh(self, context=None): + """Loads updates for this Portgroup. + + Loads a portgroup with the same uuid from the database and + checks for updated attributes. Updates are applied from + the loaded portgroup column by column, if there are any updates. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Portgroup(context) + :raises: PortgroupNotFound + + """ + current = self.__class__.get_by_uuid(self._context, uuid=self.uuid) + self.obj_refresh(current) diff --git a/ironic/tests/unit/objects/test_objects.py b/ironic/tests/unit/objects/test_objects.py index 4616c6c88dd..ce52ad8ee1c 100644 --- a/ironic/tests/unit/objects/test_objects.py +++ b/ironic/tests/unit/objects/test_objects.py @@ -407,7 +407,8 @@ expected_object_fingerprints = { 'Node': '1.14-9ee8ab283b06398545880dfdedb49891', 'MyObj': '1.5-4f5efe8f0fcaf182bbe1c7fe3ba858db', 'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905', - 'Port': '1.4-f5aa3ff81d1459d6d7e6d9d9dceed351', + 'Port': '1.5-a224755c3da5bc5cf1a14a11c0d00f3f', + 'Portgroup': '1.0-1ac4db8fa31edd9e1637248ada4c25a1', 'Conductor': '1.0-5091f249719d4a465062a1b3dc7f860d' } diff --git a/ironic/tests/unit/objects/test_portgroup.py b/ironic/tests/unit/objects/test_portgroup.py new file mode 100644 index 00000000000..4ced77a2f68 --- /dev/null +++ b/ironic/tests/unit/objects/test_portgroup.py @@ -0,0 +1,135 @@ +# 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 datetime +import mock +from testtools.matchers import HasLength + +from ironic.common import exception +from ironic import objects +from ironic.tests.unit.db import base +from ironic.tests.unit.db import utils + + +class TestPortgroupObject(base.DbTestCase): + + def setUp(self): + super(TestPortgroupObject, self).setUp() + self.fake_portgroup = utils.get_test_portgroup() + + def test_get_by_id(self): + portgroup_id = self.fake_portgroup['id'] + with mock.patch.object(self.dbapi, 'get_portgroup_by_id', + autospec=True) as mock_get_portgroup: + mock_get_portgroup.return_value = self.fake_portgroup + + portgroup = objects.Portgroup.get(self.context, portgroup_id) + + mock_get_portgroup.assert_called_once_with(portgroup_id) + self.assertEqual(self.context, portgroup._context) + + def test_get_by_uuid(self): + uuid = self.fake_portgroup['uuid'] + with mock.patch.object(self.dbapi, 'get_portgroup_by_uuid', + autospec=True) as mock_get_portgroup: + mock_get_portgroup.return_value = self.fake_portgroup + + portgroup = objects.Portgroup.get(self.context, uuid) + + mock_get_portgroup.assert_called_once_with(uuid) + self.assertEqual(self.context, portgroup._context) + + def test_get_by_address(self): + address = self.fake_portgroup['address'] + with mock.patch.object(self.dbapi, 'get_portgroup_by_address', + autospec=True) as mock_get_portgroup: + mock_get_portgroup.return_value = self.fake_portgroup + + portgroup = objects.Portgroup.get(self.context, address) + + mock_get_portgroup.assert_called_once_with(address) + self.assertEqual(self.context, portgroup._context) + + def test_get_by_name(self): + name = self.fake_portgroup['name'] + with mock.patch.object(self.dbapi, 'get_portgroup_by_name', + autospec=True) as mock_get_portgroup: + mock_get_portgroup.return_value = self.fake_portgroup + portgroup = objects.Portgroup.get(self.context, name) + + mock_get_portgroup.assert_called_once_with(name) + self.assertEqual(self.context, portgroup._context) + + def test_get_bad_id_and_uuid_and_address_and_name(self): + self.assertRaises(exception.InvalidIdentity, + objects.Portgroup.get, + self.context, + 'not:a_name_or_uuid') + + def test_save(self): + uuid = self.fake_portgroup['uuid'] + address = "b2:54:00:cf:2d:40" + test_time = datetime.datetime(2000, 1, 1, 0, 0) + with mock.patch.object(self.dbapi, 'get_portgroup_by_uuid', + autospec=True) as mock_get_portgroup: + mock_get_portgroup.return_value = self.fake_portgroup + with mock.patch.object(self.dbapi, 'update_portgroup', + autospec=True) as mock_update_portgroup: + mock_update_portgroup.return_value = ( + utils.get_test_portgroup(address=address, + updated_at=test_time)) + p = objects.Portgroup.get_by_uuid(self.context, uuid) + p.address = address + p.save() + + mock_get_portgroup.assert_called_once_with(uuid) + mock_update_portgroup.assert_called_once_with( + uuid, {'address': "b2:54:00:cf:2d:40"}) + self.assertEqual(self.context, p._context) + res_updated_at = (p.updated_at).replace(tzinfo=None) + self.assertEqual(test_time, res_updated_at) + + def test_refresh(self): + uuid = self.fake_portgroup['uuid'] + returns = [self.fake_portgroup, + utils.get_test_portgroup(address="c3:54:00:cf:2d:40")] + expected = [mock.call(uuid), mock.call(uuid)] + with mock.patch.object(self.dbapi, 'get_portgroup_by_uuid', + side_effect=returns, + autospec=True) as mock_get_portgroup: + p = objects.Portgroup.get_by_uuid(self.context, uuid) + self.assertEqual("52:54:00:cf:2d:31", p.address) + p.refresh() + self.assertEqual("c3:54:00:cf:2d:40", p.address) + + self.assertEqual(expected, mock_get_portgroup.call_args_list) + self.assertEqual(self.context, p._context) + + def test_list(self): + with mock.patch.object(self.dbapi, 'get_portgroup_list', + autospec=True) as mock_get_list: + mock_get_list.return_value = [self.fake_portgroup] + portgroups = objects.Portgroup.list(self.context) + self.assertThat(portgroups, HasLength(1)) + self.assertIsInstance(portgroups[0], objects.Portgroup) + self.assertEqual(self.context, portgroups[0]._context) + + def test_list_by_node_id(self): + with mock.patch.object(self.dbapi, 'get_portgroups_by_node_id', + autospec=True) as mock_get_list: + mock_get_list.return_value = [self.fake_portgroup] + node_id = self.fake_portgroup['node_id'] + portgroups = objects.Portgroup.list_by_node_id(self.context, + node_id) + self.assertThat(portgroups, HasLength(1)) + self.assertIsInstance(portgroups[0], objects.Portgroup) + self.assertEqual(self.context, portgroups[0]._context) diff --git a/ironic/tests/unit/objects/utils.py b/ironic/tests/unit/objects/utils.py index 79577f924c0..f3718182259 100644 --- a/ironic/tests/unit/objects/utils.py +++ b/ironic/tests/unit/objects/utils.py @@ -97,3 +97,30 @@ def create_test_chassis(ctxt, **kw): chassis = get_test_chassis(ctxt, **kw) chassis.create() return chassis + + +def get_test_portgroup(ctxt, **kw): + """Return a Portgroup object with appropriate attributes. + + NOTE: The object leaves the attributes marked as changed, such + that a create() could be used to commit it to the DB. + """ + db_portgroup = db_utils.get_test_portgroup(**kw) + # Let DB generate ID if it isn't specified explicitly + if 'id' not in kw: + del db_portgroup['id'] + portgroup = objects.Portgroup(ctxt) + for key in db_portgroup: + setattr(portgroup, key, db_portgroup[key]) + return portgroup + + +def create_test_portgroup(ctxt, **kw): + """Create and return a test portgroup object. + + Create a portgroup in the DB and return a Portgroup object with appropriate + attributes. + """ + portgroup = get_test_portgroup(ctxt, **kw) + portgroup.create() + return portgroup