存档

2014年10月 的存档

Glance 源码分析(3) – WSGI 框架

2014年10月27日 没有评论

Table of Contents

Server

OpenStack 大多数模块封装了 eventlet 以及 eventlet.wsgi.server 组成一个 class Server 来作为一个 HTTP RestFul API Server. 咋一看他的封装好像很复杂, 但是如果了解 eventlet 的基本概念, 这部分代码其实非常简单.

eventlet 在它内部定义的 greenthread(绿色线程) 里, 它利用协程来实现并发. 协程的介绍可以看我以前写的文章: Coroutine(协程) 介绍

为了彻底实现高性能的 I/O 并发, eventlet 甚至把底层的一些 API 都做了修改, .e.g socket, listen. 更多的 eventlet 的内容不在这讨论, 可以去 官网 查看相关的文档

下面是一个简单的小例子利用 eventlet 构建一个 HTTP Server, 基本上就是 glance 的 class Server 的核心骨架:

class APP(object):
    @webob.dec.wsgify
    def __call__(self, req):
        return 'hello world'
 
# 创建一个绑定在本机 8000 端口 的 socket.
sock = eventlet.listen(('0.0.0.0', 8000))
# 启动 wsgi server
eventlet.wsgi.server(sock, APP())

that’s it, so easy. 把他保存到文件并运行, 就是一个简单的 Http server, 并且 我可以付责任的告诉你, openstack 中的 WSGI 服务就是这么运行的.

所以我们回过头看 glance.common.wsgi.py:Server 这个类, 它只不过包装了一下 eventlet 的参数, 并且 WSGI 实例是在子进程里面运行的, 支持多进程运行该服务(多核考虑), 对外提供了 Server:start()Server:wait() 两个 API, 其他的, 就真没什么了.

paste 的工厂函数

请先复习一下 paste 的相关内容. 然后看下面的图:

API 框架

图中每一个 filter 都对应着一个工厂函数(可调用的实例), 比如:

[filter:versionnegotiation]
paste.filter_factory = glance.api.middleware.version_negotiation:VersionNegotiationFilter.factory

对应的实例都是 Middleware 其实是的子类, 前面我们讲过, paste配置里面的工厂函数 所创造的是一个能调用的实例(包含__call__方法), 当收到用户请求的时候就会自动调用该 实例, 也就是调用 __call__ 方法. 这里 openstack 所有模块抽象除了一个类 Middleware, 封装了 process_request 方法. 每个 filter 子类只需要继承 该类, 如果需要做处理, 就覆盖 process_request 方法, 然后 Middleware 里面 的 __call__ 方法会根据 process_request 的返回值来判断是否交给下一个 filter 处理.

# file: glance/api/middleware/version_negotiation.py
class VersionNegotiationFilter(wsgi.Middleware):
class Middleware(object):
    def __init__(self, application):
        self.application = application
 
    @classmethod
    def factory(cls, global_conf, **local_conf):
        def filter(app):
            return cls(app)
        return filter
 
    def process_request(self, req):
        return None
 
    def process_response(self, response):
        return response
 
    @webob.dec.wsgify
    def __call__(self, req):
        # 首先调用子类的 process_request 方法, 如果子类没有实现这个方法或者
        # 返回值不为空, 那么直接将子类的返回回复给用户, 否则进行下一个 filter
        # 的处理. 这其实是一个递归的过程, 最后返回从下游(filter或app)的到的返回
        # 给用户
        response = self.process_request(req)
        if response:
            return response
        response = req.get_response(self.application)
        response.request = req
        try:
            return self.process_response(response)
        except webob.exc.HTTPException as e:
            return e

分类: OpenStack 标签:

Glance 源码分析(2) – 配置文件

2014年10月25日 没有评论

这里我们会分析 glance-api 读取以下两个配置文件

  • glance-api.conf: glance-api 的用户配置文件
  • glance-api-paste.ini: glance-api 的 WSGI 配置文件

glance-api.conf

该配置文件的读取是利用 oslo 模块来实现的, oslo 提供了 .ini 格式的配置 文件的解析, 被所有 OpenStack 模块用来解析配置文件.

oslo 的用法很简单, 下面举个简单的例子:

