Browse Source

Add VolumeTarget object

This patch adds the VolumeTarget object. It handles the
volume target information which is stored in the database.

Co-Authored-By: Stephane Miller <stephane@alum.mit.edu>
Co-Authored-By: Ruby Loo <ruby.loo@intel.com>
Change-Id: I814454ec5a515080d21aad58f8d43abb49b77fb3
Partial-Bug: 1526231
tags/7.0.0
Satoru Moriya 3 years ago
parent
commit
f766bbab45

+ 1
- 0
ironic/objects/__init__.py View File

@@ -30,3 +30,4 @@ def register_all():
30 30
     __import__('ironic.objects.port')
31 31
     __import__('ironic.objects.portgroup')
32 32
     __import__('ironic.objects.volume_connector')
33
+    __import__('ironic.objects.volume_target')

+ 14
- 0
ironic/objects/base.py View File

@@ -89,6 +89,20 @@ class IronicObject(object_base.VersionedObject):
89 89
         obj.obj_reset_changes()
90 90
         return obj
91 91
 
92
+    @classmethod
93
+    def _from_db_object_list(cls, context, db_objects):
94
+        """Returns objects corresponding to database entities.
95
+
96
+        Returns a list of formal objects of this class that correspond to
97
+        the list of database entities.
98
+
99
+        :param context: security context
100
+        :param db_objects: A  list of DB models of the object
101
+        :returns: A list of objects corresponding to the database entities
102
+        """
103
+        return [cls._from_db_object(cls(context), db_obj)
104
+                for db_obj in db_objects]
105
+
92 106
 
93 107
 class IronicObjectSerializer(object_base.VersionedObjectSerializer):
94 108
     # Base class to use for object hydration

+ 235
- 0
ironic/objects/volume_target.py View File

