class ServerGroup(base.MoganObject, object_base.VersionedObjectDictCompat): # Version 1.0: Initial version VERSION = '1.0' dbapi = dbapi.get_instance() fields = { 'id': object_fields.IntegerField(), 'user_id': object_fields.StringField(nullable=True), 'project_id': object_fields.StringField(nullable=True), 'uuid': object_fields.UUIDField(), 'name': object_fields.StringField(nullable=True), 'policies': object_fields.ListOfStringsField(nullable=True), 'members': object_fields.ListOfStringsField(nullable=True), } @staticmethod def _from_db_object(context, server_group, db_server): """Method to help with migration to objects. Converts a database entity to a formal object. """ for field in server_group.fields: server_group[field] = db_server[field] server_group._context = context server_group.obj_reset_changes() return server_group def create(self): values = self.obj_get_changes() policies = values.pop('policies', None) members = values.pop('members', None) db_group = self.dbapi.server_group_create(self._context, values, policies=policies, members=members) self._from_db_object(self._context, self, db_group) @classmethod def get_by_uuid(cls, context, uuid): db_group = cls.dbapi.server_group_get(context, uuid) return cls._from_db_object(context, cls(), db_group) def destroy(self): self.dbapi.server_group_delete(self._context, self.uuid) self.obj_reset_changes() @classmethod def add_members(cls, context, group_uuid, members): cls.dbapi.server_group_members_add(context, group_uuid, members) def save(self, context=None): updates = self.obj_get_changes() self.dbapi.server_group_update(context, self.uuid, updates) self.obj_reset_changes()
class NotificationPublisher(NotificationObject): # Version 1.0: Initial version VERSION = '1.0' fields = { 'host': fields.StringField(nullable=False), 'binary': fields.StringField(nullable=False), } @classmethod def from_service_obj(cls, service): return cls(host=service.host, binary=service.binary)
class KeyPair(base.MoganObject): # Version 1.0: Initial version VERSION = '1.0' dbapi = dbapi.get_instance() fields = { 'id': fields.IntegerField(), 'name': fields.StringField(nullable=False), 'user_id': fields.StringField(nullable=True), 'fingerprint': fields.StringField(nullable=True), 'public_key': fields.StringField(nullable=True), 'type': fields.StringField(nullable=False), } @staticmethod def _from_db_object(context, keypair, db_keypair): ignore = {'deleted': False, 'deleted_at': None} for key in keypair.fields: if key in ignore and not hasattr(db_keypair, key): setattr(keypair, key, ignore[key]) else: setattr(keypair, key, db_keypair[key]) keypair._context = context keypair.obj_reset_changes() return keypair @classmethod def get_by_name(cls, context, user_id, name): db_keypair = cls.dbapi.key_pair_get(context, user_id, name) return cls._from_db_object(context, cls(), db_keypair) @classmethod def destroy_by_name(cls, context, user_id, name): cls.dbapi.key_pair_destroy(context, user_id, name) def create(self): if self.obj_attr_is_set('id'): raise exception.ObjectActionError(action='create', reason='already created') try: self.dbapi.key_pair_get(self._context, self.user_id, self.name) raise exception.KeypairExists(key_name=self.name) except exception.KeypairNotFound: pass updates = self.obj_get_changes() db_keypair = self.dbapi.key_pair_create(self._context, updates) self._from_db_object(self._context, self, db_keypair) def destroy(self): self.dbapi.key_pair_destroy(self._context, self.user_id, self.name)
class ServerFault(base.MoganObject, object_base.VersionedObjectDictCompat): # Version 1.0: Initial version VERSION = '1.0' dbapi = dbapi.get_instance() fields = { 'id': object_fields.IntegerField(), 'server_uuid': object_fields.UUIDField(), 'code': object_fields.IntegerField(), 'message': object_fields.StringField(nullable=True), 'detail': object_fields.StringField(nullable=True), } def return_dict(self): return dict((k, getattr(self, k)) for k in ['code', 'message', 'detail'] if hasattr(self, k)) @staticmethod def _from_db_object(context, fault, db_fault): for key in fault.fields: fault[key] = db_fault[key] fault._context = context fault.obj_reset_changes() return fault @classmethod def get_latest_for_server(cls, context, server_uuid): db_faults = cls.dbapi.server_fault_get_by_server_uuids( context, [server_uuid]) if server_uuid in db_faults and db_faults[server_uuid]: return cls._from_db_object(context, cls(), db_faults[server_uuid][0]) def create(self): if self.obj_attr_is_set('id'): raise exception.ObjectActionError(action='create', reason='already created') values = { 'server_uuid': self.server_uuid, 'code': self.code, 'message': self.message, 'detail': self.detail, } db_fault = self.dbapi.server_fault_create(self._context, values) self._from_db_object(self._context, self, db_fault) self.obj_reset_changes()
class MyObj(base.MoganObject, object_base.VersionedObjectDictCompat): VERSION = '1.1' fields = { 'foo': fields.IntegerField(), 'bar': fields.StringField(), 'missing': fields.StringField(), } def obj_load_attr(self, attrname): setattr(self, attrname, 'loaded!') @object_base.remotable_classmethod def query(cls, context): obj = cls(context) obj.foo = 1 obj.bar = 'bar' obj.obj_reset_changes() return obj @object_base.remotable def marco(self, context=None): return 'polo' @object_base.remotable def update_test(self, ctxt=None): if ctxt and ctxt.tenant == 'alternate': self.bar = 'alternate-context' else: self.bar = 'updated' @object_base.remotable def save(self, context=None): self.obj_reset_changes() @object_base.remotable def refresh(self, context=None): self.foo = 321 self.bar = 'refreshed' self.obj_reset_changes() @object_base.remotable def modify_save_modify(self, context=None): self.bar = 'meow' self.save() self.foo = 42
class ExceptionPayload(base.NotificationPayloadBase): # Version 1.0: Initial version VERSION = '1.0' fields = { 'module_name': fields.StringField(), 'function_name': fields.StringField(), 'exception': fields.StringField(), 'exception_message': fields.StringField() } @classmethod def from_exception(cls, fault): trace = inspect.trace()[-1] module = inspect.getmodule(trace[0]) module_name = module.__name__ if module else 'unknown' return cls(function_name=trace[3], module_name=module_name, exception=fault.__class__.__name__, exception_message=six.text_type(fault))
class ExceptionPayload(base.NotificationPayloadBase): # Version 1.0: Initial version VERSION = '1.0' fields = { 'module_name': fields.StringField(), 'function_name': fields.StringField(), 'exception': fields.StringField(), 'exception_message': fields.StringField() } @classmethod def from_exception(cls, fault): trace = inspect.trace()[-1] # TODO(gibi): apply strutils.mask_password on exception_message and # consider emitting the exception_message only if the safe flag is # true in the exception like in the REST API module = inspect.getmodule(trace[0]) module_name = module.__name__ if module else 'unknown' return cls(function_name=trace[3], module_name=module_name, exception=fault.__class__.__name__, exception_message=six.text_type(fault))
class ServerNic(base.MoganObject, object_base.VersionedObjectDictCompat): # Version 1.0: Initial version VERSION = '1.0' dbapi = dbapi.get_instance() fields = { 'port_id': object_fields.UUIDField(nullable=False), 'server_uuid': object_fields.UUIDField(nullable=True), 'mac_address': object_fields.MACAddressField(nullable=True), 'network_id': object_fields.UUIDField(nullable=True), 'fixed_ips': object_fields.ListOfDictOfNullableStringsField(nullable=True), 'floating_ip': object_fields.StringField(nullable=True), 'preserve_on_delete': object_fields.BooleanField(), } @staticmethod def _from_db_object(context, obj, db_object, server_uuid=None): if server_uuid: db_object = copy.deepcopy(db_object) db_object.update(server_uuid=server_uuid) if not isinstance(db_object, dict): db_object_dict = db_object.as_dict() else: db_object_dict = db_object obj = ServerNic(context) obj.update(db_object_dict) obj.obj_reset_changes() return obj @classmethod def delete_by_port_id(cls, context, port_id): cls.dbapi.server_nic_delete(context, port_id) @classmethod def get_by_port_id(cls, context, port_id): return cls.dbapi.server_nic_get(context, port_id) def save(self, context): updates = self.obj_get_changes() self.dbapi.server_nic_update_or_create(context, self.port_id, updates) def create(self, context): values = self.obj_to_primitive()['mogan_object.data'] self.dbapi.server_nic_update_or_create(context, self.port_id, values) def delete(self, context): self.dbapi.server_nic_delete(context, self.port_id)
class EventType(NotificationObject): # Version 1.0: Initial version VERSION = '1.0' fields = { 'object': fields.StringField(nullable=False), 'action': fields.NotificationActionField(nullable=False), 'phase': fields.NotificationPhaseField(nullable=True), } def to_notification_event_type_field(self): """Serialize the object to the wire format.""" s = '%s.%s' % (self.object, self.action) if self.obj_attr_is_set('phase'): s += '.%s' % self.phase return s
class ServerPayload(base.NotificationPayloadBase): SCHEMA = { 'name': ('server', 'name'), 'uuid': ('server', 'uuid'), 'user_id': ('server', 'user_id'), 'project_id': ('server', 'project_id'), 'availability_zone': ('server', 'availability_zone'), 'image_uuid': ('server', 'image_uuid'), 'created_at': ('server', 'created_at'), 'launched_at': ('server', 'launched_at'), 'updated_at': ('server', 'updated_at'), 'status': ('server', 'status'), 'power_state': ('server', 'power_state'), 'flavor_uuid': ('server', 'flavor_uuid'), 'description': ('server', 'description') } # Version 1.0: Initial version VERSION = '1.0' fields = { 'name': fields.StringField(nullable=False), 'uuid': fields.UUIDField(nullable=False), 'user_id': fields.StringField(nullable=True), 'project_id': fields.StringField(nullable=True), 'description': fields.StringField(nullable=True), 'flavor_uuid': fields.UUIDField(nullable=False), 'image_uuid': fields.UUIDField(nullable=True), 'availability_zone': fields.StringField(nullable=True), 'power_state': fields.StringField(nullable=True), 'created_at': fields.DateTimeField(nullable=True), 'launched_at': fields.DateTimeField(nullable=True), 'updated_at': fields.DateTimeField(nullable=True), 'status': fields.StringField(nullable=True), # 'network_info' # 'extra' } def __init__(self, server, **kwargs): super(ServerPayload, self).__init__(**kwargs) self.populate_schema(server=server)
class Server(base.MoganObject, object_base.VersionedObjectDictCompat): # Version 1.0: Initial version VERSION = '1.0' dbapi = dbapi.get_instance() fields = { 'id': object_fields.IntegerField(), 'uuid': object_fields.UUIDField(nullable=True), 'name': object_fields.StringField(nullable=True), 'description': object_fields.StringField(nullable=True), 'project_id': object_fields.UUIDField(nullable=True), 'user_id': object_fields.UUIDField(nullable=True), 'status': object_fields.StringField(nullable=True), 'power_state': object_fields.StringField(nullable=True), 'flavor_uuid': object_fields.UUIDField(nullable=True), 'availability_zone': object_fields.StringField(nullable=True), 'image_uuid': object_fields.UUIDField(nullable=True), 'nics': object_fields.ObjectField('ServerNics', nullable=True), 'fault': object_fields.ObjectField('ServerFault', nullable=True), 'node_uuid': object_fields.UUIDField(nullable=True), 'launched_at': object_fields.DateTimeField(nullable=True), 'metadata': object_fields.FlexibleDictField(nullable=True), 'locked': object_fields.BooleanField(default=False), 'locked_by': object_fields.StringField(nullable=True), } def __init__(self, context=None, **kwargs): server_nics = kwargs.pop('nics', None) if server_nics and isinstance(server_nics, list): nics_obj = objects.ServerNics(context) for nic in server_nics: nic_obj = objects.ServerNic(context, server_uuid=kwargs['uuid'], **nic) nics_obj.objects.append(nic_obj) kwargs['nics'] = nics_obj super(Server, self).__init__(context=context, **kwargs) @staticmethod def _from_db_object(server, db_server, expected_attrs=None): """Method to help with migration to objects. Converts a database entity to a formal object. :param server: An object of the Server class. :param db_server: A DB Server model of the object :return: The object of the class with the database entity added """ for field in set(server.fields) - set(OPTIONAL_ATTRS): if field == 'metadata': server[field] = db_server['extra'] else: server[field] = db_server[field] if expected_attrs is None: expected_attrs = [] if 'nics' in expected_attrs: server._load_server_nics(server._context, server.uuid) else: server.nics = None if 'fault' in expected_attrs: server._load_fault(server._context, server.uuid) server.obj_reset_changes() return server def _load_server_nics(self, context, server_uuid): self.nics = objects.ServerNics.get_by_server_uuid( context=context, server_uuid=server_uuid) @staticmethod def _from_db_object_list(db_objects, cls, context): """Converts a list of database entities to a list of formal objects.""" servers = [] for obj in db_objects: expected_attrs = ['nics', 'fault'] servers.append( Server._from_db_object(cls(context), obj, expected_attrs)) return servers def _load_fault(self, context, server_uuid): self.fault = objects.ServerFault.get_latest_for_server( context=context, server_uuid=server_uuid) def _save_nics(self, context): for nic_obj in self.nics or []: nic_obj.save(context) def as_dict(self): data = dict(self.items()) if 'nics' in data: data.update(nics=data['nics'].as_list_of_dict()) return data @classmethod def list(cls, context, project_only=False, filters=None): """Return a list of Server objects.""" db_servers = cls.dbapi.server_get_all(context, project_only=project_only, filters=filters) return Server._from_db_object_list(db_servers, cls, context) @classmethod def get(cls, context, uuid): """Find a server and return a Server object.""" expected_attrs = ['nics', 'fault'] db_server = cls.dbapi.server_get(context, uuid) server = Server._from_db_object(cls(context), db_server, expected_attrs) return server def create(self, context=None): """Create a Server record in the DB.""" values = self.obj_get_changes() metadata = values.pop('metadata', None) if metadata is not None: values['extra'] = metadata server_nics = values.pop('nics', None) if server_nics: values['nics'] = server_nics.as_list_of_dict() db_server = self.dbapi.server_create(context, values) expected_attrs = None if server_nics: expected_attrs = ['nics'] self._from_db_object(self, db_server, expected_attrs) def destroy(self, context=None): """Delete the Server from the DB.""" self.dbapi.server_destroy(context, self.uuid) self.obj_reset_changes() def save(self, context=None): """Save updates to this Server.""" updates = self.obj_get_changes() for field in list(updates): if (self.obj_attr_is_set(field) and isinstance( self.fields[field], object_fields.ObjectField) and getattr(self, field, None) is not None): try: getattr(self, '_save_%s' % field)(context) except AttributeError: LOG.exception('No save handler for %s', field, server=self) except db_exc.DBReferenceError as exp: if exp.key != 'server_uuid': raise updates.pop(field) metadata = updates.pop('metadata', None) if metadata is not None: updates['extra'] = metadata self.dbapi.server_update(context, self.uuid, updates) self.obj_reset_changes() def refresh(self, context=None): """Refresh the object by re-fetching from the DB.""" current = self.__class__.get(context, self.uuid) self.obj_refresh(current) self.obj_reset_changes()
class Flavor(base.MoganObject, object_base.VersionedObjectDictCompat): # Version 1.0: Initial version VERSION = '1.0' dbapi = dbapi.get_instance() fields = { 'uuid': object_fields.UUIDField(nullable=True), 'name': object_fields.StringField(nullable=True), 'description': object_fields.StringField(nullable=True), 'is_public': object_fields.BooleanField(), 'disabled': object_fields.BooleanField(), 'resources': object_fields.FlexibleDictField(nullable=True), 'resource_traits': object_fields.FlexibleDictField(nullable=True), 'projects': object_fields.ListOfStringsField(), } def __init__(self, *args, **kwargs): super(Flavor, self).__init__(*args, **kwargs) self._orig_projects = {} @staticmethod def _from_db_object(context, flavor, db_flavor, expected_attrs=None): if expected_attrs is None: expected_attrs = [] for name, field in flavor.fields.items(): if name in OPTIONAL_FIELDS: continue value = db_flavor[name] if isinstance(field, object_fields.IntegerField): value = value if value is not None else 0 flavor[name] = value if 'projects' in expected_attrs: flavor._load_projects(context) flavor.obj_reset_changes() return flavor def _load_projects(self, context): self.projects = [x['project_id'] for x in self.dbapi.flavor_access_get(context, self.uuid)] self.obj_reset_changes(['projects']) def obj_reset_changes(self, fields=None, recursive=False): super(Flavor, self).obj_reset_changes(fields=fields, recursive=recursive) if fields is None or 'projects' in fields: self._orig_projects = (list(self.projects) if self.obj_attr_is_set('projects') else []) def obj_what_changed(self): changes = super(Flavor, self).obj_what_changed() if 'projects' in self and self.projects != self._orig_projects: changes.add('projects') return changes @staticmethod def _from_db_object_list(db_objects, cls, context): """Converts a list of database entities to a list of formal objects.""" return [Flavor._from_db_object(context, cls(context), obj) for obj in db_objects] @classmethod def list(cls, context): """Return a list of Flavor objects.""" db_flavors = cls.dbapi.flavor_get_all(context) return Flavor._from_db_object_list(db_flavors, cls, context) @classmethod def get(cls, context, flavor_uuid): """Find a Flavor and return a Flavor object.""" db_flavor = cls.dbapi.flavor_get(context, flavor_uuid) flavor = Flavor._from_db_object( context, cls(context), db_flavor, expected_attrs=['projects']) return flavor def create(self, context=None): """Create a Flavor record in the DB.""" values = self.obj_get_changes() db_flavor = self.dbapi.flavor_create(context, values) self._from_db_object(context, self, db_flavor) def destroy(self, context=None): """Delete the Flavor from the DB.""" self.dbapi.flavor_destroy(context, self.uuid) self.obj_reset_changes() def save(self, context=None): updates = self.obj_get_changes() projects = updates.pop('projects', None) # access projects if projects is not None: deleted_projects = set(self._orig_projects) - set(projects) added_projects = set(projects) - set(self._orig_projects) else: added_projects = deleted_projects = None if added_projects or deleted_projects: self.save_projects(context, added_projects, deleted_projects) self.dbapi.flavor_update(context, self.uuid, updates) def save_projects(self, context, to_add=None, to_delete=None): """Add or delete projects. :param:to_add: A list of projects to add :param:to_delete: A list of projects to remove """ ident = self.uuid to_add = to_add if to_add is not None else [] to_delete = to_delete if to_delete is not None else [] for project_id in to_add: self.dbapi.flavor_access_add(context, ident, project_id) for project_id in to_delete: self.dbapi.flavor_access_remove(context, ident, project_id) self.obj_reset_changes(['projects'])
class TestSubclassedObject(MyObj): fields = {'new_field': fields.StringField()}
class TestObj(base.MoganObject): fields = {'foo': fields.IntegerField(), 'bar': fields.StringField()}
class TestObj(base.MoganObject, object_base.VersionedObjectDictCompat): fields = {'foo': fields.IntegerField(), 'bar': fields.StringField()}
class Quota(base.MoganObject, object_base.VersionedObjectDictCompat): # Version 1.0: Initial version VERSION = '1.0' dbapi = dbapi.get_instance() fields = { 'id': object_fields.IntegerField(), 'project_id': object_fields.UUIDField(nullable=True), 'resource_name': object_fields.StringField(nullable=True), 'hard_limit': object_fields.IntegerField(nullable=True), 'allocated': object_fields.IntegerField(default=0), } def __init__(self, *args, **kwargs): super(Quota, self).__init__(*args, **kwargs) self.quota_driver = driver.DriverManager('mogan.quota.backend_driver', CONF.quota.quota_driver, invoke_on_load=True).driver self._resources = {} @property def resources(self): return self._resources @staticmethod def _from_db_object_list(db_objects, cls, context): """Converts a list of database entities to a list of formal objects.""" return [Quota._from_db_object(cls(context), obj) for obj in db_objects] @classmethod def list(cls, context, project_only=False): """Return a list of Quota objects.""" db_quotas = cls.dbapi.quota_get_all(context, project_only=project_only) return Quota._from_db_object_list(db_quotas, cls, context) @classmethod def get(cls, context, project_id, resource_name): """Find a quota of resource and return a Quota object.""" db_quota = cls.dbapi.quota_get(context, project_id, resource_name) quota = Quota._from_db_object(cls(context), db_quota) return quota def create(self, context): """Create a Quota record in the DB.""" values = self.obj_get_changes() # Since we need to avoid passing False down to the DB layer # (which uses an integer), we can always default it to zero here. values['deleted'] = 0 db_quota = self.dbapi.quota_create(context, values) self._from_db_object(self, db_quota) def destroy(self, context, project_id, resource_name): """Delete the Quota from the DB.""" self.dbapi.quota_destroy(context, project_id, resource_name) self.obj_reset_changes() def save(self, context, project_id, resource_name): """Save updates to this Quota.""" updates = self.obj_get_changes() self.dbapi.quota_update(context, project_id, resource_name, updates) self.obj_reset_changes() def refresh(self, context, project_id, resource_name): """Refresh the object by re-fetching from the DB.""" current = self.__class__.get(context, project_id, resource_name) self.obj_refresh(current) self.obj_reset_changes() def reserve(self, context, expire=None, project_id=None, **deltas): """reserve the Quota.""" return self.quota_driver.reserve(context, self.resources, deltas, expire=expire, project_id=project_id) def commit(self, context, reservations, project_id=None): self.quota_driver.commit(context, reservations, project_id=project_id) def rollback(self, context, reservations, project_id=None): self.quota_driver.rollback(context, reservations, project_id=project_id) def expire(self, context): return self.quota_driver.expire(context) def count(self, context, resource, *args, **kwargs): """Count a resource. For countable resources, invokes the count() function and returns its result. Arguments following the context and resource are passed directly to the count function declared by the resource. :param context: The request context, for access checks. :param resource: The name of the resource, as a string. """ # Get the resource res = self.resources.get(resource) if not res or not hasattr(res, 'count'): raise exception.QuotaResourceUnknown(unknown=[resource]) return res.count(context, *args, **kwargs) def register_resource(self, resource): """Register a resource.""" self._resources[resource.name] = resource def register_resources(self, resources): """Register a list of resources.""" for resource in resources: self.register_resource(resource) def get_quota_limit_and_usage(self, context, resources, project_id): return self.quota_driver.get_project_quotas(context, resources, project_id, usages=True)
class Aggregate(base.MoganObject, object_base.VersionedObjectDictCompat): # Version 1.0: Initial version VERSION = '1.0' dbapi = dbapi.get_instance() fields = { 'id': object_fields.IntegerField(read_only=True), 'uuid': object_fields.UUIDField(read_only=True), 'name': object_fields.StringField(), 'metadata': object_fields.FlexibleDictField(nullable=True), } def __init__(self, *args, **kwargs): super(Aggregate, self).__init__(*args, **kwargs) self._orig_metadata = {} @staticmethod def _from_db_object(context, aggregate, db_aggregate): """Converts a database entity to a formal object.""" for field in aggregate.fields: if field == 'metadata': aggregate[field] = db_aggregate['metadetails'] else: aggregate[field] = db_aggregate[field] aggregate.obj_reset_changes() return aggregate def obj_reset_changes(self, fields=None, recursive=False): super(Aggregate, self).obj_reset_changes(fields=fields, recursive=recursive) if fields is None or 'metadata' in fields: self._orig_metadata = (dict(self.metadata) if self.obj_attr_is_set('metadata') else {}) def obj_what_changed(self): changes = super(Aggregate, self).obj_what_changed() if ('metadata' in self and self.metadata != self._orig_metadata): changes.add('metadata') return changes @classmethod def get(cls, context, aggregate_id): """Find an aggregate and return an Aggregate object.""" db_aggregate = cls.dbapi.aggregate_get(context, aggregate_id) aggregate = cls._from_db_object(context, cls(context), db_aggregate) return aggregate def create(self, context=None): """Create an Aggregate record in the DB.""" values = self.obj_get_changes() db_aggregate = self.dbapi.aggregate_create(context, values) self._from_db_object(context, self, db_aggregate) def destroy(self, context=None): """Delete the Aggregate from the DB.""" self.dbapi.aggregate_destroy(context, self.id) self.obj_reset_changes() def save(self, context=None): """Save updates to this Aggregate.""" updates = self.obj_get_changes() metadata = updates.pop('metadata', None) # metadata if metadata is not None: deleted_keys = (set(self._orig_metadata.keys()) - set(metadata.keys())) added_keys = self.metadata else: added_keys = deleted_keys = None if added_keys or deleted_keys: self.save_metadata(context, self.metadata, deleted_keys) self.dbapi.aggregate_update(context, self.id, updates) self.obj_reset_changes() def save_metadata(self, context, to_add=None, to_delete=None): """Add or delete metadata. :param:to_add: A dict of new keys to add/update :param:to_delete: A list of keys to remove """ ident = self.id to_add = to_add if to_add is not None else {} to_delete = to_delete if to_delete is not None else [] if to_add: self.dbapi.aggregate_metadata_update_or_create( context, ident, to_add) for key in to_delete: self.dbapi.aggregate_metadata_delete(context, ident, key) self.obj_reset_changes(['metadata'])