from oslo.config import cfg
 
default_opts = [
    cfg.StrOpt('bind_host',
               default='0.0.0.0',
               help='IP address to listen on'),
    cfg.IntOpt('bind_port',
               default=9292,
               help='Port number to listen on')
]
 
app_opt = cfg.StrOpt('name',
                     default='blog',
                     help='name of this app')
 
# cfg.CONF 是在 oslo.cfg 模块中的一个全局变量, 首先我们需要得到一个它的引用
# 然后调用 register_opt() 注册我们需要解析的配置项, 或者使用 register_opts()
# 同时注册多个配置项
# 如果配置文件中可以找到配置项, 那么使用配置项中的值, 不然使用注册该配置项时指定
# 的默认值
CONF = cfg.CONF
CONF.register_opt(app_opt, group='app')
CONF.register_opts(default_opts)
CONF(default_config_files=['app.conf'])
 
# 使用的时候可以用 CONF.cfgname 来使用, 如果该 cfgname 不在 [DEFAULT] 段下,
# 那么使用 CONF.section.cfgname 来引用
print CONF.items()
print CONF.app.name
print CONF.bind_host
print CONF.bind_port
# file: app.conf
[DEFAULT]
bind_port = 8080
[app]
name = test
# python test.py 
[('bind_port', 8080), ('config_dir', None), ('config_file', ['app.conf']), ('bind_host', '0.0.0.0'), ('app', <oslo.config.cfg.GroupAttr object at 0x7fa4a3d75c50>)]
0.0.0.0
8080
test

那么问题来了… 前面讲过, glance 中的配置文件通过 config.parse_args() 来调用的, 也就是所有的配置都是在 glance.common.config:parse_args() 中完成的, 通过上面的分析, 这个函数其实很简单, 这里就不深入探讨了. 总之, 以后, glance 的各个模块在想访问用户提供的配置的时候, 只需要使用类似以下的代码就可以了:

from oslo.config import cfg
CONF = cfg.CONF
print CONF.enable_v2_api

glance-api-paste.ini

该内容和 glance 无关, 只要搞懂了 python paste 模块的用法, 就很简单了. 关于 paste 模块的使用说明, 请参考 python paste.deploy 探索

下面把之前在框架那里的内容再贴一份:

API 框架

[pipeline:glance-api-keystone]
pipeline = versionnegotiation authtoken context rootapp
[filter:versionnegotiation]
paste.filter_factory = glance.api.middleware.version_negotiation:VersionNegotiationFilter.factory
[filter:authtoken]
paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory
delay_auth_decision = true
[filter:context]
paste.filter_factory = glance.api.middleware.context:ContextMiddleware.factory
[composite:rootapp]
paste.composite_factory = glance.api:root_app_factory
/: apiversions
/v1: apiv1app
/v2: apiv2app

分类: OpenStack 标签:

Glance 源码分析(1) – 框架

2014年10月24日 没有评论

以下主要分析 V2 版本的代码, V1 和 V2的最大区别就是:

  • V1: 包含两个服务 glance-api 和 glance-registry. glance-api 接受客户端的 所有命令, 分发并响应, 涉及到数据库的操作由内部转发到 glance-registry 完成
  • V2 简化了流程, 所有的处理都在内部实现, 不需要额外的服务. 因此只需要 glance-api 一个服务

启动流程

启动流程

# glance/cmd/api.py
def main():
    # 载入配置, 下面的函数在 glance/common/config.py 中定义, 调用此函数会初始化
    # oslo 模块中的模块变量 CONF, 把 glance-api.conf 中的值设置在 CONF 的属性中,
    # 使用的时候按照 "CONF.enable_v1_api" 此方式调用, 具体的细节不多讲
    config.parse_args()
 
    # 初始化后端存储,
    # 1. 将所有后端存储的类名注册到 glance/store/__init__.py:REGISTERED_STORES
    # 2. 将所有后端存储的名字和实例注册到 glance/store/location.py:SCHEME_TO_CLS_MAP
    #    以后 glance-api 可以根据用户的请求和配置文件找到具体的后端存储的实例, 调用相应的
    #    实例的函数(add/delete)来操作
    glance.store.create_stores()
 
    # 启动 WSGI 程序
    # load_paste_app 会在默认位置(/etc/glance/)找到 glance-api-paste.ini,
    # 然后根据用户的配置(是否启用 keystone等)调用 paste.loadapp 载入相应的 app,
    # 最后传递给 server.start 启动 WSGI 应用, 具体的细节之后的系列会讲到
    server = wsgi.Server()
    server.start(config.load_paste_app('glance-api'), default_port=9292)
    server.wait()

