本帖最后由 nettman 于 2014-1-20 22:07 编辑
OpenStack开发指南
目录 1、nova源码架构介绍2 1.1、源码的获取2 1.2、开发环境的搭建2 1.3、nova模块调用介绍6 1.4、nova源码模块功能介绍7 2、数据库表结构的扩展8 2.1、nova表结构的扩展8 2.2、keystone表结构的扩展10 3、resetful接口服务的扩展12 3.1、nova数据库调用接口服务的扩展12 3.2、compute接口的扩展17 3.3、keystone接口服务的扩展20 3.4、基于openstack服务、配置架构自定义服务模块21 4、dashboard源码介绍25 4.1 horizon代码模块介绍26 4.2 中文化的功能实现26 4.3 页面按钮的添加27 4.4 列表中下拉菜单的添加29 4.5 列表中文字链接的添加29
1、nova源码架构介绍 1.1、源码的获取 openstack的源码可以从安装好的openstack环境上直接copy下来使用,也可以从官网下载(红帽地址: http://repos.fedorapeople.org/repos/openstack/),ubuntu环境下openstack的源码目录为:/usr/share/pyshared/,这里面的源码安装好后会被外链到/usr/lib/python2.7/dist-packages/,这是python2.7以后的版本的做法,所有的python项目都会采取这种存储方式;centos环境下openstack一般会安装python2.6的环境,所以openstack的源码存储在/usr/lib/python2.6/site-packages/。
1.2、开发环境的搭建 第二步、然后安装开发python程序的IDE,python的程序一般使用PyDev作为开发工具,它可以被集成到eclipse和aptana等IDE中。PyDev的下载地址: http://pydev.org/download.html,aptana的下载地址: http://aptana.org/,当前发布的是aptana3.0,默认已经集成了PyDev。当前我使用的是aptana,由于3.0使用的面板背景色是黑色的,我又习惯使用2.0的风格怎么办那,3.0有添加了一个颜色模板的菜单,选择一下“Aptana Studio 2.x”就可以了,如下图。 第三步、导入openstack的项目,打开IED,在左边的面板空白处右键,左键点击“import”, 如下图:
从弹出的窗口从选择“Existing Folder As New Project”,导入一个文件夹作为一个工程,如下图所示: 然后选择一个openstack项目,比如我当前选择了nova文件夹,起一个工程名,只要自己好记就行,下边的单选框可以不用选,点击完成就行了,如下图: 导入进来的nova目录结构如下图: 1.3、nova模块调用介绍 nova-api :起到Cloud Controller的作用,主要为所有的API查询提供了一个接口(比如Openstack API ,EC2 API),引发多数业务流程的活动(如运行一个实例),并实施一些政策(主要是配额检查)。 nova-schedule :接受一个消息队列的虚拟实例请求,通过算法决定该请求应该在那台主机上运行,这个算法可以由我们指定。即起到调度器(Scheduler)的作用. nova-compute:是一个非常重要的守护进程,负责创建和终止虚拟机实例,即管理着虚拟机实例的生命周期。该模块内部非常复杂,基本原理是简单的,就是接受来自队列的动作然后执行一些列的系统操作(如启动一个KVM实例),并且更新数据库的状态。 nova-network :该守护进程跟nova-compute and nova-volume 2个模块的功能是相似的。接受来自队列的任务,然后执行相应的任务对网络进行操作(比如:安装网桥接口和改变iptable规则) Queue:为各个模块之间的通信提供起到一个集线器的作用,即数据交换中心。目前是采用 RabbitMQ ,理论上是可以采用任何的基于python ampqlib的AMPQ message queue。 SQL database: 存储云基础设施构建时和运行时状态。包括可用的实例类型,正在使用的实例类型,可用的网络和项目。理论上,OpenStack Compute是支持所有基于 SQL-Alchemy的数据库,但目前广泛使用的数据库主要是Sqlite3,Mysql,PostgreSQL。 Glance:该项目独立于Openstack Compute,起到镜像的作用。在该项目中,主要包括三个部分: glance-api, glance-registry and 镜像存储。Glance-api接受API调用,glance-registry存储和检索镜像的元数据。镜像存储Image blobs。存储可以选择不同的存储方案,比如用Swift实现存储。 Dashboard:该项目是一个可选的项目,主要是为开发者等提供API。
1.4、nova源码模块功能介绍 2、数据库表结构的扩展 2.1、nova表结构的扩展 第一步、创建表的数据库结构,创建表结构的文件存在于目录nova.db.sqlalchemy.migrate_repo.versions,当执行nova-manage db sync命令时就会按照这个目录下文件开头的序列号顺序执行。添加表时只需要实现一个类似的文件,把名字的开头命名为最大序列值加1。比如我要创建一张表,表名为:domains,在这个versions下创建一个084_add_domains.py的文件,文件内容如下 #-*- coding: utf-8 -*- from sqlalchemy import Boolean, Column, DateTime, Integer from sqlalchemy import MetaData, String, Table from nova import log as logging LOG = logging.getLogger(__name__)
def upgrade(migrate_engine): ''' 创建表 ''' meta = MetaData() meta.bind = migrate_engine #生成创建表sql语句 domains = Table('domains', meta, Column('id', Integer(), primary_key=True, nullable=False), Column('name', String(length=255, convert_unicode=False, assert_unicode=None, unicode_error=None, _warn_on_bytestring=False)), Column('ttl', Integer()), Column('rdtype', String(length=255, convert_unicode=False, assert_unicode=None, unicode_error=None, _warn_on_bytestring=False)), Column('rdata', String(length=255, convert_unicode=False, assert_unicode=None, unicode_error=None, _warn_on_bytestring=False)), Column('project_id', String(length=255, convert_unicode=False, assert_unicode=None, unicode_error=None, _warn_on_bytestring=False)), Column('created_at', DateTime(timezone=False)), Column('updated_at', DateTime(timezone=False)), mysql_engine='InnoDB' ) try: domains.create() except Exception, e: LOG.exception('Exception while creating domains table, error' % str(e)) raise
#构造初始化信息 DOMAINDS = { 'NS': dict(name='test.com', rdata='ns0.test.com.', rdtype='NS', ttl=86400), 'SOA': dict(name='test.com', rdata='test.com. www.test.com. 200309181 28800 7200 86400 28800', rdtype='SOA', ttl=86400)} try: #逐行插入初始化信息 domain_insert = domains.insert() for name, values in DOMAINDS.iteritems(): domain_insert.execute({'name': values["name"], 'rdata': values["rdata"], 'rdtype': values["rdtype"], 'ttl': values["ttl"]}) except Exception, e: LOG.exception('Exception while seeding domains table, error: %s' % str(e)) raise
def downgrade(migrate_engine): ''' 删除表 ''' meta = MetaData() meta.bind = migrate_engine domains = Table('domains', meta, autoload=True) domains.drop() 第二步、生成表结构的模型,nova的表对象模型存在于nova.db.sqlalchemy.migrate_repo.models.py文件中,需要在这里面添加一个实现类,位置无所谓,但一般新添加的都放到文件的最末尾,这里我添加一个domains的模型类,代码如下: class Domain(BASE, PhysicalBase): """ 虚拟机主机名 """ __tablename__ = "domains" id = Column(Integer, primary_key=True) name = Column(String(255)) ttl = Column(Integer) rdtype = Column(String(255)) rdata = Column(String(255)) project_id = Column(String(255)) 第三步、实现表查询,因为nova使用的事sqlalchemy,所以查询只需要按照它的规则构造就行,数据库查询的方法都在nova.db.sqlalchemy.api.py文件中,可以实现增删改查功能,下边我实现一个创建功能的语句,代码如下: @require_admin_context def domain_create(context, values): if model_query(context, models.Domain, read_deleted="yes").\ filter(and_(models.Domain.name == values['name'], models.Domain.rdata == values['rdata'])).\ count() == 0: domain = models.Domain() domain.update(values) domain.project_id = context.project_id domain.save() return domain else: return None 第四步、添加一个表查询语句的封装结构,为了安全起见,nova的其它模块不直接调用执行数据库查询的方法,而是调用一个上层封装的方法。在nova.db.api.py文件中添加一行调用就行,nova的其它模块调用数据库接口都是引用的这个文件,代码如下: def domain_create(context, values): return IMPL.domain_create(context, values)
2.2、keystone表结构的扩展 Keystone的表结构扩展相对容易,虽然数据库中间件使用的也是sqlalchemy,但是在keystone中只需要在keystone.identity.backends.sql.py文件中实现一个抽象类就可以,使用命令执行keystone-manage db_sync后表结构就可以正常创建。 第一步,构建表结构,现在我添加一个表tenant_key,实现的代码如下: class TenantKey(sql.ModelBase, sql.DictBase): __tablename__ = 'tenant_key' id = sql.Column(sql.String(64), primary_key=True) tenant_id = sql.Column(sql.String(64), unique=True) extra = sql.Column(sql.JsonBlob())
@classmethod def from_dict(cls, tenant_dict): # shove any non-indexed properties into extra extra = {} for k, v in tenant_dict.copy().iteritems(): # TODO(termie): infer this somehow if k not in ['id', 'tenant_id', 'extra']: extra[k] = tenant_dict.pop(k)
tenant_dict['extra'] = extra return cls(**tenant_dict)
def to_dict(self): extra_copy = copy.deepcopy(self.extra) extra_copy['id'] = self.id extra_copy['tenant_id'] = self.tenant_id return extra_copy keystone中每个表都需要有extra这个字段,内容最多两个元素的字典数据,在接口调用时会把字典中的键值构造成表的字段,对应值的数据类型也会原封不变的恢复。比如一条记录extra的值为:{“addresses”: [“192.168.71.101”, “192.168.71.102”]},值是一个列表,存到数据库是原样存储的,接口返回时会还会被构造为一个列表。 每个实体类都有from_dict、to_dict两个方法,这个是用来接口和数据库进行数据转换时用的函数。
第二步,添加查询语句,查询语句需要在下边的Identity类中添加,下边是实现的创建tenant_key记录的代码: def create_tenant_key(self, tenant_id): session = self.get_session() with session.begin(): tenant_key_ref = TenantKey() tenant_key_ref.id = uuid.uuid4().hex tenant_key_ref.tenant_id = tenant_id tenant_key_ref.extra = {'public_key': public_key} session.add(tenant_key_ref) session.flush() return tenant_key_ref.to_dict()
第三步,添加调用封装,和nova的类似,需要加一层封装,这里的封装更像是一个java中的接口方法的添加,在keystone.identity.core.py的Driver添加一个方法,以供上层模块调用数据库接口,如果只是在backends.sql.py中内部调用的话,此步可以省略。
3、resetful接口服务的扩展 3.1、nova数据库调用接口服务的扩展 Openstack的所有模块之间的交互都是采用reset接口的方式实现的,所以掌握模块reset接口的扩展至关重要,而接口的扩展中数据库调用的接口相对容易实现。 Nova的接口服务都是在nova.api.openstack下边实现,而对于扩展或者发行版来说,openstack的版本升级是很大的一个隐患,所以接口的扩展一般都独立写成一个文件,或者一个单独的类、方法。 nova.api.openstack下边有两个模块,一个是compute这个主要是虚拟机和宿主机相关的接口实现,volume是快存储相关接口的实现。 第一步,实现接口的数据库调用模块,现在实现一个宿主机相关的接口文件,在nova.api.openstack.compute下边添加一个文件:machines.py,实现一个查询所有machine实例列表接口,代码如下: #-*- coding:utf-8 -*-
import webob.exc
from nova.api.openstack import common from nova.api.openstack.compute.views import machines as views_machines from nova.api.openstack import wsgi from nova.api.openstack import xmlutil from nova import exception from nova import flags from nova import db from nova import log as logging
LOG = logging.getLogger(__name__) FLAGS = flags.FLAGS
def make_machine(elem, detailed=False): elem.set('floating_ip') elem.set('name') elem.set('id')
if detailed: elem.set('updated') elem.set('created')
class MinimalMachinesTemplate(xmlutil.TemplateBuilder): def construct(self): root = xmlutil.TemplateElement('machines') elem = xmlutil.SubTemplateElement(root, 'machine', selector='machines') make_machine(elem) xmlutil.make_links(root, 'machines_links') return xmlutil.MasterTemplate(root, 1, nsmap=machine_nsmap)
class MachinesTemplate(xmlutil.TemplateBuilder): def construct(self): root = xmlutil.TemplateElement('machines') elem = xmlutil.SubTemplateElement(root, 'machine', selector='machines') make_machine(elem, detailed=True) return xmlutil.MasterTemplate(root, 1, nsmap=machine_nsmap)
class Controller(wsgi.Controller): _view_builder_class = views_machines.ViewBuilder
def __init__(self, **kwargs): super(Controller, self).__init__(**kwargs)
@wsgi.serializers(xml=MinimalMachinesTemplate) def index(self, req): """ 获取machines所有实例名字信息 """ context = req.environ['nova.context']
try: all_machines = db.machine_get_all(context) except exception.Invalid as e: raise webob.exc.HTTPBadRequest(explanation=str(e)) return self._view_builder.index(req, all_machines)
@wsgi.serializers(xml=MachinesTemplate) def detail(self, req): """ 获取machine所有列表的详细字段信息 """ context = req.environ['nova.context'] try: all_machines = db.machine_get_all(context) except exception.Invalid as e: raise webob.exc.HTTPBadRequest(explanation=str(e))
return self._view_builder.detail(req, all_machines)
def create_resource(): return wsgi.Resource(Controller())
MinimalMachinesTemplate、MachinesTemplate两个类用来进行接口数据输出时json格式的转化,先使用xml定义json的模板。 Index方法将在默认调用时显示,detail显示详细的数据参数,调用时在url的最后添加/detail就行。 req.environ['nova.context']用来获取请求头中的用户信息,只有符合权限的用户才能调用相应接口,返回对应用户的数据信息。 machine_get_all是nova.db.api.py中的方法,从文件最上边的模块导入中可以看到。 views_machines.ViewBuilder用来在接口返回时向模板填充数据 第二步,实现接口调用后的展示部分, 在nova.api.openstack.compute.views下添加machines.py文件,代码如下: #-*- coding:utf-8 -*-
import os.path
from nova.api.openstack import common from nova import flags from nova import utils
FLAGS = flags.FLAGS
class ViewBuilder(common.ViewBuilder):
_collection_name = "machine"
def basic(self, request, machine): return { "machine": { "id": machine.get("uuid"), "name": machine.get("domain_name"), "floating_ip": machine.get("floating_ip"), "links": self._get_links(request, machine["uuid"]), }, }
def show(self, request, machine): machine2dict = { "id": machine.get("uuid"), 'name': machine.get("domain_name"), 'floating_ip': machine.get("floating_ip"), "created": self._format_time(machine.get("created_at")), "updated": self._format_time(machine.get("updated_at")), "links": self._get_links(request, machine["uuid"]), } return dict(machine=machine2dict)
def detail(self, request, machines): list_func = self.show return self._list_view(list_func, request, machines)
def index(self, request, machines): list_func = self.basic return self._list_view(list_func, request, machines)
def _list_view(self, list_func, request, machines): machine_list = [list_func(request, machine)["machine"] for machine in machines] machines_links = self._get_collection_links(request, machines) machines_dict = dict(machines=machine_list)
if machines_links: machines_dict["machines_links"] = machines_links
return machines_dict
def _get_links(self, request, identifier): """Return a list of links for this machine.""" return [{ "rel": "self", "href": self._get_href_link(request, identifier), }, { "rel": "bookmark", "href": self._get_bookmark_link(request, identifier),
@staticmethod def _format_time(date_string): """Return standard format for given date.""" if date_string is not None: return date_string.strftime('%Y-%m-%d %H:%M:%S')
这个文件就属于web框架中MVC中的页面展示部分
|