nova

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


在nova.db模块中E\F\G\H这几个版本差异不大,但是从G版本开始加入了conductor,不允许compute直接访问数据库,所以在G版本之后compute的代码中调用数据库需要通过conductor。

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

  1. db模块中内容编写;主要包括数据表创建、功能及API编写
  2. 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权限。

创建方法的总结:

  1. 需要传入context和values(这个是一个包含字段与字段值的字典元素)
  2. 创建一个数据表model的实例(somemodel_ref = models.SomeModel())
  3. 将values更新到实例中(somemodel_ref.update(values))
  4. 存入数据表中(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数据库方面的编程。

posted @ 2016-10-19 22:51  Jason_Li126  阅读(541)  评论(0)    收藏  举报