API 框架

API 框架

# file: /etc/glance/glance-api-paste.ini
[pipeline:glance-api-keystone]
pipeline = versionnegotiation authtoken context rootapp
[filter:versionnegotiation]
paste.filter_factory = glance.api.middleware.version_negotiation:VersionNegotiationFilter.factory
[filter:authtoken]
paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory
delay_auth_decision = true
[filter:context]
paste.filter_factory = glance.api.middleware.context:ContextMiddleware.factory
[composite:rootapp]
paste.composite_factory = glance.api:root_app_factory
/: apiversions
/v1: apiv1app
/v2: apiv2app

关于 paste 模块的使用说明, 请参考 python paste.deploy 探索

流程如下:

  • 首先 WSGI 接收到用户请求, 将用户的请求信息交给 paste 模块处理
  • paste 模块根据配置文件的规则依次经过 versionnegotiation authtoken context 这几个过滤器
  • 最后交由 rootapp, 这是个类型为 app 的处理, rootapp 内部再根据用户提供的版本 信息(v1/v2) 交由 apiv1app 或者 apiv2app 处理, 最后把返回 HTTP Response
  • 真正的业务逻辑在 apiv1app/apiv2app 内部实现, 见下面的处理流程

处理流程

apiv2app 定义的工厂方法在 glance/api/v2/router:API.factory 中, API 类继承自 wsgi.Router, Router 类利用了 python-route 模块做 url 的选择处理, 具体的流程 见下(详细的分析请参考后面的系列): 处理流程 关于 route 模块的使用说明, 请参考 python Route 简单使用笔记

分类: OpenStack 标签:

Glance 源码分析(0) – 介绍

2014年10月23日 没有评论

这周我把 glance 的源代码通读了一遍(业务流程主要是 V2 版本), 感觉这个模块虽然是 OpenStack 里面相对较小的模块(vs Nova), 但是他的设计模式, 架构一点都不含糊, 我看完 glance 之后再去看其它项目, 发现 API 的等框架大部分地方都是一模一样. 也就是 说看完这个模块, 再去分析其它模块就省事很多了, 以下将我的分析拆分做一个记录:

分类: OpenStack 标签:

python Route 简单使用笔记

2014年10月15日 没有评论

Routes is a Python re-implementation of the Rails routes system for mapping URLs to application actions, and conversely to generate URLs. Routes makes it easy to create pretty and concise URLs that are RESTful with little effort.

Routes 其实就是 Python 版本的 Rails 的 routes. 它用来将用户的不同 URLs 自动匹配 到不同的应用上, 对于开发 RESTful 的 API 和其他 web 的应用非常方便.

例如下面根据官网上的例子修改的, 简单明了:

>>> from routes import Mapper
>>> map = Mapper()
>>> map.connect(None, "/error/{action}/{id}", controller="error")
>>> map.connect("home", "/", controller="main", action="index")
>>> print map
Route name Methods Path                
                   /error/{action}/{id}
home               /                   
>>> print map.match('/error/myapp/4')
{'action': u'myapp', 'controller': u'error', 'id': u'4'}
>>> print map.match('/')
{'action': u'index', 'controller': u'main'}

简单来讲, routes 的使用有以下简单的几步:

  1. 创建一个用来响应用户请求的控制器 C
  2. 创建一个 Mapper m
  3. 用 m.connect(或者 resource) 连接需要解析的 URLs 到 Controller 上
  4. 使用 routes.middleware.RoutesMiddleware 自动化 URLs 到 Controller 的处理

下面是一个例子

import webob.dec
import eventlet
from eventlet import wsgi, listen
from routes import Mapper, middleware
 
 
# 控制器
# 只有两个动作: index 和 add
class controller(object):
    def index(self):
        return "do index()"
 
    def add(self):
        return "do show()"
 
 