@@ -0,0 +1,235 @@
1
+#    Copyright (c) 2016 Hitachi, Ltd.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+from oslo_utils import strutils
16
+from oslo_utils import uuidutils
17
+from oslo_versionedobjects import base as object_base
18
+
19
+from ironic.common import exception
20
+from ironic.db import api as db_api
21
+from ironic.objects import base
22
+from ironic.objects import fields as object_fields
23
+
24
+
25
+@base.IronicObjectRegistry.register
26
+class VolumeTarget(base.IronicObject,
27
+                   object_base.VersionedObjectDictCompat):
28
+    # Version 1.0: Initial version
29
+    VERSION = '1.0'
30
+
31
+    dbapi = db_api.get_instance()
32
+
33
+    fields = {
34
+        'id': object_fields.IntegerField(),
35
+        'uuid': object_fields.UUIDField(nullable=True),
36
+        'node_id': object_fields.IntegerField(nullable=True),
37
+        'volume_type': object_fields.StringField(nullable=True),
38
+        'properties': object_fields.FlexibleDictField(nullable=True),
39
+        'boot_index': object_fields.IntegerField(nullable=True),
40
+        'volume_id': object_fields.StringField(nullable=True),
41
+        'extra': object_fields.FlexibleDictField(nullable=True),
42
+    }
43
+
44
+    # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
45
+    # methods can be used in the future to replace current explicit RPC calls.
46
+    # Implications of calling new remote procedures should be thought through.
47
+    # @object_base.remotable_classmethod
48
+    @classmethod
49
+    def get(cls, context, ident):
50
+        """Find a volume target based on its ID or UUID.
51
+
52
+        :param context: security context
53
+        :param ident: the database primary key ID *or* the UUID of a volume
54
+                      target
55
+        :returns: a :class:`VolumeTarget` object
56
+        :raises: InvalidIdentity if ident is neither an integer ID nor a UUID
57
+        :raises: VolumeTargetNotFound if no volume target with this ident
58
+                 exists
59
+        """
60
+        if strutils.is_int_like(ident):
61
+            return cls.get_by_id(context, ident)
62
+        elif uuidutils.is_uuid_like(ident):
63
+            return cls.get_by_uuid(context, ident)
64
+        else:
65
+            raise exception.InvalidIdentity(identity=ident)
66
+
67
+    # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
68
+    # methods can be used in the future to replace current explicit RPC calls.
69
+    # Implications of calling new remote procedures should be thought through.
70
+    # @object_base.remotable_classmethod
71
+    @classmethod
72
+    def get_by_id(cls, context, db_id):
73
+        """Find a volume target based on its database ID.
74
+
75
+        :param context: security context
76
+        :param db_id: the database primary key (integer) ID of a volume target
77
+        :returns: a :class:`VolumeTarget` object
78
+        :raises: VolumeTargetNotFound if no volume target with this ID exists
79
+        """
80
+        db_target = cls.dbapi.get_volume_target_by_id(db_id)
81
+        target = VolumeTarget._from_db_object(cls(context), db_target)
82
+        return target
83
+
84
+    # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
85
+    # methods can be used in the future to replace current explicit RPC calls.
86
+    # Implications of calling new remote procedures should be thought through.
87
+    # @object_base.remotable_classmethod
88
+    @classmethod
89
+    def get_by_uuid(cls, context, uuid):
90
+        """Find a volume target based on its UUID.
91
+
92
+        :param context: security context
93
+        :param uuid: the UUID of a volume target
94
+        :returns: a :class:`VolumeTarget` object
95
+        :raises: VolumeTargetNotFound if no volume target with this UUID exists
96
+        """
97
+        db_target = cls.dbapi.get_volume_target_by_uuid(uuid)
98
+        target = VolumeTarget._from_db_object(cls(context), db_target)
99
+        return target
100
+
101
+    # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
102
+    # methods can be used in the future to replace current explicit RPC calls.
103
+    # Implications of calling new remote procedures should be thought through.
104
+    # @object_base.remotable_classmethod
105
+    @classmethod
106
+    def list(cls, context, limit=None, marker=None,
107
+             sort_key=None, sort_dir=None):
108
+        """Return a list of VolumeTarget objects.
109
+
110
+        :param context: security context
111
+        :param limit: maximum number of resources to return in a single result
112
+        :param marker: pagination marker for large data sets
113
+        :param sort_key: column to sort results by
114
+        :param sort_dir: direction to sort. "asc" or "desc".
115
+        :returns: a list of :class:`VolumeTarget` objects
116
+        :raises: InvalidParameterValue if sort_key does not exist
117
+        """
118
+        db_targets = cls.dbapi.get_volume_target_list(limit=limit,
119
+                                                      marker=marker,
120
+                                                      sort_key=sort_key,
121
+                                                      sort_dir=sort_dir)
122
+        return VolumeTarget._from_db_object_list(context, db_targets)
123
+
124
+    # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
125
+    # methods can be used in the future to replace current explicit RPC calls.
126
+    # Implications of calling new remote procedures should be thought through.
127
+    # @object_base.remotable_classmethod
128
+    @classmethod
129
+    def list_by_node_id(cls, context, node_id, limit=None, marker=None,
130
+                        sort_key=None, sort_dir=None):
131
+        """Return a list of VolumeTarget objects related to a given node ID.
132
+
133
+        :param context: security context
134
+        :param node_id: the integer ID of the node
135
+        :param limit: maximum number of resources to return in a single result
136
+        :param marker: pagination marker for large data sets
137
+        :param sort_key: column to sort results by
138
+        :param sort_dir: direction to sort. "asc" or "desc".
139
+        :returns: a list of :class:`VolumeTarget` objects
140
+        :raises: InvalidParameterValue if sort_key does not exist
141
+        """
142
+        db_targets = cls.dbapi.get_volume_targets_by_node_id(
143
+            node_id,
144
+            limit=limit,
145
+            marker=marker,
146
+            sort_key=sort_key,
147
+            sort_dir=sort_dir)
148
+        return VolumeTarget._from_db_object_list(context, db_targets)
149
+
150
+    # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
151
+    # methods can be used in the future to replace current explicit RPC calls.
152
+    # Implications of calling new remote procedures should be thought through.
153
+    # @object_base.remotable
154
+    def create(self, context=None):
155
+        """Create a VolumeTarget record in the DB.
156
+
157
+        :param context: security context. NOTE: This should only
158
+                        be used internally by the indirection_api.
159
+                        Unfortunately, RPC requires context as the first
160
+                        argument, even though we don't use it.
161
+                        A context should be set when instantiating the
162
+                        object, e.g.: VolumeTarget(context).
163
+        :raises: VolumeTargetBootIndexAlreadyExists if a volume target already
164
+                 exists with the same node ID and boot index
165
+        :raises: VolumeTargetAlreadyExists if a volume target with the same
166
+                 UUID exists
167
+        """
168
+        values = self.obj_get_changes()
169
+        db_target = self.dbapi.create_volume_target(values)
170
+        self._from_db_object(self, db_target)
171
+
172
+    # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
173
+    # methods can be used in the future to replace current explicit RPC calls.
174
+    # Implications of calling new remote procedures should be thought through.
175
+    # @object_base.remotable
176
+    def destroy(self, context=None):
177
+        """Delete the VolumeTarget from the DB.
178
+
179
+        :param context: security context. NOTE: This should only
180
+                        be used internally by the indirection_api.
181
+                        Unfortunately, RPC requires context as the first
182
+                        argument, even though we don't use it.
183
+                        A context should be set when instantiating the
184
+                        object, e.g.: VolumeTarget(context).
185
+        :raises: VolumeTargetNotFound if the volume target cannot be found
186
+        """
187
+        self.dbapi.destroy_volume_target(self.uuid)
188
+        self.obj_reset_changes()
189
+
190
+    # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
191
+    # methods can be used in the future to replace current explicit RPC calls.
192
+    # Implications of calling new remote procedures should be thought through.
193
+    # @object_base.remotable
194
+    def save(self, context=None):
195
+        """Save updates to this VolumeTarget.
196
+
197
+        Updates will be made column by column based on the result
198
+        of self.obj_get_changes().
199
+
200
+        :param context: security context. NOTE: This should only
201
+                        be used internally by the indirection_api.
202
+                        Unfortunately, RPC requires context as the first
203
+                        argument, even though we don't use it.
204
+                        A context should be set when instantiating the
205
+                        object, e.g.: VolumeTarget(context).
206
+        :raises: InvalidParameterValue if the UUID is being changed
207
+        :raises: VolumeTargetBootIndexAlreadyExists if a volume target already
208
+                 exists with the same node ID and boot index values
209
+        :raises: VolumeTargetNotFound if the volume target cannot be found
210
+        """
211
+        updates = self.obj_get_changes()
212
+        updated_target = self.dbapi.update_volume_target(self.uuid, updates)
213
+        self._from_db_object(self, updated_target)
214
+
215
+    # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
216
+    # methods can be used in the future to replace current explicit RPC calls.
217
+    # Implications of calling new remote procedures should be thought through.
218
+    # @object_base.remotable
219
+    def refresh(self, context=None):
220
+        """Loads updates for this VolumeTarget.
221
+
222
+        Load a volume target with the same UUID from the database
223
+        and check for updated attributes. If there are any updates,
224
+        they are applied from the loaded volume target, column by column.
225
+
226
+        :param context: security context. NOTE: This should only
227
+                        be used internally by the indirection_api.
228
+                        Unfortunately, RPC requires context as the first
229
+                        argument, even though we don't use it.
230
+                        A context should be set when instantiating the
231
+                        object, e.g.: VolumeTarget(context).
232
+        :raises: VolumeTargetNotFound if the volume target cannot be found
233
+        """
234
+        current = self.__class__.get_by_uuid(self._context, uuid=self.uuid)
235
+        self.obj_refresh(current)

