nova数据库模块的开发和使用
在nova.db这一块E\F\G\H这几个版本差异不大,但是从G版开始加入了conductor,不允许compute直接访问数据库,所以在compute的代码里调用数据库需要通过conductor。如果要增加一个新的功能,而且这个功能需要操作数据库,在操作数据库这个方面一般分为两个步骤:一、db模块中的内容编写,主要包括数据表的创建、功能及api的编写;二、compute模块中,对db提供的api调用方法的编写。
推荐优先学习sqlalchemy模块
openstack环境版本:H版(G版类似,F\E版的区别会有说明)
一、db模块中的内容编写
1.描述数据表
nova数据库的创建:
第一次同步nova数据库时的操作:
nova-manage db sync
这个指定的代码在nova.db.sqlalchemy.migration.py中:
def db_sync(version=None):
if version is not None:
try:
version = int(version)
except ValueError:
raise exception.NovaException(_("version should be an integer"))
current_version = db_version()
repository = _find_migrate_repo()
if version is None or version > current_version:
return versioning_api.upgrade(get_engine(), repository, version)
else:
return versioning_api.downgrade(get_engine(), repository,version)
因为version=None,所以走到upgrade里,它最后会找到nova.db.sqlalchemy.migrate_repo.versions.133_folsom.py这个文件里面的upgrade方法,代码很长,这里仅截取创建数据表相关的部分:
首先,写一个数据表的结构(以下这个就是nova.instances的表结构):
instances = Table('instances', meta, Column('created_at', DateTime), Column('updated_at', DateTime), Column('deleted_at', DateTime), Column('deleted', Boolean), Column('id', Integer, primary_key=True, nullable=False), Column('internal_id', Integer), Column('user_id', String(length=255)), Column('project_id', String(length=255)), Column('image_ref', String(length=255)), Column('kernel_id', String(length=255)), Column('ramdisk_id', String(length=255)), Column('server_name', String(length=255)), Column('launch_index', Integer), Column('key_name', String(length=255)), Column('key_data', MediumText()), Column('power_state', Integer), Column('vm_state', String(length=255)), Column('memory_mb', Integer), Column('vcpus', Integer), Column('hostname', String(length=255)), Column('host', String(length=255)), Column('user_data', MediumText()), Column('reservation_id', String(length=255)), Column('scheduled_at', DateTime), Column('launched_at', DateTime), Column('terminated_at', DateTime), Column('display_name', String(length=255)), Column('display_description', String(length=255)), Column('availability_zone', String(length=255)), Column('locked', Boolean), Column('os_type', String(length=255)), Column('launched_on', MediumText()), Column('instance_type_id', Integer), Column('vm_mode', String(length=255)), Column('uuid', String(length=36)), Column('architecture', String(length=255)), Column('root_device_name', String(length=255)), Column('access_ip_v4', String(length=255)), Column('access_ip_v6', String(length=255)), Column('config_drive', String(length=255)), Column('task_state', String(length=255)), Column('default_ephemeral_device', String(length=255)), Column('default_swap_device', String(length=255)), Column('progress', Integer), Column('auto_disk_config', Boolean), Column('shutdown_terminate', Boolean), Column('disable_terminate', Boolean), Column('root_gb', Integer), Column('ephemeral_gb', Integer), Column('cell_name', String(length=255)), mysql_engine='InnoDB', mysql_charset='utf8' )
创建表:
tables = [……, instances, ……] for table in tables: try: table.create() except Exception: LOG.info(repr(table)) LOG.exception(_('Exception while creating table.')) raise
也就是说,想在nova-manage db sync的时候创建一个新的表,就需要现在upgrade方法中加入一个表的结构描述,再把这个表的名称加入tables这个list元素中。
2.model的建立
假设已经创建了这个表,现在需要的是为这个表编写api。
打开nova.db.sqlalchemy.models.py,这个文件中是用来定义数据表模型的,把一个数据表转化成一个类。
以snapshot的model为例:
class Snapshot(BASE, NovaBase): """Represents a block storage device that can be attached to a VM.""" __tablename__ = 'snapshots' __table_args__ = () id = Column(String(36), primary_key=True, nullable=False) deleted = Column(String(36), default="") @property def name(self): return CONF.snapshot_name_template % self.id @property def volume_name(self): return CONF.volume_name_template % self.volume_id user_id = Column(String(255)) project_id = Column(String(255)) volume_id = Column(String(36), nullable=False) status = Column(String(255)) progress = Column(String(255)) volume_size = Column(Integer) scheduled_at = Column(DateTime) display_name = Column(String(255)) display_description = Column(String(255))
实际上就是把数据库里的字段一个个定义出来,主要有两个部分:__tablename__和字段的定义。这个类是继承了BASE和NovaBase这两个类,所以编写起来会很方便,因为NovaBase类中已经提供了一些基本的方法和实用的字段(可以在nova.openstack.common.db.sqlalchemy.models.py文件中查看NovaBase所继承的三个类),比如:
class TimestampMixin(object): created_at = Column(DateTime, default=timeutils.utcnow) updated_at = Column(DateTime, onupdate=timeutils.utcnow)
这个类提供了创建时间和更新时间(注意,NovaBase所提供的字段也需要写入133_folsom.py中表结构的定义里)。
3.api的编写
拥有model之后,就需要提供操作这个model的api,进入nova.db.sqlalchemy.api.py。
api一般分为创建、查询、更新、删除,以flavor的一些操作为例:
创建:
@require_admin_contextdef flavor_create(context, values): """Create a new instance type. In order to pass in extra specs, the values dict should contain a 'extra_specs' key/value pair: {'extra_specs' : {'k1': 'v1', 'k2': 'v2', ...}} """ specs = values.get('extra_specs') specs_refs = [] if specs: for k, v in specs.iteritems(): specs_ref = models.InstanceTypeExtraSpecs() specs_ref['key'] = k specs_ref['value'] = v specs_refs.append(specs_ref) values['extra_specs'] = specs_refs instance_type_ref = models.InstanceTypes() instance_type_ref.update(values) try: instance_type_ref.save() except db_exc.DBDuplicateEntry as e: if 'flavorid' in e.columns: raise exception.InstanceTypeIdExists(flavor_id=values['flavorid']) raise exception.InstanceTypeExists(name=values['name']) except Exception as e: raise db_exc.DBError(e) return _dict_with_extra_specs(instance_type_ref)
@require_admin_context
这是一个修饰函数,相当于一个函数加工,这里的这个修饰作用是检查context,看是否为admin权限。这个可以自定义,一般使用
@require_context
意味着需要传入context才可以执行。
instance_type_ref = models.InstanceTypes()
这里创建了一个models中InstanceTypes类的实例,通过instance_type_ref.update(values)为实例添加一个新的记录(update这个方法就是由NovaBase这个类提供的,非常方便)。然后通过instance_type_ref.save()把这条记录存入数据表当中(save也是由NovaBase这个类提供的,可以参考nova.openstack.common.db.sqlalchemy.models.py中ModelBase这个类)。
创建方法的总结:
1.需要传入context和values(这个是一个包含字段与字段值的字典元素)
2.创建一个数据表model的实例(somemodel_ref = models.SomeModel())
3.将values更新到实例中(somemodel_ref.update(values))
4.存入数据表中(somemodel_ref.save())
查询:
@require_contextdef flavor_get(context, id): """Returns a dict describing specific instance_type.""" result = _instance_type_get_query(context).\ filter_by(id=id).\ first() if not result: raise exception.InstanceTypeNotFound(instance_type_id=id) return _dict_with_extra_specs(result)def _instance_type_get_query(context, session=None, read_deleted=None): query = model_query(context, models.InstanceTypes, session=session, read_deleted=read_deleted).\ options(joinedload('extra_specs')) if not context.is_admin: the_filter = [models.InstanceTypes.is_public == True] the_filter.extend([ models.InstanceTypes.projects.any(project_id=context.project_id) ]) query = query.filter(or_(*the_filter)) return query def model_query(context, model, *args, **kwargs): """Query helper that accounts for context's `read_deleted` field. :param context: context to query under :param session: if present, the session to use :param read_deleted: if present, overrides context's read_deleted field. :param project_only: if present and context is user-type, then restrict query to match the context's project_id. If set to 'allow_none', restriction includes project_id = None. :param base_model: Where model_query is passed a "model" parameter which is not a subclass of NovaBase, we should pass an extra base_model parameter that is a subclass of NovaBase and corresponds to the model parameter. """ session = kwargs.get('session') or get_session() read_deleted = kwargs.get('read_deleted') or context.read_deleted project_only = kwargs.get('project_only', False) def issubclassof_nova_base(obj): return isinstance(obj, type) and issubclass(obj, models.NovaBase) base_model = model if not issubclassof_nova_base(base_model): base_model = kwargs.get('base_model', None) if not issubclassof_nova_base(base_model): raise Exception(_("model or base_model parameter should be " "subclass of NovaBase")) query = session.query(model, *args) default_deleted_value = base_model.__mapper__.c.deleted.default.arg if read_deleted == 'no': query = query.filter(base_model.deleted == default_deleted_value) elif read_deleted == 'yes': pass # omit the filter to include deleted and active elif read_deleted == 'only': query = query.filter(base_model.deleted != default_deleted_value) else: raise Exception(_("Unrecognized read_deleted value '%s'") % read_deleted) if nova.context.is_user_context(context) and project_only: if project_only == 'allow_none': query = query.\ filter(or_(base_model.project_id == context.project_id, base_model.project_id == None)) else: query = query.filter_by(project_id=context.project_id) return query
model_query是实际执行查询的函数,它的作用是先获取一个数据库的session(这是sqlalchemy定义的一个实例,可以在nova.openstack.common.db.sqlalchemy.session.py中查看get_session这个函数),返回一个query(这是通过session实例的query方法,根据传入的参数查询数据表后获取的返回数据)。
在编写查询api的时候可以不用关心这个。主要的是这个函数的编写:
def _instance_type_get_query(context, session=None, read_deleted=None): query = model_query(context, models.InstanceTypes, session=session, read_deleted=read_deleted).\ options(joinedload('extra_specs')) if not context.is_admin: the_filter = [models.InstanceTypes.is_public == True] the_filter.extend([ models.InstanceTypes.projects.any(project_id=context.project_id) ]) query = query.filter(or_(*the_filter)) return query
model_query需要传入context、model(模型的名字)、session,还有一些参数如read_deleted(read_deleted值为"yes"或者"no",用于是选择否获取deleted为true的记录,因为openstack几乎不删除记录,只是把记录的deleted值从0改成1)
一些完整的实例:
获取SomeModel中id字段值为some_id的第一个没有被删除的数据:
result = model_query(context, models.SomeModel, session=session, read_deleted="no").\ filter_by(id=some_id).\ first()
获取SomeModel中size字段值为some_size的第一个没有被删除的数据:
result = model_query(context, models.SomeModel, session=session, read_deleted="no").\ filter_by(size=some_size).\ first()
获取SomeModel中id字段值为some_id的所有没有被删除的数据:
result = model_query(context, models.SomeModel, session=session, read_deleted="no").\ filter_by(id=some_id).\ all()
大概就是这个样子。
查询方法的总结:
1.根据model_query编写一个正确的查询语句
2.创建filter或者不创建
3.选择all或者first(其实也是列表和字符串的区别)
更新:
@require_contextdef some_update(context, some_id, values): session = get_session() with session.begin(): some_ref = some_get(context, some_id, session=session) some_ref.update(values) some_ref.save(session=session)
更新这个比较简单,和创建很像,区别是创建方法中的some_ref是models.py中数据模型的实例,而更新方法中的some_ref是通过查询得到的一个model_query实例。
这个方法就不总结了。
删除:
@require_admin_contextdef some_destroy(context, some_id): session = get_session() with session.begin(): session.query(models.SomeModel).\ filter_by(id=some_id).\ update({'deleted': True, 'deleted_at': timeutils.utcnow(), 'updated_at': literal_column('updated_at')})
删除这个也比较容易理解,查询到需要删除的那个记录,把deleted更新为True(或者1等等布尔值为true的值)。
这个也不总结了。。
4.把api封装一下
进入nova.db.api.py
将在nova.db.sqlalchemy.api.py中编写的api加入这个文件,示例:
def some_create(context, values): return IMPL.some_create(context, values)def some_destroy(context, some_id): return IMPL.some_destroy(context, some_id)def some_get(context, some_id): return IMPL.some_get(context, some_id)def some_get_by_size(context, some_size): return IMPL.some_get_by_size(context, some_size)def some_get_all(context): return IMPL.some_get_all(context)def some_update(context, some_id, values): return IMPL.some_update(context, some_id, values)
到这里,在nova.db中的编程就基本结束了
二、compute模块中的调用方法编写
先说E\F版的nova.compute.manager.py中调用数据库的方法:
通过Manager类就可以导入db,只用使用self.db.some_api就能直接掉nova.db.api里的方法,非常方便T_T
再说G\H版中通过conductor来调用nova.db:
当你要调用nova.db.api中的方法时,需要在以下几个文件中添加相应的方法。
示例,现在nova.db.api中有这么一个方法:some_create(context, values)用于创建某条记录,在nova.compute.manager.py中一个功能会使用到它,这个功能里就必须有这么一条语句:
self.conductor_api.some_create(ontext, values)
(当然在这里无论是函数名还是参数都可以随意,除了context)
它会调入nova.conductor.api.py中LocalAPI类的some_create这个方法(LocalAPI代表这个功能的操作在本计算节点执行)LocalAPI类中some_create方法可以这样编写:
def some_create(self, context, values): return self._manager.some_create(context, values)
接着调入nova.conductor.rpcapi.py中ConductorAPI类的some_create方法,可以这么编写:
def some_create(self, context, values): cctxt = self.client.prepare(version='1.50') return cctxt.call(context, 'some_create', values=values)
这里conductor还调用了rpc,也就是说这个创建数据表中记录的动作通过消息发回了控制节点,被控制节点指配给nova.conductor.manager.py中ConductorManager类的some_create方法来执行。
在这个方法里,就可以做和E\F版nova.compute.manager.py中一样的操作了,就是直接调用nova.db.api.py中的方法。
def some_create(self, context, values): self.db.some_create(context, values)
这样基本就完成了一个功能在操作nova数据库方面的编程。