# WSGI App, 详情请参考我前面的文章 python paste.deploy 探索
class App(object):
    def __init__(self):
        # 上面提到的 1, 2, 3, 4
        self.controller = controller()
        m = Mapper()
        m.connect('blog', '/blog/{action}/{id}', controller=controller,
                  conditions={'method': ['GET']})
        self.router = middleware.RoutesMiddleware(self.dispatch, m)
 
    @webob.dec.wsgify
    def dispatch(self, req):
        # RoutesMiddleware 会根据接收到的 url, 自动调用 map.match(), 做路由匹配,
        # 然后调用第一个参数, 这里即是 self.dispatch()
        match = req.environ['wsgiorg.routing_args'][1]
        if not match:
            return 'error url: %s' % req.environ['PATH_INFO']
 
        # 根据用户的请求动作, 调用相应的 action 处理函数(index/add)
        action = match['action']
        if hasattr(self.controller, action):
            func = getattr(self.controller, action)
            ret = func()
            return ret
        else:
            return "has no action:%s" % action
 
    @webob.dec.wsgify
    def __call__(self, req):
        return self.router
 
if __name__ == '__main__':
    socket = listen(('0.0.0.0', 8000))
 
    server = eventlet.spawn(wsgi.server, socket, App())
    server.wait()

Table of Contents

其它

关于使用 Mapper.resource() 来批量创建 RESTful API, 这里不介绍了, 详情请到 官网 查看

分类: programming, python 标签: ,

python paste.deploy 探索

2014年10月15日 没有评论

简介

Paste Deployment 是一个用来查找和配置 WSGI 应用的系统. 只需要利用它提供的一个 简单的入口 loadapp, 就可以从配置文件或者 python EGG 里加载 WSGI 应用. 用户 所要做的, 仅仅是调用 loadapp 接口, 不需要暴露程序内部的实现细节.

配置文件说明

配置文件是 INI 格式 的, 被分为不同的段, 每个段的段名由 [类型:名字] 组成, 类型包括以下几种:

app

app 接受的参数(environ, start_response), app 需要完成的任务是响应 envrion 中的请求, 准备好响应头和消息体, 然后交给 start_response 处理, 并返回响应消息体. 这个比较好理解, 就是 WSGI 直接调用 app 指定的对象.

filter

filter 是过滤器, 和 python 中的装饰器是一个路子, 它接受一个 app 对象作为参数, 返回一个封装后的 app. 在一般的应用中, 可能在运行到最后一个 app 时候, 前面需要先 处理其他 filter, 如果在处理某一个 filter 的时候, 某些判断条件未通过, filter 有权直接返回, 不交由下面的模块继续处理, 比如认证未通过等.

filter-app

也是一个过滤器, 在某个应用只需要一个过滤器的时候, 一般用该类型, 它需要一个 next 字段指定这个 filter 应用到哪个应用上.

pipeline

如果需要使用多个 filter 过滤一个应用, 需要使用 pipeline 的方式, 他的配置就是一个 名为 pipeline 的 key, value 是以多个 filter和最后的一个应用结尾的列表: 如

[pipeline:main]
pipeline = filter1 filter2 filter3 app

composite

用来完成将将一个请求调度定向(dispatched)到多个(多种)应用上. 比如应用有 v1, v2 的版本, 就可以用 composite 来做调度. composite 其实像是 app, 但是实际上是由 多个应用组成. 比如下面是 openstack glance 的使用

[composite:rootapp]
paste.composite_factory = glance.api:root_app_factory
/: apiversions
/v1: apiv1app
/v2: apiv2app

使用示例

app

下面的例子展示了一个只有名为 blog 的app, 配置文件指定了 app_factory 为 Blog.factory, 这个工厂函数会创建一个 Blog 的实例, 当然你需要实现该类的 __call__ 方法, 这样 wsgi 就会在适当的使用调用你完成你想要做的.

# file: app.ini
[app:blog]
paste.app_factory = app:Blog.factory
# use = call:app:blog_app
#!/usr/bin/python
#encoding: utf-8
 
# file: app.py
 