+ 2
- 1
ironic/tests/unit/objects/test_objects.py View File

@@ -421,7 +421,8 @@ expected_object_fingerprints = {
421 421
     'NodeSetProvisionStateNotification':
422 422
         '1.0-59acc533c11d306f149846f922739c15',
423 423
     'NodeSetProvisionStatePayload': '1.1-743be1f5748f346e3da33390983172b1',
424
-    'VolumeConnector': '1.0-3e0252c0ab6e6b9d158d09238a577d97'
424
+    'VolumeConnector': '1.0-3e0252c0ab6e6b9d158d09238a577d97',
425
+    'VolumeTarget': '1.0-0b10d663d8dae675900b2c7548f76f5e',
425 426
 }
426 427
 
427 428
 

+ 175
- 0
ironic/tests/unit/objects/test_volume_target.py View File

@@ -0,0 +1,175 @@
1
+# Copyright 2016 Hitachi, Ltd.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+import datetime
16
+
17
+import mock
18
+from testtools.matchers import HasLength
19
+
20
+from ironic.common import exception
21
+from ironic import objects
22
+from ironic.tests.unit.db import base
23
+from ironic.tests.unit.db import utils
24
+
25
+
26
+class TestVolumeTargetObject(base.DbTestCase):
27
+
28
+    def setUp(self):
29
+        super(TestVolumeTargetObject, self).setUp()
30
+        self.volume_target_dict = utils.get_test_volume_target()
31
+
32
+    @mock.patch('ironic.objects.VolumeTarget.get_by_uuid')
33
+    @mock.patch('ironic.objects.VolumeTarget.get_by_id')
34
+    def test_get(self, mock_get_by_id, mock_get_by_uuid):
35
+        id = self.volume_target_dict['id']
36
+        uuid = self.volume_target_dict['uuid']
37
+
38
+        objects.VolumeTarget.get(self.context, id)
39
+        mock_get_by_id.assert_called_once_with(self.context, id)
40
+        self.assertFalse(mock_get_by_uuid.called)
41
+
42
+        objects.VolumeTarget.get(self.context, uuid)
43
+        mock_get_by_uuid.assert_called_once_with(self.context, uuid)
44
+
45
+        # Invalid identifier (not ID or UUID)
46
+        self.assertRaises(exception.InvalidIdentity,
47
+                          objects.VolumeTarget.get,
48
+                          self.context, 'not-valid-identifier')
49
+
50
+    def test_get_by_id(self):
51
+        id = self.volume_target_dict['id']
52
+        with mock.patch.object(self.dbapi, 'get_volume_target_by_id',
53
+                               autospec=True) as mock_get_volume_target:
54
+            mock_get_volume_target.return_value = self.volume_target_dict
55
+
56
+            target = objects.VolumeTarget.get(self.context, id)
57
+
58
+            mock_get_volume_target.assert_called_once_with(id)
59
+            self.assertIsInstance(target, objects.VolumeTarget)
60
+            self.assertEqual(self.context, target._context)
61
+
62
+    def test_get_by_uuid(self):
63
+        uuid = self.volume_target_dict['uuid']
64
+        with mock.patch.object(self.dbapi, 'get_volume_target_by_uuid',
65
+                               autospec=True) as mock_get_volume_target:
66
+            mock_get_volume_target.return_value = self.volume_target_dict
67
+
68
+            target = objects.VolumeTarget.get(self.context, uuid)
69
+
70
+            mock_get_volume_target.assert_called_once_with(uuid)
71
+            self.assertIsInstance(target, objects.VolumeTarget)
72
+            self.assertEqual(self.context, target._context)
73
+
74
+    def test_list(self):
75
+        with mock.patch.object(self.dbapi, 'get_volume_target_list',
76
+                               autospec=True) as mock_get_list:
77
+            mock_get_list.return_value = [self.volume_target_dict]
78
+            volume_targets = objects.VolumeTarget.list(
79
+                self.context, limit=4, sort_key='uuid', sort_dir='asc')
80
+
81
+            mock_get_list.assert_called_once_with(
82
+                limit=4, marker=None, sort_key='uuid', sort_dir='asc')
83
+            self.assertThat(volume_targets, HasLength(1))
84
+            self.assertIsInstance(volume_targets[0],
85
+                                  objects.VolumeTarget)
86
+            self.assertEqual(self.context, volume_targets[0]._context)
87
+
88
+    def test_list_none(self):
89
+        with mock.patch.object(self.dbapi, 'get_volume_target_list',
90
+                               autospec=True) as mock_get_list:
91
+            mock_get_list.return_value = []
92
+            volume_targets = objects.VolumeTarget.list(
93
+                self.context, limit=4, sort_key='uuid', sort_dir='asc')
94
+
95
+            mock_get_list.assert_called_once_with(
96
+                limit=4, marker=None, sort_key='uuid', sort_dir='asc')
97
+            self.assertEqual([], volume_targets)
98
+
99
+    def test_list_by_node_id(self):
100
+        with mock.patch.object(self.dbapi, 'get_volume_targets_by_node_id',
101
+                               autospec=True) as mock_get_list_by_node_id:
102
+            mock_get_list_by_node_id.return_value = [self.volume_target_dict]
103
+            node_id = self.volume_target_dict['node_id']
104
+            volume_targets = objects.VolumeTarget.list_by_node_id(
105
+                self.context, node_id, limit=10, sort_dir='desc')
106
+
107
+            mock_get_list_by_node_id.assert_called_once_with(
108
+                node_id, limit=10, marker=None, sort_key=None, sort_dir='desc')
109
+            self.assertThat(volume_targets, HasLength(1))
110
+            self.assertIsInstance(volume_targets[0], objects.VolumeTarget)
111
+            self.assertEqual(self.context, volume_targets[0]._context)
112
+
113
+    def test_create(self):
114
+        with mock.patch.object(self.dbapi, 'create_volume_target',
115
+                               autospec=True) as mock_db_create:
116
+            mock_db_create.return_value = self.volume_target_dict
117
+            new_target = objects.VolumeTarget(
118
+                self.context, **self.volume_target_dict)
119
+            new_target.create()
120
+
121
+            mock_db_create.assert_called_once_with(self.volume_target_dict)
122
+
123
+    def test_destroy(self):
124
+        uuid = self.volume_target_dict['uuid']
125
+        with mock.patch.object(self.dbapi, 'get_volume_target_by_uuid',
126
+                               autospec=True) as mock_get_volume_target:
127
+            mock_get_volume_target.return_value = self.volume_target_dict
128
+            with mock.patch.object(self.dbapi, 'destroy_volume_target',
129
+                                   autospec=True) as mock_db_destroy:
130
+                target = objects.VolumeTarget.get_by_uuid(self.context, uuid)
131
+                target.destroy()
132
+
133
+                mock_db_destroy.assert_called_once_with(uuid)
134
+
135
+    def test_save(self):
136
+        uuid = self.volume_target_dict['uuid']
137
+        boot_index = 100
138
+        test_time = datetime.datetime(2000, 1, 1, 0, 0)
139
+        with mock.patch.object(self.dbapi, 'get_volume_target_by_uuid',
140
+                               autospec=True) as mock_get_volume_target:
141
+            mock_get_volume_target.return_value = self.volume_target_dict
142
+            with mock.patch.object(self.dbapi, 'update_volume_target',
143
+                                   autospec=True) as mock_update_target:
144
+                mock_update_target.return_value = (
145
+                    utils.get_test_volume_target(boot_index=boot_index,
146
+                                                 updated_at=test_time))
147
+                target = objects.VolumeTarget.get_by_uuid(self.context, uuid)
148
+                target.boot_index = boot_index
149
+                target.save()
150
+
151
+                mock_get_volume_target.assert_called_once_with(uuid)
152
+                mock_update_target.assert_called_once_with(uuid,
153
+                                                           {'boot_index':
154
+                                                            boot_index})
155
+                self.assertEqual(self.context, target._context)
156
+                res_updated_at = (target.updated_at).replace(tzinfo=None)
157
+                self.assertEqual(test_time, res_updated_at)
158
+
159
+    def test_refresh(self):
160
+        uuid = self.volume_target_dict['uuid']
161
+        old_boot_index = self.volume_target_dict['boot_index']
162
+        returns = [self.volume_target_dict,
163
+                   utils.get_test_volume_target(boot_index=100)]
164
+        expected = [mock.call(uuid), mock.call(uuid)]
165
+        with mock.patch.object(self.dbapi, 'get_volume_target_by_uuid',
166
+                               side_effect=returns,
167
+                               autospec=True) as mock_get_volume_target:
168
+            target = objects.VolumeTarget.get_by_uuid(self.context, uuid)
169
+            self.assertEqual(old_boot_index, target.boot_index)
170
+            target.refresh()
171
+            self.assertEqual(100, target.boot_index)
172
+
173
+            self.assertEqual(expected,
174
+                             mock_get_volume_target.call_args_list)
175
+            self.assertEqual(self.context, target._context)

Loading…
Cancel
Save