nova
Nova数据库模块的开发和使用
在nova.db模块中E\F\G\H这几个版本差异不大,但是从G版本开始加入了conductor,不允许compute直接访问数据库,所以在G版本之后compute的代码中调用数据库需要通过conductor。
如果新增一个新的功能,而且该功能需要操作数据库,在操作数据库这个方面一般分为两步:
- db模块中内容编写;主要包括数据表创建、功能及API编写
- compute模块中;对db提供的API调用方法的表写
本文以在OpenStack Nova的Mitaka版本中增加local disk 数据库操作为例
一、DB模块中内容编写
1.1 描述数据表
1.1.1 Nova数据库创建:
第一次同步nova数据库时的操作
nova-manager db sync
命令对应的代码在nova.db.sqlalchemy.migration.py中:
def db_sync(version=None, database='main', context=None):
if version is not None:
try:
version = int(version)
except ValueError:
raise exception.NovaException(_("version should be an integer"))
current_version = db_version(database, context=context)
repository = _find_migrate_repo(database)
if version is None or version > current_version:
return versioning_api.upgrade(get_engine(database, context=context),
repository, version)
else:
return versioning_api.downgrade(get_engine(database, context=context),
repository, version)
代码会调用upgrade方法,它最后会找到nova.db.sqlalchemy.migrate_repo.versions.346_local_disk.py文件,调用该文件中的upgrade方法,代码很长,这里仅截取创建数据表相关的部分:
首先,写一个数据表的结构(以下这个就是nova.instances的表结构)
local_block_devices = Table('local_block_device', meta,
Column('created_at', DateTime),
Column('updated_at', DateTime),
Column('deleted_at', DateTime),
Column('deleted', Boolean),
Column('dev_name', String(length=255)),
Column('hba', String(length=255)),
Column('capacity', Integer),
Column('instance_id', Integer),
Column('status', 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-manager db sync时候创建一个新表,就需要在update方法中加入一个表结构的描述,在把这个表的名称加到tables这个list中。
1.2. model 的建立
假设这个表已经创建,现在需要为这个表编写API。
打开nova.db.sqlalchemy.models.py,这个文件是用来定义数据表模型的,把一个数据表转化成一个类。以PciDevice的model为例
class PciDevice(BASE, NovaBase, models.SoftDeleteMixin):
"""Represents a PCI host device that can be passed through to instances.
"""
__tablename__ = 'pci_devices'
__table_args__ = (
Index('ix_pci_devices_compute_node_id_deleted',
'compute_node_id', 'deleted'),
Index('ix_pci_devices_instance_uuid_deleted',
'instance_uuid', 'deleted'),
Index('ix_pci_devices_compute_node_id_parent_addr_deleted',
'compute_node_id', 'parent_addr', 'deleted'),
schema.UniqueConstraint(
"compute_node_id", "address", "deleted",
name="uniq_pci_devices0compute_node_id0address0deleted")
)
id = Column(Integer, primary_key=True)
compute_node_id = Column(Integer, ForeignKey('compute_nodes.id'),
nullable=False)
# physical address of device domain:bus:slot.func (0000:09:01.1)
address = Column(String(12), nullable=False)
vendor_id = Column(String(4), nullable=False)
product_id = Column(String(4), nullable=False)
dev_type = Column(String(8), nullable=False)
dev_id = Column(String(255))
# label is abstract device name, that is used to unify devices with the
# same functionality with different addresses or host.
label = Column(String(255), nullable=False)
status = Column(String(36), nullable=False)
# the request_id is used to identify a device that is allocated for a
# particular request
request_id = Column(String(36), nullable=True)
extra_info = Column(Text)
instance_uuid = Column(String(36))
numa_node = Column(Integer, nullable=True)
parent_addr = Column(String(12), nullable=True)
instance = orm.relationship(Instance, backref="pci_devices",
foreign_keys=instance_uuid,
primaryjoin='and_('
'PciDevice.instance_uuid == Instance.uuid,'
'PciDevice.deleted == 0)')
实际上就是把数据库里的字段一个个定义出来,主要有两个部分:__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所提供的字段也需要写入346_local_disk.py中表结构的定义里)。
1.3. API的编写
拥有model之后,就需要提供操作这个model的api,进入nova.db.sqlalchemy.api.py。
api一般分为创建、查询、更新、删除,以Instance的一些操作为例:
创建
@require_context
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
@pick_context_manager_writer
def instance_create(context, values):
"""Create a new Instance record in the database.
context - request context object
values - dict containing column values.
"""
security_group_ensure_default(context)
values = values.copy()
values['metadata'] = _metadata_refs(
values.get('metadata'), models.InstanceMetadata)
values['system_metadata'] = _metadata_refs(
values.get('system_metadata'), models.InstanceSystemMetadata)
_handle_objects_related_type_conversions(values)
instance_ref = models.Instance()
if not values.get('uuid'):
values['uuid'] = uuidutils.generate_uuid()
instance_ref['info_cache'] = models.InstanceInfoCache()
info_cache = values.pop('info_cache', None)
if info_cache is not None:
instance_ref['info_cache'].update(info_cache)
security_groups = values.pop('security_groups', [])
instance_ref['extra'] = models.InstanceExtra()
instance_ref['extra'].update(
{'numa_topology': None,
'pci_requests': None,
'vcpu_model': None,
})
instance_ref['extra'].update(values.pop('extra', {}))
instance_ref.update(values)
def _get_sec_group_models(security_groups):
models = []
default_group = _security_group_ensure_default(context)
if 'default' in security_groups:
models.append(default_group)
# Generate a new list, so we don't modify the original
security_groups = [x for x in security_groups if x != 'default']
if security_groups:
models.extend(_security_group_get_by_names(
context, security_groups))
return models
if 'hostname' in values:
_validate_unique_server_name(context, values['hostname'])
instance_ref.security_groups = _get_sec_group_models(security_groups)
context.session.add(instance_ref)
# create the instance uuid to ec2_id mapping entry for instance
ec2_instance_create(context, instance_ref['uuid'])
return instance_ref
require_context 装饰器,意味着需要传入context才可以执行
pick_context_manager_writer这是一个修饰函数,相当于一个函数加工,这里的这个修饰作用是检查context,看是否为admin权限。
创建方法的总结:
- 需要传入context和values(这个是一个包含字段与字段值的字典元素)
- 创建一个数据表model的实例(somemodel_ref = models.SomeModel())
- 将values更新到实例中(somemodel_ref.update(values))
- 存入数据表中(somemodel_ref.save())
查询
@require_context
@pick_context_manager_reader
def instance_get_all(context, columns_to_join=None):
if columns_to_join is None:
columns_to_join_new = ['info_cache', 'security_groups']
manual_joins = ['metadata', 'system_metadata']
else:
manual_joins, columns_to_join_new = (
_manual_join_columns(columns_to_join))
query = model_query(context, models.Instance)
for column in columns_to_join_new:
query = query.options(joinedload(column))
if not context.is_admin:
# If we're not admin context, add appropriate filter..
if context.project_id:
query = query.filter_by(project_id=context.project_id)
else:
query = query.filter_by(user_id=context.user_id)
instances = query.all()
return _instances_fill_metadata(context, instances, manual_joins)
model_query是实际执行查询的函数,它的作用是先获取一个数据库的session(这是sqlalchemy定义的一个实例,可以在nova.openstack.common.db.sqlalchemy.session.py中查看get_session这个函数),返回一个query(这是通过session实例的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(其实也是列表和字符串的区别)
更新和删除这里就不在啰嗦了。
1.4. 把API封装起来
进入nova.db.api.py
将在nova.db.sqlalchemy.api.py中编写的api加入这个文件,示例:
def localdisk_create(context, values):
return IMPL.localdisk_create(context, values)
def localdisk_destroy(context, somelocaldisk_id):
return IMPL.localdisk_destroy(context, localdisk_id)
def localdisk_get(context, localdisk_id):
return IMPL.localdisk_get(context, localdisk_id)
def localdisk_get_by_size(context, localdisk_size):
return IMPL.localdisk_get_by_size(context, localdisk_size)
def localdisk_get_all(context):
return IMPL.localdisk_get_all(context)
def localdisk_update(context, localdisk_id, values):
return IMPL.localdisk_update(context, localdisk_id, values)
到此处,nova.db中的修改就基本完成了,下面看compute中的使用。
二、compute模块中的调用方法编写
2.1. G版本之前的版本(E、F版本)
先说E、F版的nova.compute.manager.py中调用数据库的方法:
通过Manager类就可以导入db,只用使用self.db.some_api就能直接掉nova.db.api里的方法,非常方便
2.2. G版本之后版本(G、H、L、M版本)
再说G之后版中通过conductor来调用nova.db:
当你要调用nova.db.api中的方法时,需要在以下几个文件中添加相应的方法。
示例,现在nova.db.api中有这么一个方法:
localdisk_create(context, values)
用于创建某条记录
在nova.compute.manager.py中一个功能会使用到它,这个功能里就必须有这么一条语句:
self.conductor_api.localdisk_create(ontext, values)
它会调入nova.conductor.api.py中LocalAPI类的localdisk_create这个方法(LocalAPI代表这个功能的操作在本计算节点执行)LocalAPI类中localdisk_create方法可以这样编写:
def localdisk_create(self, context, values):
return self._manager.localdisk_create(context, values)
接着调入nova.conductor.rpcapi.py中ConductorAPI类的localdisk_create方法,可以这么编写:
def localdisk_create(self, context, values):
cctxt = self.client.prepare(version='1.50')
return cctxt.call(context, 'some_create',
values=values)
这里conductor还调用了rpcapi,也就是说这个创建数据表中记录的动作通过消息发回了控制节点,被控制节点指配给nova.conductor.manager.py中ConductorManager类的localdisk_create方法来执行。
在这个方法里,就可以做和G之前版本nova.compute.manager.py中一样的操作了,就是直接调用nova.db.api.py中的方法。
def localdisk_create(self, context, values):
self.db.localdisk_create(context, values)
这样基本就完成了一个功能在操作nova数据库方面的编程。

浙公网安备 33010602011771号