import os
import eventlet
from eventlet import wsgi, listen
from paste import deploy
 
cfg_file = 'app.ini'
 
 
class Blog(object):
    def __init__(self):
        pass
 
    # 工厂函数, ini 配置文件中指定的值会找到这个函数来创建该 app 的实例
    @classmethod
    def factory(cls, global_conf, **local_conf):
        return cls()
 
    # python 的对象调用机制, 简单的来讲就是 python 中对象分为可调用的和不可调用的
    # 有 __call__ 方法的可以调用, 这样 ini 中指定的配置项的值调用的时候就直接调用到
    # 这个函数了
    def __call__(self, environ, start_response):
        # start_response 把用户传递的 HTTP status 和 headers 记录然后返回
        start_response('200 OK', {("Content-type", "text/plain")})
        return 'welcome to my blog\n'
blog_app = Blog.factory
 
if __name__ == '__main__':
    socket = listen(('0.0.0.0', 8000))
    # paste 提供的入口函数
    app = deploy.loadapp('config:%s' % os.path.abspath(cfg_file), 'blog')
    server = eventlet.spawn(wsgi.server, socket, app)
    server.wait()

一个复杂的例子

下面是一个复杂的例子, 先来看他的 ini 文件:

[composite:wiki]
use = egg:Paste#urlmap
/: home
/v1: wikiv1
 
[filter-app:home]
paste.filter_factory = app:WikiFilter.factory
next = homeapp
 
[app:homeapp]
paste.app_factory = app:Wiki.factory
 
 
[pipeline:wikiv1]
pipeline = logip logmethod v1
 
[filter:logip]
paste.filter_factory = app:LogIPFilter.factory
 
[filter:logmethod]
paste.filter_factory = app:LogMethodFilter.factory
 
[app:v1]
paste.app_factory = app:V1.factory

app 名为 wiki, 首先它是一个 composite 的类型, 根据前面说的(egg.Paste 这个是一个 app), 它会根据 url 对应不同的应用, 比如如果是 http://localhost/ 它就到 home, http://localhost/v1, 就到 wikiv1

home 是只有一个 filter 过滤的 app, filter 对应的工厂函数为 Wiki.factory, 它过滤后 在调用 homeapp, 这个 homeapp 就和前面的 blog一样只是一个简单的 app

wikiv1 是一个 pipeline, 它经过 logip(记录对方 IP), logmethod(记录 HTTP medhod) 这两个 filter, 然后到 v1 的 app

相关代码如下:

class Wiki(object):
    def __init__(self):
        pass
 
    @classmethod
    def factory(cls, global_conf, **local_conf):
        return cls()
 
    def __call__(self, environ, start_response):
        start_response('200 OK', {("Content-type", "text/plain")})
        return 'welcome to my wiki\n'
 
 
class Middleware(object):
    def __init__(self, app):
        self.app = app
 
    @classmethod
    def factory(cls, global_conf, **kwargs):
        def filter(app):
            return cls(app)
        return filter
 
 
class WikiFilter(Middleware):
    def __init__(self, app):
        super(WikiFilter, self).__init__(app)
 
    def __call__(self, environ, start_response):
        req = Request(environ)
        if req.method == 'PUT':
            start_response('200 OK', {("Content-type", "text/plain")})
            return 'Bad request\n'
        else:
            return self.app(environ, start_response)
 
 
class LogIPFilter(Middleware):
    def __init__(self, app):
        super(LogIPFilter, self).__init__(app)
 
    def __call__(self, environ, start_response):
        print 'request IP is: %s' % environ['REMOTE_ADDR']
        return self.app(environ, start_response)
 
 
class LogMethodFilter(Middleware):
    def __init__(self, app):
        super(LogMethodFilter, self).__init__(app)
 
    def __call__(self, environ, start_response):
        print 'request method is: %s' % environ['REQUEST_METHOD']
        return self.app(environ, start_response)
 
 
class V1(object):
    @classmethod
    def factory(cls, global_conf, **local_conf):
        return cls()
 
    def __call__(self, environ, start_response):
        start_response('200 OK', {("Content-type", "text/plain")})
        return 'welcome to my V1 wiki\n'

Resources

