diff --git a/ironic/objects/__init__.py b/ironic/objects/__init__.py index 6e62da2f6e..9457de1f45 100644 --- a/ironic/objects/__init__.py +++ b/ironic/objects/__init__.py @@ -29,3 +29,4 @@ def register_all(): __import__('ironic.objects.node') __import__('ironic.objects.port') __import__('ironic.objects.portgroup') + __import__('ironic.objects.volume_connector') diff --git a/ironic/objects/volume_connector.py b/ironic/objects/volume_connector.py new file mode 100644 index 0000000000..a77c986663 --- /dev/null +++ b/ironic/objects/volume_connector.py @@ -0,0 +1,246 @@ +# Copyright (c) 2015 Hitachi, 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 oslo_utils import strutils +from oslo_utils import uuidutils +from oslo_versionedobjects import base as object_base + +from ironic.common import exception +from ironic.db import api as db_api +from ironic.objects import base +from ironic.objects import fields as object_fields + + +@base.IronicObjectRegistry.register +class VolumeConnector(base.IronicObject, + object_base.VersionedObjectDictCompat): + # Version 1.0: Initial version + VERSION = '1.0' + + dbapi = db_api.get_instance() + + fields = { + 'id': object_fields.IntegerField(), + 'uuid': object_fields.UUIDField(nullable=True), + 'node_id': object_fields.IntegerField(nullable=True), + 'type': object_fields.StringField(nullable=True), + 'connector_id': object_fields.StringField(nullable=True), + 'extra': object_fields.FlexibleDictField(nullable=True), + } + + @staticmethod + def _from_db_object_list(db_objects, cls, context): + """Convert a list of database entities to a list of formal objects.""" + return [VolumeConnector._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, ident): + """Find a volume connector based on its ID or UUID. + + :param context: security context + :param ident: the database primary key ID *or* the UUID of a volume + connector + :returns: a :class:`VolumeConnector` object + :raises: InvalidIdentity if ident is neither an integer ID nor a UUID + :raises: VolumeConnectorNotFound if no volume connector exists with + the specified ident + """ + if strutils.is_int_like(ident): + return cls.get_by_id(context, ident) + elif uuidutils.is_uuid_like(ident): + return cls.get_by_uuid(context, ident) + else: + raise exception.InvalidIdentity(identity=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, id): + """Find a volume connector based on its integer ID. + + :param context: security context + :param id: the integer (database primary key) ID of a volume connector + :returns: a :class:`VolumeConnector` object + :raises: VolumeConnectorNotFound if no volume connector exists with + the specified ID + """ + db_connector = cls.dbapi.get_volume_connector_by_id(id) + connector = VolumeConnector._from_db_object(cls(context), db_connector) + return connector + + # 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 volume connector based on its UUID. + + :param context: security context + :param uuid: the UUID of a volume connector + :returns: a :class:`VolumeConnector` object + :raises: VolumeConnectorNotFound if no volume connector exists with + the specified UUID + """ + db_connector = cls.dbapi.get_volume_connector_by_uuid(uuid) + connector = VolumeConnector._from_db_object(cls(context), db_connector) + return connector + + # 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 VolumeConnector 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:`VolumeConnector` objects + :raises: InvalidParameterValue if sort_key does not exist + """ + db_connectors = cls.dbapi.get_volume_connector_list(limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir) + return VolumeConnector._from_db_object_list(db_connectors, + 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 VolumeConnector objects related to a given node ID. + + :param context: security context + :param node_id: the integer 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:`VolumeConnector` objects + :raises: InvalidParameterValue if sort_key does not exist + """ + db_connectors = cls.dbapi.get_volume_connectors_by_node_id( + node_id, + limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir) + return VolumeConnector._from_db_object_list(db_connectors, + 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 VolumeConnector 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.: VolumeConnector(context). + :raises: VolumeConnectorTypeAndIdAlreadyExists if a volume + connector already exists with the same type and connector_id + :raises: VolumeConnectorAlreadyExists if a volume connector with the + same UUID already exists + """ + values = self.obj_get_changes() + db_connector = self.dbapi.create_volume_connector(values) + self._from_db_object(self, db_connector) + + # 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 VolumeConnector 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.: VolumeConnector(context). + :raises: VolumeConnectorNotFound if the volume connector cannot be + found + """ + self.dbapi.destroy_volume_connector(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 VolumeConnector. + + Updates will be made column by column based on the result + of self.obj_get_changes(). + + :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.: VolumeConnector(context). + :raises: VolumeConnectorNotFound if the volume connector cannot be + found + :raises: VolumeConnectorTypeAndIdAlreadyExists if another connector + already exists with the same values for type and connector_id + fields + :raises: InvalidParameterValue when the UUID is being changed + """ + updates = self.obj_get_changes() + updated_connector = self.dbapi.update_volume_connector(self.uuid, + updates) + self._from_db_object(self, updated_connector) + + # 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): + """Load updates for this VolumeConnector. + + Load a volume connector with the same UUID from the database + and check for updated attributes. If there are any updates, + they are applied from the loaded volume connector, column by column. + + :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.: VolumeConnector(context). + """ + 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 c08fd001a8..ce1a9984b6 100644 --- a/ironic/tests/unit/objects/test_objects.py +++ b/ironic/tests/unit/objects/test_objects.py @@ -420,7 +420,8 @@ expected_object_fingerprints = { 'NodeCorrectedPowerStatePayload': '1.0-2a484d7c342caa9fe488de16dc5f1f1e', 'NodeSetProvisionStateNotification': '1.0-59acc533c11d306f149846f922739c15', - 'NodeSetProvisionStatePayload': '1.0-91be7439b9b6b04931c9b99b8e1ea87a' + 'NodeSetProvisionStatePayload': '1.0-91be7439b9b6b04931c9b99b8e1ea87a', + 'VolumeConnector': '1.0-3e0252c0ab6e6b9d158d09238a577d97' } diff --git a/ironic/tests/unit/objects/test_volume_connector.py b/ironic/tests/unit/objects/test_volume_connector.py new file mode 100644 index 0000000000..2f5a94b720 --- /dev/null +++ b/ironic/tests/unit/objects/test_volume_connector.py @@ -0,0 +1,179 @@ +# Copyright 2015 Hitachi Data Systems +# +# 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 TestVolumeConnectorObject(base.DbTestCase): + + def setUp(self): + super(TestVolumeConnectorObject, self).setUp() + self.volume_connector_dict = utils.get_test_volume_connector() + + @mock.patch('ironic.objects.VolumeConnector.get_by_uuid') + @mock.patch('ironic.objects.VolumeConnector.get_by_id') + def test_get(self, mock_get_by_id, mock_get_by_uuid): + id = self.volume_connector_dict['id'] + uuid = self.volume_connector_dict['uuid'] + + objects.VolumeConnector.get(self.context, id) + mock_get_by_id.assert_called_once_with(self.context, id) + self.assertFalse(mock_get_by_uuid.called) + + objects.VolumeConnector.get(self.context, uuid) + mock_get_by_uuid.assert_called_once_with(self.context, uuid) + + # Invalid identifier (not ID or UUID) + self.assertRaises(exception.InvalidIdentity, + objects.VolumeConnector.get, + self.context, 'not-valid-identifier') + + def test_get_by_id(self): + id = self.volume_connector_dict['id'] + with mock.patch.object(self.dbapi, 'get_volume_connector_by_id', + autospec=True) as mock_get_volume_connector: + mock_get_volume_connector.return_value = self.volume_connector_dict + + connector = objects.VolumeConnector.get_by_id(self.context, id) + + mock_get_volume_connector.assert_called_once_with(id) + self.assertIsInstance(connector, objects.VolumeConnector) + self.assertEqual(self.context, connector._context) + + def test_get_by_uuid(self): + uuid = self.volume_connector_dict['uuid'] + with mock.patch.object(self.dbapi, 'get_volume_connector_by_uuid', + autospec=True) as mock_get_volume_connector: + mock_get_volume_connector.return_value = self.volume_connector_dict + + connector = objects.VolumeConnector.get_by_uuid(self.context, uuid) + + mock_get_volume_connector.assert_called_once_with(uuid) + self.assertIsInstance(connector, objects.VolumeConnector) + self.assertEqual(self.context, connector._context) + + def test_list(self): + with mock.patch.object(self.dbapi, 'get_volume_connector_list', + autospec=True) as mock_get_list: + mock_get_list.return_value = [self.volume_connector_dict] + volume_connectors = objects.VolumeConnector.list( + self.context, limit=4, sort_key='uuid', sort_dir='asc') + + mock_get_list.assert_called_once_with( + limit=4, marker=None, sort_key='uuid', sort_dir='asc') + self.assertThat(volume_connectors, HasLength(1)) + self.assertIsInstance(volume_connectors[0], + objects.VolumeConnector) + self.assertEqual(self.context, volume_connectors[0]._context) + + def test_list_none(self): + with mock.patch.object(self.dbapi, 'get_volume_connector_list', + autospec=True) as mock_get_list: + mock_get_list.return_value = [] + volume_connectors = objects.VolumeConnector.list( + self.context, limit=4, sort_key='uuid', sort_dir='asc') + + mock_get_list.assert_called_once_with( + limit=4, marker=None, sort_key='uuid', sort_dir='asc') + self.assertEqual([], volume_connectors) + + def test_list_by_node_id(self): + with mock.patch.object(self.dbapi, 'get_volume_connectors_by_node_id', + autospec=True) as mock_get_list_by_node_id: + mock_get_list_by_node_id.return_value = [ + self.volume_connector_dict] + node_id = self.volume_connector_dict['node_id'] + volume_connectors = objects.VolumeConnector.list_by_node_id( + self.context, node_id, limit=10, sort_dir='desc') + + mock_get_list_by_node_id.assert_called_once_with( + node_id, limit=10, marker=None, sort_key=None, sort_dir='desc') + self.assertThat(volume_connectors, HasLength(1)) + self.assertIsInstance(volume_connectors[0], + objects.VolumeConnector) + self.assertEqual(self.context, volume_connectors[0]._context) + + def test_create(self): + with mock.patch.object(self.dbapi, 'create_volume_connector', + autospec=True) as mock_db_create: + mock_db_create.return_value = self.volume_connector_dict + new_connector = objects.VolumeConnector( + self.context, **self.volume_connector_dict) + new_connector.create() + + mock_db_create.assert_called_once_with(self.volume_connector_dict) + + def test_destroy(self): + uuid = self.volume_connector_dict['uuid'] + with mock.patch.object(self.dbapi, 'get_volume_connector_by_uuid', + autospec=True) as mock_get_volume_connector: + mock_get_volume_connector.return_value = self.volume_connector_dict + with mock.patch.object(self.dbapi, 'destroy_volume_connector', + autospec=True) as mock_db_destroy: + connector = objects.VolumeConnector.get_by_uuid(self.context, + uuid) + connector.destroy() + + mock_db_destroy.assert_called_once_with(uuid) + + def test_save(self): + uuid = self.volume_connector_dict['uuid'] + connector_id = "new_connector_id" + test_time = datetime.datetime(2000, 1, 1, 0, 0) + with mock.patch.object(self.dbapi, 'get_volume_connector_by_uuid', + autospec=True) as mock_get_volume_connector: + mock_get_volume_connector.return_value = self.volume_connector_dict + with mock.patch.object(self.dbapi, 'update_volume_connector', + autospec=True) as mock_update_connector: + mock_update_connector.return_value = ( + utils.get_test_volume_connector(connector_id=connector_id, + updated_at=test_time)) + c = objects.VolumeConnector.get_by_uuid(self.context, uuid) + c.connector_id = connector_id + c.save() + + mock_get_volume_connector.assert_called_once_with(uuid) + mock_update_connector.assert_called_once_with( + uuid, + {'connector_id': connector_id}) + self.assertEqual(self.context, c._context) + res_updated_at = (c.updated_at).replace(tzinfo=None) + self.assertEqual(test_time, res_updated_at) + + def test_refresh(self): + uuid = self.volume_connector_dict['uuid'] + old_connector_id = self.volume_connector_dict['connector_id'] + returns = [self.volume_connector_dict, + utils.get_test_volume_connector( + connector_id="new_connector_id")] + expected = [mock.call(uuid), mock.call(uuid)] + with mock.patch.object(self.dbapi, 'get_volume_connector_by_uuid', + side_effect=returns, + autospec=True) as mock_get_volume_connector: + c = objects.VolumeConnector.get_by_uuid(self.context, uuid) + self.assertEqual(old_connector_id, c.connector_id) + c.refresh() + self.assertEqual('new_connector_id', c.connector_id) + + self.assertEqual(expected, + mock_get_volume_connector.call_args_list) + self.assertEqual(self.context, c._context)