class Cluster(base.CinderPersistentObject, base.CinderObject, base.CinderComparableObject): """Cluster Versioned Object. Method get_by_id supports as additional named arguments: - get_services: If we want to load all services from this cluster. - services_summary: If we want to load num_nodes and num_down_nodes fields. - is_up: Boolean value to filter based on the cluster's up status. - read_deleted: Filtering based on delete status. Default value "no". - Any other cluster field will be used as a filter. """ # Version 1.0: Initial version # Version 1.1: Add replication fields VERSION = '1.1' OPTIONAL_FIELDS = ('num_hosts', 'num_down_hosts', 'services') # NOTE(geguileo): We don't want to expose race_preventer field at the OVO # layer since it is only meant for the DB layer internal mechanism to # prevent races. fields = { 'id': fields.IntegerField(), 'name': fields.StringField(nullable=False), 'binary': fields.StringField(nullable=False), 'disabled': fields.BooleanField(default=False, nullable=True), 'disabled_reason': fields.StringField(nullable=True), 'num_hosts': fields.IntegerField(default=0, read_only=True), 'num_down_hosts': fields.IntegerField(default=0, read_only=True), 'last_heartbeat': fields.DateTimeField(nullable=True, read_only=True), 'services': fields.ObjectField('ServiceList', nullable=True, read_only=True), # Replication properties 'replication_status': c_fields.ReplicationStatusField(nullable=True), 'frozen': fields.BooleanField(default=False), 'active_backend_id': fields.StringField(nullable=True), } def obj_make_compatible(self, primitive, target_version): """Make a cluster representation compatible with a target version.""" # Convert all related objects super(Cluster, self).obj_make_compatible(primitive, target_version) # Before v1.1 we didn't have relication fields so we have to remove # them. if target_version == '1.0': for obj_field in ('replication_status', 'frozen', 'active_backend_id'): primitive.pop(obj_field, None) @classmethod def _get_expected_attrs(cls, context, *args, **kwargs): """Return expected attributes when getting a cluster. Expected attributes depend on whether we are retrieving all related services as well as if we are getting the services summary. """ expected_attrs = [] if kwargs.get('get_services'): expected_attrs.append('services') if kwargs.get('services_summary'): expected_attrs.extend(('num_hosts', 'num_down_hosts')) return expected_attrs @staticmethod def _from_db_object(context, cluster, db_cluster, expected_attrs=None): """Fill cluster OVO fields from cluster ORM instance.""" expected_attrs = expected_attrs or tuple() for name, field in cluster.fields.items(): # The only field that cannot be assigned using setattr is services, # because it is an ObjectField. So we don't assign the value if # it's a non expected optional field or if it's services field. if ((name in Cluster.OPTIONAL_FIELDS and name not in expected_attrs) or name == 'services'): continue value = getattr(db_cluster, name) setattr(cluster, name, value) cluster._context = context if 'services' in expected_attrs: cluster.services = base.obj_make_list( context, objects.ServiceList(context), objects.Service, db_cluster.services) cluster.obj_reset_changes() return cluster def obj_load_attr(self, attrname): """Lazy load services attribute.""" # NOTE(geguileo): We only allow lazy loading services to raise # awareness of the high cost of lazy loading num_hosts and # num_down_hosts, so if we are going to need this information we should # be certain we really need it and it should loaded when retrieving the # data from the DB the first time we read the OVO. if attrname != 'services': raise exception.ObjectActionError( action='obj_load_attr', reason=_('attribute %s not lazy-loadable') % attrname) if not self._context: raise exception.OrphanedObjectError(method='obj_load_attr', objtype=self.obj_name()) self.services = objects.ServiceList.get_all( self._context, {'cluster_name': self.name}) self.obj_reset_changes(fields=('services',)) def create(self): if self.obj_attr_is_set('id'): raise exception.ObjectActionError(action='create', reason=_('already created')) updates = self.cinder_obj_get_changes() if updates: for field in self.OPTIONAL_FIELDS: if field in updates: raise exception.ObjectActionError( action='create', reason=_('%s assigned') % field) db_cluster = db.cluster_create(self._context, updates) self._from_db_object(self._context, self, db_cluster) def save(self): updates = self.cinder_obj_get_changes() if updates: for field in self.OPTIONAL_FIELDS: if field in updates: raise exception.ObjectActionError( action='save', reason=_('%s changed') % field) db.cluster_update(self._context, self.id, updates) self.obj_reset_changes() def destroy(self): with self.obj_as_admin(): updated_values = db.cluster_destroy(self._context, self.id) for field, value in updated_values.items(): setattr(self, field, value) self.obj_reset_changes(updated_values.keys()) @property def is_up(self): return (self.last_heartbeat and self.last_heartbeat >= utils.service_expired_time(True)) def reset_service_replication(self): """Reset service replication flags on promotion. When an admin promotes a cluster, each service member requires an update to maintain database consistency. """ actions = { 'replication_status': 'enabled', 'active_backend_id': None, } expectations = { 'cluster_name': self.name, } db.conditional_update(self._context, objects.Service.model, actions, expectations)
class Group(base.CinderPersistentObject, base.CinderObject, base.CinderObjectDictCompat, base.ClusteredObject): # Version 1.0: Initial version # Version 1.1: Added group_snapshots, group_snapshot_id, and # source_group_id # Version 1.2: Added replication_status VERSION = '1.2' OPTIONAL_FIELDS = ['volumes', 'volume_types', 'group_snapshots'] fields = { 'id': fields.UUIDField(), 'user_id': fields.StringField(), 'project_id': fields.StringField(), 'cluster_name': fields.StringField(nullable=True), 'host': fields.StringField(nullable=True), 'availability_zone': fields.StringField(nullable=True), 'name': fields.StringField(nullable=True), 'description': fields.StringField(nullable=True), 'group_type_id': fields.StringField(), 'volume_type_ids': fields.ListOfStringsField(nullable=True), 'status': c_fields.GroupStatusField(nullable=True), 'group_snapshot_id': fields.UUIDField(nullable=True), 'source_group_id': fields.UUIDField(nullable=True), 'replication_status': c_fields.ReplicationStatusField(nullable=True), 'volumes': fields.ObjectField('VolumeList', nullable=True), 'volume_types': fields.ObjectField('VolumeTypeList', nullable=True), 'group_snapshots': fields.ObjectField('GroupSnapshotList', nullable=True), } def obj_make_compatible(self, primitive, target_version): """Make an object representation compatible with target version.""" super(Group, self).obj_make_compatible(primitive, target_version) target_version = versionutils.convert_version_to_tuple(target_version) if target_version < (1, 1): for key in ('group_snapshot_id', 'source_group_id', 'group_snapshots'): primitive.pop(key, None) if target_version < (1, 2): primitive.pop('replication_status', None) @staticmethod def _from_db_object(context, group, db_group, expected_attrs=None): if expected_attrs is None: expected_attrs = [] for name, field in group.fields.items(): if name in Group.OPTIONAL_FIELDS: continue value = db_group.get(name) setattr(group, name, value) if 'volumes' in expected_attrs: volumes = base.obj_make_list(context, objects.VolumeList(context), objects.Volume, db_group['volumes']) group.volumes = volumes if 'volume_types' in expected_attrs: volume_types = base.obj_make_list(context, objects.VolumeTypeList(context), objects.VolumeType, db_group['volume_types']) group.volume_types = volume_types if 'group_snapshots' in expected_attrs: group_snapshots = base.obj_make_list( context, objects.GroupSnapshotList(context), objects.GroupSnapshot, db_group['group_snapshots']) group.group_snapshots = group_snapshots group._context = context group.obj_reset_changes() return group def create(self, group_snapshot_id=None, source_group_id=None): if self.obj_attr_is_set('id'): raise exception.ObjectActionError(action='create', reason=_('already_created')) updates = self.cinder_obj_get_changes() if 'volume_types' in updates: raise exception.ObjectActionError( action='create', reason=_('volume_types assigned')) if 'volumes' in updates: raise exception.ObjectActionError(action='create', reason=_('volumes assigned')) if 'group_snapshots' in updates: raise exception.ObjectActionError( action='create', reason=_('group_snapshots assigned')) db_groups = db.group_create(self._context, updates, group_snapshot_id, source_group_id) self._from_db_object(self._context, self, db_groups) def obj_load_attr(self, attrname): if attrname not in Group.OPTIONAL_FIELDS: raise exception.ObjectActionError( action='obj_load_attr', reason=_('attribute %s not lazy-loadable') % attrname) if not self._context: raise exception.OrphanedObjectError(method='obj_load_attr', objtype=self.obj_name()) if attrname == 'volume_types': self.volume_types = objects.VolumeTypeList.get_all_by_group( self._context, self.id) if attrname == 'volumes': self.volumes = objects.VolumeList.get_all_by_generic_group( self._context, self.id) if attrname == 'group_snapshots': self.group_snapshots = objects.GroupSnapshotList.get_all_by_group( self._context, self.id) self.obj_reset_changes(fields=[attrname]) def save(self): updates = self.cinder_obj_get_changes() if updates: if 'volume_types' in updates: msg = _('Cannot save volume_types changes in group object ' 'update.') raise exception.ObjectActionError(action='save', reason=msg) if 'volumes' in updates: msg = _('Cannot save volumes changes in group object update.') raise exception.ObjectActionError(action='save', reason=msg) if 'group_snapshots' in updates: msg = _('Cannot save group_snapshots changes in group object ' 'update.') raise exception.ObjectActionError(action='save', reason=msg) db.group_update(self._context, self.id, updates) self.obj_reset_changes() def destroy(self): with self.obj_as_admin(): db.group_destroy(self._context, self.id)
class Service(base.CinderPersistentObject, base.CinderObject, base.CinderObjectDictCompat, base.CinderComparableObject, base.ClusteredObject): # Version 1.0: Initial version # Version 1.1: Add rpc_current_version and object_current_version fields # Version 1.2: Add get_minimum_rpc_version() and get_minimum_obj_version() # Version 1.3: Add replication fields # Version 1.4: Add cluster fields VERSION = '1.4' OPTIONAL_FIELDS = ('cluster', ) fields = { 'id': fields.IntegerField(), 'host': fields.StringField(nullable=True), 'binary': fields.StringField(nullable=True), 'cluster_name': fields.StringField(nullable=True), 'cluster': fields.ObjectField('Cluster', nullable=True, read_only=True), 'topic': fields.StringField(nullable=True), 'report_count': fields.IntegerField(default=0), 'disabled': fields.BooleanField(default=False, nullable=True), 'availability_zone': fields.StringField(nullable=True, default='cinder'), 'disabled_reason': fields.StringField(nullable=True), 'modified_at': fields.DateTimeField(nullable=True), 'rpc_current_version': fields.StringField(nullable=True), 'object_current_version': fields.StringField(nullable=True), # Replication properties 'replication_status': c_fields.ReplicationStatusField(nullable=True), 'frozen': fields.BooleanField(default=False), 'active_backend_id': fields.StringField(nullable=True), } def obj_make_compatible(self, primitive, target_version): """Make a service representation compatible with a target version.""" # Convert all related objects super(Service, self).obj_make_compatible(primitive, target_version) target_version = versionutils.convert_version_to_tuple(target_version) # Before v1.4 we didn't have cluster fields so we have to remove them. if target_version < (1, 4): for obj_field in ('cluster', 'cluster_name'): primitive.pop(obj_field, None) @staticmethod def _from_db_object(context, service, db_service, expected_attrs=None): expected_attrs = expected_attrs or [] for name, field in service.fields.items(): if name in Service.OPTIONAL_FIELDS: continue value = db_service.get(name) if isinstance(field, fields.IntegerField): value = value or 0 elif isinstance(field, fields.DateTimeField): value = value or None service[name] = value service._context = context if 'cluster' in expected_attrs: db_cluster = db_service.get('cluster') # If this service doesn't belong to a cluster the cluster field in # the ORM instance will have value of None. if db_cluster: service.cluster = objects.Cluster(context) objects.Cluster._from_db_object(context, service.cluster, db_cluster) else: service.cluster = None service.obj_reset_changes() return service def obj_load_attr(self, attrname): if attrname not in self.OPTIONAL_FIELDS: raise exception.ObjectActionError( action='obj_load_attr', reason=_('attribute %s not lazy-loadable') % attrname) if not self._context: raise exception.OrphanedObjectError(method='obj_load_attr', objtype=self.obj_name()) # NOTE(geguileo): We only have 1 optional field, so we don't need to # confirm that we are loading the cluster. # If this service doesn't belong to a cluster (cluster_name is empty), # then cluster field will be None. if self.cluster_name: self.cluster = objects.Cluster.get_by_id(self._context, name=self.cluster_name) else: self.cluster = None self.obj_reset_changes(fields=(attrname, )) @classmethod def get_by_host_and_topic(cls, context, host, topic): db_service = db.service_get(context, disabled=False, host=host, topic=topic) return cls._from_db_object(context, cls(context), db_service) @classmethod def get_by_args(cls, context, host, binary_key): db_service = db.service_get(context, host=host, binary=binary_key) return cls._from_db_object(context, cls(context), db_service) def create(self): if self.obj_attr_is_set('id'): raise exception.ObjectActionError(action='create', reason=_('already created')) updates = self.cinder_obj_get_changes() if 'cluster' in updates: raise exception.ObjectActionError(action='create', reason=_('cluster assigned')) db_service = db.service_create(self._context, updates) self._from_db_object(self._context, self, db_service) def save(self): updates = self.cinder_obj_get_changes() if 'cluster' in updates: raise exception.ObjectActionError(action='save', reason=_('cluster changed')) if updates: db.service_update(self._context, self.id, updates) self.obj_reset_changes() def destroy(self): with self.obj_as_admin(): updated_values = db.service_destroy(self._context, self.id) self.update(updated_values) self.obj_reset_changes(updated_values.keys()) @classmethod def _get_minimum_version(cls, attribute, context, binary): services = ServiceList.get_all_by_binary(context, binary) min_ver = None min_ver_str = None for s in services: ver_str = getattr(s, attribute) if ver_str is None: # NOTE(dulek) None in *_current_version means that this # service is in Liberty version, which we now don't provide # backward compatibility to. msg = _('One of the services is in Liberty version. We do not ' 'provide backward compatibility with Liberty now, you ' 'need to upgrade to Mitaka first.') raise exception.ServiceTooOld(msg) ver = versionutils.convert_version_to_int(ver_str) if min_ver is None or ver < min_ver: min_ver = ver min_ver_str = ver_str return min_ver_str @classmethod def get_minimum_rpc_version(cls, context, binary): return cls._get_minimum_version('rpc_current_version', context, binary) @classmethod def get_minimum_obj_version(cls, context, binary=None): return cls._get_minimum_version('object_current_version', context, binary)
class Service(base.CinderPersistentObject, base.CinderObject, base.CinderObjectDictCompat, base.CinderComparableObject, base.ClusteredObject): # Version 1.0: Initial version # Version 1.1: Add rpc_current_version and object_current_version fields # Version 1.2: Add get_minimum_rpc_version() and get_minimum_obj_version() # Version 1.3: Add replication fields # Version 1.4: Add cluster fields # Version 1.5: Add UUID field # Version 1.6: Modify UUID field to be not nullable VERSION = '1.6' OPTIONAL_FIELDS = ('cluster',) # NOTE: When adding a field obj_make_compatible needs to be updated fields = { 'id': fields.IntegerField(), 'host': fields.StringField(nullable=True), 'binary': fields.StringField(nullable=True), 'cluster_name': fields.StringField(nullable=True), 'cluster': fields.ObjectField('Cluster', nullable=True, read_only=True), 'topic': fields.StringField(nullable=True), 'report_count': fields.IntegerField(default=0), 'disabled': fields.BooleanField(default=False, nullable=True), 'availability_zone': fields.StringField(nullable=True, default='cinder'), 'disabled_reason': fields.StringField(nullable=True), 'modified_at': fields.DateTimeField(nullable=True), 'rpc_current_version': fields.StringField(nullable=True), 'object_current_version': fields.StringField(nullable=True), # Replication properties 'replication_status': c_fields.ReplicationStatusField(nullable=True), 'frozen': fields.BooleanField(default=False), 'active_backend_id': fields.StringField(nullable=True), 'uuid': fields.StringField(), } @staticmethod def _from_db_object(context, service, db_service, expected_attrs=None): expected_attrs = expected_attrs or [] for name, field in service.fields.items(): if ((name == 'uuid' and not db_service.get(name)) or name in service.OPTIONAL_FIELDS): continue value = db_service.get(name) if isinstance(field, fields.IntegerField): value = value or 0 elif isinstance(field, fields.DateTimeField): value = value or None service[name] = value service._context = context if 'cluster' in expected_attrs: db_cluster = db_service.get('cluster') # If this service doesn't belong to a cluster the cluster field in # the ORM instance will have value of None. if db_cluster: service.cluster = objects.Cluster(context) objects.Cluster._from_db_object(context, service.cluster, db_cluster) else: service.cluster = None service.obj_reset_changes() return service def obj_load_attr(self, attrname): if attrname not in self.OPTIONAL_FIELDS: raise exception.ObjectActionError( action='obj_load_attr', reason=_('attribute %s not lazy-loadable') % attrname) if not self._context: raise exception.OrphanedObjectError(method='obj_load_attr', objtype=self.obj_name()) # NOTE(geguileo): We only have 1 optional field, so we don't need to # confirm that we are loading the cluster. # If this service doesn't belong to a cluster (cluster_name is empty), # then cluster field will be None. if self.cluster_name: self.cluster = objects.Cluster.get_by_id(self._context, None, name=self.cluster_name) else: self.cluster = None self.obj_reset_changes(fields=(attrname,)) @classmethod def get_by_host_and_topic(cls, context, host, topic, disabled=False): db_service = db.service_get(context, disabled=disabled, host=host, topic=topic) return cls._from_db_object(context, cls(context), db_service) @classmethod def get_by_args(cls, context, host, binary_key): db_service = db.service_get(context, host=host, binary=binary_key) return cls._from_db_object(context, cls(context), db_service) @classmethod def get_by_uuid(cls, context, service_uuid): db_service = db.service_get_by_uuid(context, service_uuid) return cls._from_db_object(context, cls(), db_service) def create(self): if self.obj_attr_is_set('id'): raise exception.ObjectActionError(action='create', reason=_('already created')) updates = self.cinder_obj_get_changes() if 'cluster' in updates: raise exception.ObjectActionError( action='create', reason=_('cluster assigned')) if 'uuid' not in updates: updates['uuid'] = uuidutils.generate_uuid() self.uuid = updates['uuid'] db_service = db.service_create(self._context, updates) self._from_db_object(self._context, self, db_service) def save(self): updates = self.cinder_obj_get_changes() if 'cluster' in updates: raise exception.ObjectActionError( action='save', reason=_('cluster changed')) if updates: db.service_update(self._context, self.id, updates) self.obj_reset_changes() def destroy(self): with self.obj_as_admin(): updated_values = db.service_destroy(self._context, self.id) self.update(updated_values) self.obj_reset_changes(updated_values.keys()) @classmethod def _get_minimum_version(cls, attribute, context, binary): services = ServiceList.get_all_by_binary(context, binary) min_ver = None min_ver_str = None for s in services: ver_str = getattr(s, attribute) if ver_str is None: # NOTE(dulek) None in *_current_version means that this # service is in Liberty version, which we now don't provide # backward compatibility to. msg = _('Service %s is in Liberty version. We do not provide ' 'backward compatibility with Liberty now, so you ' 'need to upgrade it, release by release if live ' 'upgrade is required. After upgrade you may need to ' 'remove any stale service records via ' '"cinder-manage service remove".') % s.binary raise exception.ServiceTooOld(msg) ver = versionutils.convert_version_to_int(ver_str) if min_ver is None or ver < min_ver: min_ver = ver min_ver_str = ver_str return min_ver_str @classmethod def get_minimum_rpc_version(cls, context, binary): return cls._get_minimum_version('rpc_current_version', context, binary) @classmethod def get_minimum_obj_version(cls, context, binary=None): return cls._get_minimum_version('object_current_version', context, binary) @property def is_up(self): """Check whether a service is up based on last heartbeat.""" return (self.updated_at and self.updated_at >= utils.service_expired_time(True))
class Service(base.CinderPersistentObject, base.CinderObject, base.CinderObjectDictCompat, base.CinderComparableObject): # Version 1.0: Initial version # Version 1.1: Add rpc_current_version and object_current_version fields # Version 1.2: Add get_minimum_rpc_version() and get_minimum_obj_version() # Version 1.3: Add replication fields VERSION = '1.3' fields = { 'id': fields.IntegerField(), 'host': fields.StringField(nullable=True), 'binary': fields.StringField(nullable=True), 'topic': fields.StringField(nullable=True), 'report_count': fields.IntegerField(default=0), 'disabled': fields.BooleanField(default=False, nullable=True), 'availability_zone': fields.StringField(nullable=True, default='cinder'), 'disabled_reason': fields.StringField(nullable=True), 'modified_at': fields.DateTimeField(nullable=True), 'rpc_current_version': fields.StringField(nullable=True), 'object_current_version': fields.StringField(nullable=True), # Replication properties 'replication_status': c_fields.ReplicationStatusField(nullable=True), 'frozen': fields.BooleanField(default=False), 'active_backend_id': fields.StringField(nullable=True), } @staticmethod def _from_db_object(context, service, db_service): for name, field in service.fields.items(): value = db_service.get(name) if isinstance(field, fields.IntegerField): value = value or 0 elif isinstance(field, fields.DateTimeField): value = value or None service[name] = value service._context = context service.obj_reset_changes() return service @base.remotable_classmethod def get_by_host_and_topic(cls, context, host, topic): db_service = db.service_get_by_host_and_topic(context, host, topic) return cls._from_db_object(context, cls(context), db_service) @base.remotable_classmethod def get_by_args(cls, context, host, binary_key): db_service = db.service_get_by_args(context, host, binary_key) return cls._from_db_object(context, cls(context), db_service) @base.remotable def create(self): if self.obj_attr_is_set('id'): raise exception.ObjectActionError(action='create', reason=_('already created')) updates = self.cinder_obj_get_changes() db_service = db.service_create(self._context, updates) self._from_db_object(self._context, self, db_service) @base.remotable def save(self): updates = self.cinder_obj_get_changes() if updates: db.service_update(self._context, self.id, updates) self.obj_reset_changes() @base.remotable def destroy(self): with self.obj_as_admin(): db.service_destroy(self._context, self.id) @classmethod def _get_minimum_version(cls, attribute, context, binary): services = ServiceList.get_all_by_binary(context, binary) min_ver = None min_ver_str = None for s in services: ver_str = getattr(s, attribute) if ver_str is None: # FIXME(dulek) None in *_current_version means that this # service is in Liberty version, so we must assume this is the # lowest one. We use handy and easy to remember token to # indicate that. This may go away as soon as we drop # compatibility with Liberty, possibly in early N. return 'liberty' ver = versionutils.convert_version_to_int(ver_str) if min_ver is None or ver < min_ver: min_ver = ver min_ver_str = ver_str return min_ver_str @base.remotable_classmethod def get_minimum_rpc_version(cls, context, binary): return cls._get_minimum_version('rpc_current_version', context, binary) @base.remotable_classmethod def get_minimum_obj_version(cls, context, binary): return cls._get_minimum_version('object_current_version', context, binary)