完整的代码点击 这里, 配置文件请点击 这里

分类: programming, python 标签: ,

搭建一个最简单的 glance 服务

2014年10月14日 没有评论

OpenStack 的各个模块是高度独立的, glance, neutron, 并且可以给其他的程序使用, e.g. ovirt, 下面的文档描述使用最简单的方式搭建一个 glance 服务.(以 ubuntu 为例)

安装 glance

# apt-get install glance

That’s all!

上面搭建的 glance 是使用 sqlite 作为后端数据库, 没有使用认证的服务.

issue

在 icehouce 版本中, 由于配置文件解析和兼容的关系, 有一个 bug, 需要手动 在指定默认的 sqlite 数据库位置.

/etc/glance/glance-api.conf/etc/glance/glance-registry.conf[default] 下, 添加一个配置

connection = sqlite:////var/lib/glance/glance.sqlite

然后重新同步一下:

# glance-manage db_sync
# service glance-api restart
# service glance-registry restart

使用

由于使用 glanceclient 的话, 默认需要加上认证的信息, 所以只能通过 API 的方式 使用, 这里我使用 curl 作为测试工具:

获取 images 列表

# curl http://192.168.3.33:9292/v2/images | python -mjson.tool

下载 images

# curl -o test.img http://192.168.3.33:9292/v2/images/ce252e1a-131a-4ebd-a9b0-0cf462f066e6/file

上传 image

# curl -i -X POST -H 'Content-Type: application/octet-stream' -H 'x-image-meta-disk_forma2' \
  -H 'x-image-meta-container_format: bare' -H 'Transfer-Encoding: chunked' \
  -H 'User-Agent: python-glanceclient' -H 'x-image-meta-is_public: False' \
  -H 'x-image-meta-name: test' -H 'x-image-meta-size: 197120' \
  --data-binary @test.qcow2 http://192.168.3.33:9292/v1/images

删除 image

# curl -X DELETE http://192.168.3.33:9292/v2/images/d7aa01fc-5999-4720-8a05-325f7ffb9332

分类: OpenStack 标签:

cloud-init 探索

2014年10月9日 6 条评论

背景

Amazon 的 EC2 有一个叫 CloudFormation 的服务, 用来定制基于同一个模板的不 同实例. 通过一个本地能访问到的 API, 实例可以获取由 EC2 提供的 实例独有 的 数据和信息(比如 IP地址, 主机名, MAC 地址等等)在启动的时候初始化自己, 这个服 务在自动部署成千上万的实例的时候给用户提供了极大地方便.

cloud-init 就是一个运行在实例里面并用来初始化实例的一个框架. 它利用这些 cloud provider(e.g. CloudFormation, nova metadata service) 提供的信息在实例 启动的时候初始化该实例, 例如, 修改主机名, 创建 ssh 密钥, 将用户的 ssh 公共密钥 导入实例中, 配置软件源, 配置 puppet, chef 等.

和其他配置工具(e.g. puppet)的区别

既然已经有 puppet/chef 等配置工具了, 为什么还需要 cloud-init 来做配置实例的 事情呢? 首先注意一个前提, 使用 puppet 这些工具之前, 需要首先安装配置好 puppet, 设置好网络, DNS, 主机名, puppet master 信息等.

当然用户也可以把这些配置步奏提前在实例基于的模板上完成, 对于部署少量的实例来说, 这样做事可行的, 但是对大规模的部署应用, 这样既费时有费力. 所以 cloud-init 不是 puppet 等工具可以取代的.

简单的例子

下面先通过一个简单的例子来感受一下 cloud-init.

# cat userdata6.txt 
#cloud-config
ssh_pwauth: true
disable_root: 0
user: root
password: abc123
chpasswd:
  expire: false
  • 首先, 这个文件把一般云主机里面的 ssh 密码认证打开, 默认关闭的
  • 然后, 通过 disable_root 允许 root 认证, 默认关闭
  • 将 root 的密码设置为 abc123
  • 如果不设置 chpasswd 的 expire 为 false, 那么登陆的时候会提示马上修改密码才能进去

然后启动一个实例

# nova boot --flavor 7 --key_name mykey --image fedora20 --nic net-id=[NIC-ID] --user-data ~/userdata6.txt vm

