OpenStack通用库(Oslo)包含了众多不需要重复发明的"轮子".当开发者觉得现有的代码中有适合被其他OpenStack项目共用的部分时,就可以申请把这些能用的代码放入oslo-incubator代码库中进行"孵化".
当oslo-incubator中的某部分代码让社区觉得已经足够成熟可以毕业后,其代码开发维护者可以提出毕业申请.毕业后的代码会成为一个单独的OpenStack项目,独立于oslo-incubator进行开发和维护.
oslo-incubator的代码库位于https://github.com/openstack/oslo-incubator.相应的Lauchpad页面是https://launchpad.net/oslo.
和使用其他第三方python库一样,使用已经毕业的oslo能用库,我们只需要在自己的代码中import对应的库就可以了,比如下面的代码就使用oslo.config库定义了一个配置选项sample_source:
from oslo_config import cfg OPTS = [ cfg.StrOpt('sample_source', default='openstack', deprecated_name='counter_source', help='Source for samples emitted on this instance.'), ] cfg.CONF.register_opts(OPTS)
如果在某一个OpenStack项目中要使用还在孵化期中的oslo.incubator代码,需要把oslo.incubator的代码同步到该项目代码的openstack/common目录下.所以我们如果看到诸如"from xxx.openstack.common import yyy"之类的代码,一般这些代码都是从oslo.incubator代码库中同步过来的,比如:
from ceilometer.openstack.common import log from ceilometer.openstack.common import service as os_service LOG = log.getLogger(__name__)
Cliff
Cliff(Command Line Interface Formulation Framework)可以用来帮助构建命令行程序.开发者可以利用Cliff框架可以构建诸如svn,git那样的支持多层命令的命令行程序.主程序只负责基本的命令行参数的解析,然后调用各个子命令去执行不同的操作.利用python动态代码载入的特性,Cliff框架中的每
个子命令可以和主程序分开来实现,打包和分发.
Cliff的代码库们于http://git.openstack.org/cgit/openstack/cliff,项目主页在https://launchpad.net/python-cliff.
整个Cliff框架主要包括以下四种不同类型的对象:
- cliff.app.App:主程序对象,用来启动程序,并且负责一些对所有子命令都能用的操作,比如设置日志选项和输入输出等.
- cliff.commandmanager.CommandManager:主要用来载入每个子命令插件.默认是通过Setuptools的entry points来载入.
- cliff.command.Command:用户可以实现Command的子类来实现不同的子命令,这些子命令被注册在Setuptools的entry points中,被CommandManager载入每个子命令可以有自己的参数解析(一般使用argparse),同时要实现take_action()方法完成具体的命令.
- cliff.interactive.InteractiveApp:实现交互式命令行.一般使用框架提供的默认实现.
Cliff源码中附带了一个示例demoApp,下面我们结合这个例子来了解Cliff的大致接口:
import logging import sys from cliff.app import App from cliff.commandmanager import CommandManager class DemoApp(App): log = logging.getLogger(__name__) def __init__(self): super(DemoApp, self).__init__( description='cliff demo app', version='0.1', command_manager=CommandManager('cliff.demo') ) def initialize_app(self, argv): self.log.debug('initialize_app') def prepare_to_run_command(self, cmd): self.log.debug('prepare_to_run_command%s', cmd.__class__.__name__) def clean_up(self, cmd, result, err): self.log.debug('clean_up %s', cmd.__class__.__name__) if err: self.log.debug('got an error: %s', err) def main(argv=sys.argv[1:]): myapp = DemoApp() return myapp.run(argv) if __name__ == "__main__": sys.exit(main(sys.argv[1:]))
上面是主程序的代码,主程序新建一个DemoApp对象实例,并且调用其run方法运行.DemoApp是cliff.app.App的子类,它的初始化函数的原型定义为:
class App(object): """Application base class. :param description: one-liner explaining the program purpose :paramtype description: str :param version: application version number :paramtype version: str :param command_manager: plugin loader :paramtype command_manager: cliff.commandmanager.CommandManager :param stdin: Standard input stream :paramtype stdin: readable I/O stream :param stdout: Standard output stream :paramtype stdout: writable I/O stream :param stderr: Standard error output stream :paramtype stderr: writable I/O stream :param interactive_app_factory: callable to create an interactive application :paramtype interactive_app_factory: cliff.interactive.InteractiveApp :param deferred_help: True - Allow subcommands to accept --help with allowing to defer help print after initialize_app :paramtype deferred_help: bool """ def __init__(self, description, version, command_manager, stdin=None, stdout=None, stderr=None, interactive_app_factory=None, deferred_help=False):其中stdin/stdout/stderr可以用来定义用户自己的标准输入/输出/错误,command_manager必须指向一个cliff.commandmanger.CommandManger
的对象实例,这个实例用来载入各个子命令插件.
cliff.commandmanager.CommandManager类的初始化函数原型定义为:
class CommandManager(object): """Discovers commands and handles lookup based on argv data. :param namespace: String containing the setuptools entrypoint namespace for the plugins to be loaded. For example, ``'cliff.formatter.list'``. :param convert_underscores: Whether cliff should convert underscores to spaces in entry_point commands. """ def __init__(self, namespace, convert_underscores=True): self.commands = {} self.namespace = namespace self.convert_underscores = convert_underscores self._load_commands()其中namespace用来指定Setuptools entry points的全名空间,CommandManager只会从这个命名空间中载入插件,convert_underscores参数指明
是否需要把entry points中的下划线转化为空格.
我们可以利用cliff.app.App类的方法initialize_app()做一些初始化工作,这个函数会在主程序解析完用户的命令行参数后被调用,而且只会被调用到唯
一一次.
prepare_to_run_command()方法可以被用来做一些针对某个具体子命令的初始化工作,它将在该子命令被执行之前调用.clean_up()方法会在具体某个子
命令完成后被调用,用来进行一些清理工作.
具体某个子命令的实现通过继承cliff.command.Command来完成:
import logging from cliff.command import Command class Simple(Command): "A simple command that prints a message." log = logging.getLogger(__name__) def take_action(self, parsed_args): self.log.info('sending greeting') self.log.debug('debugging') self.app.stdout.write('hi!\n')子命令的实际工作由take_action()完成.这个例子里,simple子命令向标准输出打印一个字符串,它的实现代码由cliff.commandmanager.CommandManager通过Setuptools entry points来载入:
from setuptools import setup, find_packages if __name__ == '__main__': if __name__ == '__main__': setup( name='cliffdemo', version='0.1', install_requires=['cliff'], namespace_packages=[], packages=find_packages(), entry_points={ 'console_scripts':[ 'cliffdemo = cliffdemo.main:main' ], 'cliff.demo':[ 'simple = cliffdemo.simple:Simple' ], }, )
在Setup tools entry points的命名空间cliff.demo中,定义了命令simple所对应的插件实现是Simple类.Cliff主程序解析用户输入后,会通过这里所定义
的对应关系调用不同的实现类.
simple命令的执结果为:
$ cliffdemo simple
sending greeting
hi!
bogon:oslo_tutorials zhangzhangxiaoan$ cliffdemo -v simple
initialize_app
prepare_to_run_commandSimple
sending greeting
debugging
hi!
clean_up Simple
bogon:oslo_tutorials zhangzhangxiaoan$
oslo.config
oslo.config库用于解析命令行和配置文件中的配置选项,是oslo-incubator中最早毕业的一个.
oslo.config的代码库位于https://github.com/openstack/oslo.config,项目主页为https://launchpad.net/oslo.config,参考文档在http://docs.openstack.org/developer/oslo.config/.
这里通过以下几个应用场景来介绍oslo.config的用法:
定义和注册配置选项
#file: service.py import socket import os from oslo_config import cfg OPTS = [ cfg.StrOpt('host', default=socket.gethostname(), help='Name of this node'), cfg.IntOpt('collector_workers', default=1, help='Number of workers for collector service.') ] cfg.CONF.register_opts(OPTS) CLI_OPTS = [ cfg.StrOpt('os-tenant-id', deprecated_group='DEFAULT', default=os.environ.get('OS_TENANT_ID', ''), help='Tenant ID to use for OpenStack service access.'), cfg.IntOpt('insecure', default=False, help='Disable X.509 certificate validation when an ' 'SSL connection to Identify Service is established.'), ] cfg.CONF.register_cli_opts(CLI_OPTS, group='service_credentials')
配置选项有不同的类型,目前所支持的如表4-4所求.
类 名 | 说 明 |
oslo.config.cfg.StrOpt | 字符串类型 |
oslo.config.cfg.BoolOpt | 布尔型 |
oslo.config.cfg.IntOpt | 整数类型 |
oslo.config.cfg.FloatOpt | 浮点数类型 |
oslo.config.cfg.ListOpt | 字符串列表类型 |
oslo.config.cfg.DictOpt | 字典类型,字典中的值需要是字符串类型 |
oslo.config.cfg.MultiStrOpt | 可以分多次配置的字符串列表 |
oslo.config.cfg.IPOpt | IP地址类型 |
定义后的配置选项,必须要注册后才能使用.此外,配置选项还可以注册为命令行选项,之后,这些配置选项的值就可以从命令行读取,并覆盖从配置文件中读取的值.
注册配置选项时,可以把某些配置选项注册在一个特定的组下.这样可以帮助管理员更好地组织配置选项文件.如果没有指定,默认的组是'DEFAULT'.
在新版本的oslo.config中(版本号>=1.3.0),增加了另一种新的定义配置选项的方式:
from oslo_config import cfg
from oslo_config import types
PortType = types.Integer(1, 65535)
common_opts = [
cfg.Opt('bind_port',
type=PortType(),
default=9292,
help="Port number to listen on.")
]
相比于前面的方法,这种定义配置选项的方式能够更好地支持选项值的合法性检查,同时也能支持自定义选项类型.因此,建议新的项目采用这种方式定义配置选项.但由于目前大部分OpenStack项目还是在采用旧方式,为了读者理解代码的方便,这里我们仍然采用旧方式.
使用配置文件和命令行选项指定配置选项
为了正确使用oslo.config,应用程序一般需要在启动的时候初始化,比如:
import sys from oslo_config import cfg cfg.CONF(sys.argv[1:], project='xyz')初始化后,才能正常解析配置文件和命令行选项.最终用户可以用默认的命令行选项"--config-file"或者"--config-dir"来指定配置文件名或者位置.如果没有明确指定,默认按下面的顺序寻找配置文件:
~/.xyz/xyz.conf ~/xyz.conf /etc/xyz/xyz.conf /etc/xyz.conf
配置文件一般采用类似.ini文件的格式,其中每一个section对应oslo.confi中定义的一个配置选项组,section[DEFAULT]对应了默认组"DEFAULT".比如:
[DEFAULT]
host = 1.1.1.1
collector_workers = 3
[service_credentials]
secure = TRUE
用命令行指定配置选项值时,如果是定义在某个选项组中的选项,命令行选项名中需要包括该组名作为前缀:
--service_credentials-os-tenant-id ab23ef67
使用其他模块已经注册过的配置选项
对于已经注册过的配置选项,开发者可以直接访问:
from oslo_config import cfg import service hostname = cfg.CONF.host tenant_id = cfg.CONF.service_credentials.os-tenant-id这里导入service模块是因为选项host和os-tenant-id是在service模块中注册的.但是从编码风格来看,上述代码比较古怪,我们导入了service模块却从未直接使用它.所以,我们也可以使用import_opt来申明在别的模块中定义的配置选项:
from oslo_config import cfg import service cfg.CONF.import_opt('host', 'service') hostname = cfg.CONF.host
oslo.db
oslo.db是针对SQLAlchemy访问的抽象.代码库位于https://github.com/openstack/oslo.db.项目主页为https://bugs.launchpad.net/oslo,参考文档在http://docs.openstack.org/developer/oslo.db.
这里我们通过几个不同的使用范例来了解oslo.db中主要接口的使用方法.
获取SQLAlchemy的engine和session对象实例
from oslo_config import cfg from oslo_db.sqlalchemy import session as db_session _FACADE = None def _create_facade_lazily(): global _FACADE if _FACADE is None: _FACADE = db_session.EngineFacade.from_config(cfg.CONF) return _FACADE def get_engine(): return _create_facade_lazily() def get_session(**kwargs): return _create_facade_lazily().get_session(**kwargs)
获取了engine和session对象实例后,开发者就可以按照一般访问SQLAlchemy的方式进行使用,这里的engine对象是共享的,同时也是线程安全的,可以等效成一组数据库连接.而session对象可以看做是一个数据库交易事务的上下文,它不是线程安全的,不应该被共享使用.
管理员可以通过配置文件来配置oslo.db的许多选项,比如:
[database]
connection = mysql://root:123456@localhost/ceilometer?charset=utf8
常用的配置选项如下表所示(具体参见oslo/db/options.py)
配置项 = 默认值 | 说 明 |
backend = sqlalchemy | (字符串类型)后台数据库标识 |
connection = None | (字符串类型)sqlalchemy用此来连接数据库 |
connection_debug = 0 | (整型)sqlalchemy的debug等级,0表示不输出任何调试信息,100表示输出所有调试信息 |
connection_trace = False | (布尔型)是否把python的调用栈信息加到SQL的注释中 |
db_inc_retry_interval = True | (布尔型)连接重试时,是否增加重试之间的时间间隔 |
db_max_retries = 20 | (整型)连接重试的最多次数(-1表示一直重试) |
db_retry_interval = 1 | (整型)连接重试时间间隔,单位为秒 |
idle_timeout = 3600 | (整型)连接被回收之前的空闲时间,单位为秒 |
max_overflow = None | (整型)如果设置了,这个参数会被直接传递给sqlalchemy |
max_pool_size = None | (整型)在一个连接池中,最大可同时打开的连接数 |
max_retires = 10 | (整型)打开连接时最大重试次数(-1表示一直重试) |
retry_interval = 10 | (整型)打开连接时重试的时间间隔 |
使用OpenStack中通用的SQLAlchemy model类
from oslo_db.sqlalchemy import models class ProjectSomething(models.TimestampMixin, models.ModelBase): id = Column(Interger, primary_key=True) ...
oslo.db.models模块目前只定义了两种Mixin: TimestampMixin和SoftDeleteMixin.使用TimestampMixin时SQLAlchemy model中会多出两列create_at和updated_at,分别表示记录的创建时间和上一次个性时间.
SoftDeleteMixin支持使用soft delete功能,比如:
from oslo_db.sqlalchemy import models class BarModel(models.SoftDeleteMixin, models.ModelBase): id = Column(Integer, primary_key=True) ... ... count = model_query(BarModel).find(some_condition).soft_delete() if count == 0: raise Exception("0 entries were soft deleted")
不同DB后端的支持
from oslo_config import cfg from oslo_db import api as db_api _BACKEND_MAPPING = {'sqlalchemy': 'project.db.sqlalchemy.api'} IMPL = db_api.DBAPI.from_config(cfg.CONF, backend_mapping=_BACKEND_MAPPING) def get_engine(): return IMPL.get_engine() def get_session(): return IMPL.get_session() def do_somethin(somethind_id): return IMPL.do_something(somethind_id)
不同backend具体实现时,需要定义如下函数返回具体DB API的实现类:
def get_backend(): return MyImplementationClass
oslo.i18n
oslo.i18n是对python gettext模块的封装,主要用于字符串的翻译和国际化.代码库们于https://github.com/openstack/oslo.i18n,项目主页为https://launchpad.net/oslo.i18n,参考文档在
http://docs.openstack.org/developer/oslo.i18n/.
使用oslo.i18n前,需要先创建一个如下的集成模块:
import oslo_i18n _translators = oslo_i18n.TranslatorFactory(domain='myapp') #主要的翻译函数,类似gettext中的"_"函数 _ = _translators.primary #不同log level对应的翻译函数 _LI = _translators.log_info _LW = _translators.log_warning _LE = _translators.log_error _LC = _translators.log_critical
之后在程序中就可以比较容易使用:
from .itegera import _, _LW, _LE LOG.warn(_LW('warning message: %s'), var) ... try: ... except Exception: LOG.exception(_LE('There was an error.')) ... raise ValueError(_('error: v1=%(v1)s v2=%(v2)s') % {'v1': v1, 'v2': v2})