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