登陆

# ip netns exec qrouter-e7985db5-f7d4-4811-892a-d8f7035f169a ssh root@10.11.0.28
Warning: Permanently added '10.11.0.28' (RSA) to the list of known hosts.
root@10.11.0.28's password:

实现细节

在 EC2/OpensStack 有一个 metadata 的服务, 这个服务接受对于 http://169.254.169.254 的请求, 提供实例需要的数据:

# curl http://169.254.169.254
1.0
2007-01-19
2007-03-01
2007-08-29
2007-10-10
2007-12-15
2008-02-01
2008-09-01
2009-04-04
latest
 
# curl http://169.254.169.254/latest/meta-data/
ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
hostname
instance-action
instance-id
instance-type
kernel-id
local-hostname
local-ipv4
placement/
public-hostname
public-ipv4
public-keys/
ramdisk-id
reservation-id
security-groups
 
# curl http://169.254.169.254/latest/meta-data/local-ipv4
10.11.0.28
 
# curl http://169.254.169.254/latest/meta-data/public-ipv4
192.168.3.117
 
# curl http://169.254.169.254/latest/user-data
#cloud-config
ssh_pwauth: true
disable_root: 0
user: root
password: abc123
chpasswd:
  expire: false

然后 cloud-init 利用 user-data 做初始化云主机的操作.

格式

User data 是 cloud-init 的核心内容, 它支持多种格式: e.g. 脚本, cloud-config

格式例子

user script

#! 的文件或者以 Content-Type: text/x-shellscript 开始的 MIME 格式

#!/usr/bin/python
import os
os.system('echo "test about user data" &gt;&gt; /tmp/userdata')
# ip netns exec qrouter-e7985db5-f7d4-4811-892a-d8f7035f169a ssh fedora@10.11.0.31
[fedora@vm3 ~]$ cat /tmp/userdata
test about user data

Cloud Config Data

Cloud-config 是 cloud-init 支持的格式, 通过这种格式, 可以很方便的完成很多 任务:

  • apt 升级
  • yum 升级
  • 设置 DNS, 网络文件
  • ……

该格式内部定义了很多语法, 方便用户的部署, 比如:

更新 hostname:

#cloud-config
 
manage_etc_hosts: true
fqdn: eayunstack.domain

创建新文件:

#cloud-config
 
write_files:
-   encoding: b64
    content: YWJjMTIzCg== 
    owner: root:root
    path: /var/b64_test
    permissions: '0644'
-   content: |
        EayunStack VM 
    path: /etc/issue

MIME

怎样同时传递多种格式/多个文件的 user-data 呢, 可以通过 MIME 来包装多个文件:

通过下面的 python 脚本, 将多个文件封装成一个 cloud-init 支持的 MIME 文件传递 给云主机

#!/usr/bin/python
 
# filename: mime.py
 
import sys
 
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
 
if len(sys.argv) == 1:
    print("%s input-file:type ..." % (sys.argv[0]))
    sys.exit(1)
 
combined_message = MIMEMultipart()
for i in sys.argv[1:]:
    (filename, format_type) = i.split(":", 1)
    with open(filename) as fh:
        contents = fh.read()
    sub_message = MIMEText(contents, format_type, sys.getdefaultencoding())
    sub_message.add_header('Content-Disposition', 'attachment; filename="%s"' % (filename))
    combined_message.attach(sub_message)
 
print(combined_message)
# ./test.py python.txt:x-shellscript userdata5.txt:cloud-config
Content-Type: multipart/mixed; boundary="===============1752240832932743336=="
MIME-Version: 1.0
 
--===============1752240832932743336==
MIME-Version: 1.0
Content-Type: text/text/x-shellscript; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="python.txt"
 
#!/usr/bin/python
import os
os.system('echo "test about user data" &gt;&gt; /tmp/userdata')
 
--===============1752240832932743336==
MIME-Version: 1.0
Content-Type: text/text/cloud-config; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="userdata5.txt"
 
#cloud-config
 
manage_etc_hosts: true
fqdn: eayunstack.domain
 
--===============1752240832932743336==--

Include file

#include
http://192.168.3.30:8000/python.txt
http://192.168.3.30:8000/host.txt

