예제 #1
0
파일: cluster.py 프로젝트: lubidl0/cinder-1
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)
예제 #2
0
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)
예제 #3
0
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)
예제 #4
0
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))
예제 #5
0
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)