Nova数据库模块的使用方法和开发

Nova DB简介

nova.db在F、G、H版本的差异不大,但是从G版开始加入了conductor,不允许compute直接访问数据库,所以在compute的代码里调用数据库需要通过conductor。(PS:现在可以在计算节点配置conductor session的use_local选项来决定是否由compute服务直接访问数据库)

如果要增加一个新的功能,而且这个功能需要操作数据库,在操作数据库这个方面一般分为两个步骤:

一、db模块中的内容编写,主要包括数据表的创建、功能及api的编写;

二、compute模块中,对db提供的api调用方法的编写。

推荐优先学习sqlalchemy模块

openstack环境版本:H版(G版类似,F、E版的区别会有说明)

DB模块中的内容编写

描述数据表

nova数据库的创建:

第一次同步nova数据库时的操作:

1
nova-manage db sync

这个指定的代码在 nova.db.sqlalchemy.migration.py 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 的表结构):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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'
)

创建表:

1
2
3
4
5
6
7
8
9
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元素中。

model的建立

假设已经创建了这个表,现在需要的是为这个表编写 api 。

打开 nova.db.sqlalchemy.models.py ,这个文件中是用来定义数据表模型的,把一个数据表转化成一个类。

以 snapshot 的 model 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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所继承的三个类),比如:

1
2
3
class TimestampMixin(object):
created_at = Column(DateTime, default=timeutils.utcnow)
updated_at = Column(DateTime, onupdate=timeutils.utcnow)

这个类提供了创建时间和更新时间(注意,NovaBase所提供的字段也需要写入133_folsom.py中表结构的定义里)。

api的编写

拥有model之后,就需要提供操作这个 model 的 api ,进入 nova.db.sqlalchemy.api.py 。
api 一般分为创建、查询、更新、删除,以 flavor 的一些操作为例:

创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@require_admin_context
def 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 才可以执行。

1
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() )

查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@require_context
def 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 的时候可以不用关心这个。主要的是这个函数的编写:

1
2
3
4
5
6
7
8
9
10
11
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 的第一个没有被删除的数据:

1
2
3
4
result = model_query(context, models.SomeModel, session=session,
read_deleted="no").\
filter_by(id=some_id).\
first()

获取 SomeModel 中 size 字段值为 some_size 的第一个没有被删除的数据:

1
2
3
4
result = model_query(context, models.SomeModel, session=session,
read_deleted="no").\
filter_by(size=some_size).\
first()

获取 SomeModel 中 id 字段值为 some_id 的所有没有被删除的数据:

1
2
3
4
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
2
3
4
5
6
7
@require_context
def 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 实例。

这个方法就不总结了。

删除

1
2
3
4
5
6
7
8
9
@require_admin_context
def 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 的值)。

这个也不总结了。

把api封装一下

进入 nova.db.api.py,将在 nova.db.sqlalchemy.api.py 中编写的 api 加入这个文件,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 中一个功能会使用到它,这个功能里就必须有这么一条语句:

1
self.conductor_api.some_create(ontext, values) #(当然在这里无论是函数名还是参数都可以随意,除了context)

它会调入 nova.conductor.api.py 中 LocalAPI 类的 some_create 这个方法( LocalAPI 代表这个功能的操作在本计算节点执行) LocalAPI 类中 some_create 方法可以这样编写:

1
2
def some_create(self, context, values):
return self._manager.some_create(context, values)

接着调入 nova.conductor.rpcapi.py 中 ConductorAPI 类的 some_create 方法,可以这么编写:

1
2
3
4
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 中的方法。

1
2
def some_create(self, context, values):
self.db.some_create(context, values)

这样基本就完成了一个功能在操作 nova 数据库方面的编程。