1#    Copyright 2015 SimpliVity Corp.
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
15from oslo_serialization import jsonutils
16from oslo_utils import versionutils
17from oslo_versionedobjects import fields
18
19from cinder import db
20from cinder import exception
21from cinder.i18n import _
22from cinder import objects
23from cinder.objects import base
24from cinder.objects import fields as c_fields
25
26
27@base.CinderObjectRegistry.register
28class VolumeAttachment(base.CinderPersistentObject, base.CinderObject,
29                       base.CinderObjectDictCompat,
30                       base.CinderComparableObject):
31    # Version 1.0: Initial version
32    # Version 1.1: Added volume relationship
33    # Version 1.2: Added connection_info attribute
34    # Version 1.3: Added the connector attribute.
35    VERSION = '1.3'
36
37    OPTIONAL_FIELDS = ['volume']
38    obj_extra_fields = ['project_id', 'volume_host']
39
40    fields = {
41        'id': fields.UUIDField(),
42        'volume_id': fields.UUIDField(),
43        'instance_uuid': fields.UUIDField(nullable=True),
44        'attached_host': fields.StringField(nullable=True),
45        'mountpoint': fields.StringField(nullable=True),
46
47        'attach_time': fields.DateTimeField(nullable=True),
48        'detach_time': fields.DateTimeField(nullable=True),
49
50        'attach_status': c_fields.VolumeAttachStatusField(nullable=True),
51        'attach_mode': fields.StringField(nullable=True),
52
53        'volume': fields.ObjectField('Volume', nullable=False),
54        'connection_info': c_fields.DictOfNullableField(nullable=True),
55        'connector': c_fields.DictOfNullableField(nullable=True)
56    }
57
58    @property
59    def project_id(self):
60        return self.volume.project_id
61
62    @property
63    def volume_host(self):
64        return self.volume.host
65
66    @classmethod
67    def _get_expected_attrs(cls, context, *args, **kwargs):
68        return ['volume']
69
70    def obj_make_compatible(self, primitive, target_version):
71        """Make an object representation compatible with target version."""
72        super(VolumeAttachment, self).obj_make_compatible(primitive,
73                                                          target_version)
74        target_version = versionutils.convert_version_to_tuple(target_version)
75        if target_version < (1, 3):
76            primitive.pop('connector', None)
77        if target_version < (1, 2):
78            primitive.pop('connection_info', None)
79
80    @classmethod
81    def _from_db_object(cls, context, attachment, db_attachment,
82                        expected_attrs=None):
83        if expected_attrs is None:
84            expected_attrs = cls._get_expected_attrs(context)
85
86        for name, field in attachment.fields.items():
87            if name in cls.OPTIONAL_FIELDS:
88                continue
89            value = db_attachment.get(name)
90            if isinstance(field, fields.IntegerField):
91                value = value or 0
92            if name in ('connection_info', 'connector'):
93                # Both of these fields are nullable serialized json dicts.
94                setattr(attachment, name,
95                        jsonutils.loads(value) if value else None)
96            else:
97                attachment[name] = value
98        # NOTE: Check against the ORM instance's dictionary instead of using
99        # hasattr or get to avoid the lazy loading of the Volume on
100        # VolumeList.get_all.
101        # Getting a Volume loads its VolumeAttachmentList, which think they
102        # have the volume loaded, but they don't.  More detail on
103        # https://review.opendev.org/632549
104        # and its related bug report.
105        if 'volume' in expected_attrs and 'volume' in vars(db_attachment):
106            db_volume = db_attachment.volume
107            if db_volume:
108                attachment.volume = objects.Volume._from_db_object(
109                    context, objects.Volume(), db_volume)
110
111        attachment._context = context
112        attachment.obj_reset_changes()
113
114        # This is an online data migration which we should remove when enough
115        # time has passed and we have a blocker schema migration to check to
116        # make sure that the attachment_specs table is empty. Operators should
117        # run the "cinder-manage db online_data_migrations" CLI to force the
118        # migration on-demand.
119        connector = db.attachment_specs_get(context, attachment.id)
120        if connector:
121            # Update ourselves and delete the attachment_specs.
122            attachment.connector = connector
123            attachment.save()
124            # TODO(mriedem): Really need a delete-all method for this.
125            for spec_key in connector:
126                db.attachment_specs_delete(
127                    context, attachment.id, spec_key)
128
129        return attachment
130
131    def obj_load_attr(self, attrname):
132        if attrname not in self.OPTIONAL_FIELDS:
133            raise exception.ObjectActionError(
134                action='obj_load_attr',
135                reason=_('attribute %s not lazy-loadable') % attrname)
136        if not self._context:
137            raise exception.OrphanedObjectError(method='obj_load_attr',
138                                                objtype=self.obj_name())
139
140        if attrname == 'volume':
141            volume = objects.Volume.get_by_id(self._context, self.volume_id)
142            self.volume = volume
143
144        self.obj_reset_changes(fields=[attrname])
145
146    @staticmethod
147    def _convert_connection_info_to_db_format(updates):
148        properties = updates.pop('connection_info', None)
149        if properties is not None:
150            updates['connection_info'] = jsonutils.dumps(properties)
151
152    @staticmethod
153    def _convert_connector_to_db_format(updates):
154        connector = updates.pop('connector', None)
155        if connector is not None:
156            updates['connector'] = jsonutils.dumps(connector)
157
158    def save(self):
159        updates = self.cinder_obj_get_changes()
160        if updates:
161            if 'connection_info' in updates:
162                self._convert_connection_info_to_db_format(updates)
163            if 'connector' in updates:
164                self._convert_connector_to_db_format(updates)
165            if 'volume' in updates:
166                raise exception.ObjectActionError(action='save',
167                                                  reason=_('volume changed'))
168
169            db.volume_attachment_update(self._context, self.id, updates)
170            self.obj_reset_changes()
171
172    def finish_attach(self, instance_uuid, host_name,
173                      mount_point, attach_mode='rw'):
174        with self.obj_as_admin():
175            db_volume, updated_values = db.volume_attached(
176                self._context, self.id,
177                instance_uuid, host_name,
178                mount_point, attach_mode)
179        self.update(updated_values)
180        self.obj_reset_changes(updated_values.keys())
181        return objects.Volume._from_db_object(self._context,
182                                              objects.Volume(),
183                                              db_volume)
184
185    def create(self):
186        if self.obj_attr_is_set('id'):
187            raise exception.ObjectActionError(action='create',
188                                              reason=_('already created'))
189        updates = self.cinder_obj_get_changes()
190        if 'connector' in updates:
191            self._convert_connector_to_db_format(updates)
192        with self.obj_as_admin():
193            db_attachment = db.volume_attach(self._context, updates)
194        self._from_db_object(self._context, self, db_attachment)
195
196    def destroy(self):
197        updated_values = db.attachment_destroy(self._context, self.id)
198        self.update(updated_values)
199        self.obj_reset_changes(updated_values.keys())
200
201
202@base.CinderObjectRegistry.register
203class VolumeAttachmentList(base.ObjectListBase, base.CinderObject):
204    # Version 1.0: Initial version
205    # Version 1.1: Remove volume_id in get_by_host|instance
206    VERSION = '1.1'
207
208    fields = {
209        'objects': fields.ListOfObjectsField('VolumeAttachment'),
210    }
211
212    @classmethod
213    def get_all_by_volume_id(cls, context, volume_id):
214        attachments = db.volume_attachment_get_all_by_volume_id(context,
215                                                                volume_id)
216        return base.obj_make_list(context,
217                                  cls(context),
218                                  objects.VolumeAttachment,
219                                  attachments)
220
221    @classmethod
222    def get_all_by_host(cls, context, host, search_opts=None):
223        attachments = db.volume_attachment_get_all_by_host(context,
224                                                           host,
225                                                           search_opts)
226        return base.obj_make_list(context, cls(context),
227                                  objects.VolumeAttachment, attachments)
228
229    @classmethod
230    def get_all_by_instance_uuid(cls, context,
231                                 instance_uuid, search_opts=None):
232        attachments = db.volume_attachment_get_all_by_instance_uuid(
233            context, instance_uuid, search_opts)
234        return base.obj_make_list(context, cls(context),
235                                  objects.VolumeAttachment, attachments)
236
237    @classmethod
238    def get_all(cls, context, search_opts=None,
239                marker=None, limit=None, offset=None,
240                sort_keys=None, sort_direction=None):
241        attachments = db.volume_attachment_get_all(
242            context, search_opts, marker, limit, offset, sort_keys,
243            sort_direction)
244        return base.obj_make_list(context, cls(context),
245                                  objects.VolumeAttachment, attachments)
246
247    @classmethod
248    def get_all_by_project(cls, context, project_id, search_opts=None,
249                           marker=None, limit=None, offset=None,
250                           sort_keys=None, sort_direction=None):
251        attachments = db.volume_attachment_get_all_by_project(
252            context, project_id, search_opts, marker, limit, offset, sort_keys,
253            sort_direction)
254        return base.obj_make_list(context, cls(context),
255                                  objects.VolumeAttachment, attachments)
256