Upstart job

Part Handler

这种形式的 user-data 可以让用户以 python 脚本的方式编写 cloud-init 的插件.

它其实只是一个 python 代码片段, 必须实现 list_types() 和 handle_part() 方法, list_types() 指定了能被该模块处理的 MIME 类型, handle_part() 是具体 的处理逻辑, 他接受用户传递的 MIME 的数据, 处理这些数据, 达到用户的需求.

下面的例子展示使用用户传递的用户名创建用户:

首先根据用户提供的文件创建传递给云主机的 user-data

# cat one 
dunrong
hunt
# cat two 
lee
coffee
# ./test.py part-handler.txt:part-handler one:plain two:plain &gt; user-data.txt
# cat part-handler.txt 
#part-handler
 
def list_types():
    # 放回一个能被这个模块处理的 MIME 类型的列表
    return(["text/plain", "text/go-cubs-go"])
 
def handle_part(data,ctype,filename,payload):
    # data: the cloudinit object
    # ctype: '__begin__', '__end__', 或者是用户指定的 MIME 类型
    # filename: 用户传递的文件名
    # payload: 用户传递的文件内容
    if ctype == "__begin__":
       print "my handler is beginning"
       return
    if ctype == "__end__":
       print "my handler is ending"
       return
 
    print "==== received ctype=%s filename=%s ====" % (ctype,filename)
    import os
    for user in payload.splitlines():
        print " == Creating user %s" % (user)
        os.system('useradd -p linux -m %s' % (user) ) # TODO
    print "==== end ctype=%s filename=%s" % (ctype, filename)
# ip netns exec qrouter-e7985db5-f7d4-4811-892a-d8f7035f169a ssh fedora@10.11.0.39
[fedora@vm3 ~]$ ls /home/
coffee  dunrong  fedora  hunt  lee
 
[fedora@vm3 ~]$ less /var/log/cloud-init.log
Oct  8 09:45:19 localhost cloud-init: my handler is beginning
Oct  8 09:45:19 localhost cloud-init: ==== received ctype=text/plain filename=one ====
Oct  8 09:45:19 localhost cloud-init: == Creating user dunrong
Oct  8 09:45:19 localhost cloud-init: == Creating user hunt
Oct  8 09:45:19 localhost cloud-init: ==== end ctype=text/plain filename=one
Oct  8 09:45:19 localhost cloud-init: ==== received ctype=text/plain filename=two ====
Oct  8 09:45:19 localhost cloud-init: == Creating user lee
Oct  8 09:45:19 localhost cloud-init: == Creating user coffee
Oct  8 09:45:19 localhost cloud-init: ==== end ctype=text/plain filename=two
Oct  8 09:45:19 localhost cloud-init: my handler is ending

目录布局

数据来源

数据来源说的是 user-data/metadata 的出处, 随着历史的发展, 有以下几种:

  • EC2 由于历史的原因, 这是最流行的数据来源, 它通过一个 http 服务(IP:169.254.169.254) 来提供 user-data.
  • Config Drive 这是 OpenStack 使用的方式(ovirt 目前也是用这种方式)
  • OpenNebula
  • Alt cloud
  • No cloud
  • MAAS
  • CloudStack
  • OVF
  • Fallback/None
# nova boot --config-drive true --image fedora20 --key-name mykey --flavor 8 --user-data ./userdata6.txt --nic net-id=b4af9baa-be56-4958-8643-feaeb7b21dea --file /etc/network/interfaces=/root/interfaces --file known_hosts=/root/.ssh/known_hosts --meta role=webservers --meta essential=false vm
# ip netns exec qrouter-e7985db5-f7d4-4811-892a-d8f7035f169a ssh root@10.11.0.42
[root@vm ~] # mount  /dev/disk/by-label/config-2 /mnt
[root@vm ~] # ls /mnt/
ec2  openstack
[root@vm ~]# ls /mnt/openstack/
2012-08-10  2013-04-04  2013-10-17  content  latest
[root@vm ~]# ls /mnt/openstack/latest/
meta_data.json  user_data  vendor_data.json

建立支持 cloud-init 的镜像

分类: Virtualization 标签: