Monthly Archives: December 2014

使用cloud-init实现虚拟机信息管理

为什么要用cloud-init

不同种类的设备VM启动总是一件非常麻烦的事情,例如安全设备有WAF、IPS等,每种设备的网络接口、启动脚本互不一样,即便同一种设备,其主机名、网络地址等也不一样。那么如何对这些VM启动过程进行管理,并完成所有数据的配置呢?

在这之前,我的实习生是怎么做的:将一台VM的管理口网络地址设置为192.168.2.100,然后每次启动实例之后定时访问http://192.168.2.100/somepath,当成功访问这个页面之后,使用REST接口配置该机器的IP地址为所需的新地址(如200.0.0.2);这个时候网络会短暂不同,然后在访问http://200.0.0.2/somepath,当成功访问之后,接下来配置各种值。 整个过程比较麻烦,所有的配置都需要实现REST接口,无法做到自定义启动脚本的更新;最不可接受的是,这个过程是串行的,当要启动100个VM时,只能一个VM一个VM顺序启动,否则两个VM都有同一个地址(192.168.2.100),那么网络访问就可能出现问题了。 不过受到各种Stack管理虚拟机用到cloud-init的启发,我认为我们也可以使用这套工具实现上述过程的。

什么是cloud-init

cloud-init(简称ci)在AWS、Openstack和Cloudstack上都有使用,所以应该算是事实上的云主机元数据管理标准。那么问题来了,google相关的文档,发现中文这方面几乎没有,Stacker你们再搞虾米呢?当然话说回来英文的资料除了官网外几乎也没有什么,我花了近一周的时间才弄明白了。

首先要明确的是cloud-init在工作之前,VM是从DHCP服务器获取到了IP,所有DHCP发现不是cloud-init的事情。当你在Openstack中用ubuntu cloud VM启动卡在cloud-init界面时,多半是因为DHCP还没获取IP,而不是cloud-init本身的问题。那么cloud-init主要走什么呢?它向一台数据服务器获取元数据(meta data)和用户数据(user data),前者是指VM的必要信息,如主机名、网络地址等;后者是系统或用户需要的数据和文件,如用户组信息、启动脚本等。当cloud-init获取这些信息后,开始使用一些模块对数据进行处理,如新建用户、启动脚本等。

cloud-init工作原理

首先,数据服务器开启HTTP服务,cloud-init会向数据服务器发送请求,确认数据源模块,依次获取版本、数据类型和具体数据内容信息。

确认数据源模块

cloud-init会查找/etc/cloud/cloud.cfg.d/90_dpkg.cfg中的datasource_list变量,依次使用其中的数据源模块,选择一个可用的数据源模块。如我的配置文件中:datasource_list: [ Nsfocus, NoCloud, AltCloud, CloudStack, ConfigDrive, Ec2, MAAS, OVF, None ],那么ci首先调用$PYTHON_HOME/dist-packages/cloudinit/sources/DataSourceNsfocus.py中类DataSourceNsfocus的get_data函数,当且仅当访问链接DEF_MD_URL为正常时,这个数据源被认为是OK的。

在我的实践中,CloudStack的DEF_MD_URL为DHCP的服务器ip,而Openstack和AWS则为一个常值169.254.169.254,然后在宿主机的中做一个iptables重定向,这样就到了我们的服务器监听端口8807:

$ sudo ip netns exec ns-router iptables -L -nvx -t nat
Chain PREROUTING (policy ACCEPT 169850 packets, 21565088 bytes)
    pkts      bytes target     prot opt in     out     source               destination         
      47     2820 REDIRECT   tcp  --  *      *       0.0.0.0/0            169.254.169.254      tcp dpt:80 redir ports 8807
$ sudo ip netns exec ns-router iptables -L -nvx
Chain INPUT (policy ACCEPT 97027 packets, 8636621 bytes)
    pkts      bytes target     prot opt in     out     source               destination         
       0        0 ACCEPT     tcp  --  *      *       0.0.0.0/0            127.0.0.1            tcp dpt:8807

一些系统假设

需要说明的是,虽然每个数据源访问的入口都是get_data,但每个数据服务的格式和位置是不一样的,元数据可能在/nsfocus/latest/metadata/,也可能在/latest/metadata.json,也就是说数据源模块根据自己系统的规定,访问相应的数据,并根据ci的规定,指定如何将这些数据与ci接下来的处理模块对应上。

那么我们的数据访问地址是这样的:

--namespace
           |
           |------version
                        |
                        |---------meta_data.json
                        |---------meta_data
                        |                  |---------public-hostname
                        |                  |---------network_config
                        |
                        |---------user_data

其中,namespace为nsfocus,meta_data.json是一个json文件,里面包含所有元数据。
其次,我们的数据服务器IP为111.0.0.2

获得元数据

因为获取是HTTP的形式,所以以curl为例说明下面过程:

$ curl http://111.0.0.2/nsfocus
1.0
latest
$ curl http://111.0.0.2/nsfocus/latest
meta_data
user_data
meta_data.json
$ curl http://111.0.0.2/nsfocus/latest/meta_data
public-hostname
local-ipv4
network_config
...
$ curl http://111.0.0.2/nsfocus/latest/meta_data/local-ipv4
111.0.0.11
$ curl http://111.0.0.2/nsfocus/latest/meta_data.json
{"files": {}, "public_keys": {"controller": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCxtEfzf8I0jA7IHDRHJtDq3nTcTXAWgFYEsAV0i7WU6v8gvFr/R+DTvkVdFGgbM/qhVNWUehmPicENac6xldbL5ov6J7c8Y+UytPwJCt13IzDHXaL1BxVYUV6dpe6SYGYohNQ2KZYkG/95NzjxI1Max5DDvU8mbpEz/KyphowseburknQTkOTEigJ7CKM4G1eGVhBHKRHXbNsoPZwJnqvIHIpDcwGaj+OgVGF+o3ytH4twrwNwUFiWrUaxo9j2uRTSejYRh1eC9KOYXTnXInzV1xCVHYs/x+eIzav+2oM8hgR3xr1efgSU2sMzXrp+mJAPzHaAyAat+s7AMDu9tKrd marvel@marvel-ThinkPad-X230"}, "hostname": "waf-ba-0001", "id": "waf-ba-0001", "network_config": {"content_path": "latest/meta_data/network_config"}}

这个meta_data.json是我们参考Openstack的标准,自己实现的。当获得meta_data.json后,DataSourceNsfocus解析里面的字段,填入自己的数据结构中,如放入DataSourceNsfocus的result字典中。

            if found and translator:
                try:
                    data = translator(data)
                except Exception as e:
                    raise BrokenMetadata("Failed to process "
                                         "path %s: %s" % (path, e)) 
            if found:
                results[name] = data

这样,如hostname就存为self.result[‘meta’][‘hostname’]。

供其他处理模块使用的获取元数据函数

在上一阶段,元数据的提供、获取和存储都是很自由的,那么这些数据怎么被使用,例如hostname怎么设置呢?那就需要根据ci的标准实现一些接口,如设置hostname就需要我们实现DataSourceNsfocus的get_hostname方法:

    def get_hostname(self, fqdn=False):
        return self.metadata.get("hostname")

这样,其他模块如set_hostname和update_hostname就会使用这个方法正确设置主机名了。如果你想设置其他数据,可参考cloud-init数据源参考的介绍。了解还有哪些处理模块,可读一下/etc/cloud/cloud.cfg文件。

至此,一些VM所需的常用配置已经搞定,那么如果我们想做一些流程方面的自动下发和运行该怎么做呢?则需要设置一下user_data。

获取用户数据

用户数据包括几类:

  • 配置文件(Cloud Config Data),类型为Content-Type: text/cloud-config,系统配置文件,如管理用户等,与/etc/cloud下的cloud.cfg最后合并配置项,更多的配置细节参考 配置样例
  • 启动任务(Upstart Job),类型为Content-Type: text/upstart-job,建立Upstart的服务
  • 用户数据脚本(User-Data Script),类型为Content-Type: text/x-shellscript,用户自定义的脚本,在启动时执行
  • 包含文件(Include File),类型为Content-Type: text/x-include-url,该文件内容是一个链接,这个链接的内容是一个文件,
  • (Cloud Boothook),类型为Content-Type: text/cloud-boothook,
  • 压缩内容( Gzip Compressed Content),
  • 处理句柄(Part Handler),类型为Content-Type: text/part-handler,内容为python脚本,根据用户数据文件的类型做相应的处理
  • 多部分存档(Mime Multi Part archive),当客户端需要下载多个上述用户数据文件时,可用Mime编码为Mime Multi Part archive一次下载

实例

我在data目录下面建立三个文件:

cloud.config

groups:
  - nsfocus: [nsfocus]
users:
  - default
  - name: nsfocus
    lock-passwd: false 
    sudo: ALL=(ALL) NOPASSWD:ALL
system_info:
  default_user:
    name: nsfocus
    groups: [nsfocus,sudo]
bootcmd:
  - echo "#HOSTS\n127.0.0.1    localhost\n::1    localhost ip6-localhost\nff02::1    ip6-allnodes\nff03::1    ip6-allrouters\n#ip#    #host#" > /etc/hosts
runcmd:
  - [echo, "RUNCMD: welcome to nsfocus-------------------------------------------"]
final_message: "Welcome to NSFOCUS SECURITY #type#====================================="

这是一个cloud-config文件,内容表示新建一个nsfocus的用户,归于nsfocus和sudo组,在启动时运行bootcmd的命令更新hosts,启动最后输出final_message。

nsfocus-init.script

$ cat nsfocus-init.script 
#!/bin/bash
echo "this is a startup script from nsfocus" 
echo "this is a startup script from nsfocus" >> /tmp/nsfocus-init-script

这是一个测试脚本,在系统启动时会被调用

nsfocus-init.upstart

$ cat nsfocus-init.upstart 
description "a nsfocus upstart job"

start on cloud-config
console output
task
script
echo "====BEGIN======="
echo "HELLO From nsfocus Upstart Job, $UPSTART_JOB"
echo "HELLO From nsfocus Upstart Job, $UPSTART_JOB" >> /tmp/hello
echo "=====END========"
end script

这是一个测试访问,在系统启动时会被启动

HTTP服务器收到/nsfocus/latest/user_data时,作如下处理:

from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
def encode_mime(fps):
    combined_message = MIMEMultipart()
    for fn, patterns in fps:
        print fn
        (filename, format_type) = fn.split(":", 1)
        print filename
        print "---"
        with open(filename) as fh: 
            contents = fh.read()
        for (p, v) in patterns:
            contents = contents.replace(p, v)
    
        sub_message = MIMEText(contents, format_type, sys.getdefaultencoding())
        sub_message.add_header('Content-Disposition', 'attachment; filename="%s"' % (filename[filename.rindex("/")+1:]))
        combined_message.attach(sub_message)
    return str(combined_message)

#main process
#....blablabla
      if subtype == "user_data":
            if len(arr) == 0:
                res = encode_mime([
                    ("./data/nsfocus-init.upstart:upstart-job",[]),
                    ("./data/nsfocus-init.script:x-shellscript",[]),
                    ("./data/cloud.config:cloud-config",[('#ip#', device.management_ip), ('#host#',device.id), ('#type#', device.type)])])
                return self.gen_resp(200, res)

虚拟机启动之后,服务器收到请求,返回下面的内容:

From nobody Fri Dec 26 15:34:36 2014
Content-Type: multipart/mixed; boundary="===============5883341837158849895=="
MIME-Version: 1.0

--===============5883341837158849895==
MIME-Version: 1.0
Content-Type: text/upstart-job; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="nsfocus-init.upstart"

description "a nsfocus upstart job"

start on cloud-config
console output
task
script
echo "====BEGIN======="
echo "HELLO From nsfocus Upstart Job, $UPSTART_JOB"
echo "HELLO From nsfocus Upstart Job, $UPSTART_JOB" >> /tmp/hello
echo "=====END========"
end script

--===============5883341837158849895==
MIME-Version: 1.0
Content-Type: text/x-shellscript; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="nsfocus-init.script"

#!/bin/bash
echo "this is a startup script from nsfocus" 
echo "this is a startup script from nsfocus" >> /tmp/nsfocus-init-script

--===============5883341837158849895==
MIME-Version: 1.0
Content-Type: text/cloud-config; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud.config"

groups:
  - nsfocus: [nsfocus]
  - dev
users:
  - default
  - name: nsfocus
    lock-passwd: false 
    sudo: ALL=(ALL) NOPASSWD:ALL
system_info:
  default_user:
    name: nsfocus
    groups: [nsfocus,sudo]
bootcmd:
  - echo "#HOSTS\n127.0.0.1    localhost\n::1    localhost ip6-localhost\nff02::1    ip6-allnodes\nff03::1    ip6-allrouters\n111.0.0.12    waf-ba-0001" > /etc/hosts
runcmd:
  - [echo, "RUNCMD: welcome to nsfocus-------------------------------------------"]

final_message: "Welcome to NSFOCUS SECURITY waf====================================="

--===============5883341837158849895==--

VM启动界面打印如下信息,且主机名变成了我们预定的值,说明确实获取meta-data和user-data成功,脚本运行也成功了。不过要说明一点,upstart在Ubuntu上没问题,但Debian没通过,可能当前阶段Debian的启动机制还有一些区别,所以还是使用bootcmd或启动脚本的方式启动。

Ubuntu登陆页面

参考文献

cloud-init数据源参考 http://cloudinit.readthedocs.org/en/latest/topics/datasources.html
dnsmasq参考 http://www.thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html
更多样例 https://github.com/number5/cloud-init/blob/master/doc/examples/

Openstack节点网卡连Cisco交换机出现环路的处理

前一阵子部署了两台Openstack的计算节点,奇怪的是每隔一段时间连管理网的网卡eth0灯不亮,交换机上对应的19端口灯变橙色,登陆上去一看,eth0显示:

2: eth0:  mtu 1500 qdisc mq master ovs−system state DOWN mode DEFAULT group default qlen 1000

运行ip link set eth0 up无效。

登陆交换机,查看端口状态:

Switch>enable 
Switch#show interfaces gigabitethernet 0/19 status 
Port    Name       Status       Vlan       Duplex  Speed Type
G0/19   err−disabled 100          full   1000 1000BaseSX

可以看到19端口出现问题,继续看日志

Switch#show logging
...
Mar 30 01:43:39.827: %ETHCNTR−3−LOOP_BACK_DETECTED: Loop−back detected on GigabitEthernet0/19.
Mar 30 01:43:39.827: %PM−4−ERR_DISABLE: loopback error detected on Gi0/19, putting Gi0/19 in err−disable state
Mar 30 01:43:40.833: %LINEPROTO−5−UPDOWN: Line protocol on Interface GigabitEthernet0/19, changed state to down
Mar 30 01:43:41.840: %LINK−3−UPDOWN: Interface GigabitEthernet0/19, changed state to down

发现原来是出现了环路,参考cisco的官方文档,一个可能的原因是交换机定时发送keepalive,这个数据包进入节点后又出来,到了同一个端口,则交换机认为可能出现环路,从而禁用。

解决办法是禁用keepalive:

Switch(config)#interface gigabitethernet 0/19 
Switch(config−if)#no keepalive

当然也可以升级到高版本的IOS,默认会禁用keeplive。

然后恢复出错的端口:

Switch(config)#errdisable recovery cause loopback 
Switch(config)#exit
Switch#show errdisable recovery
...
Interfaces that will be enabled at the next timeout:  
Interface       Errdisable reason       Time left(sec)
−−−−−−−−−       −−−−−−−−−−−−−−−−−       −−−−−−−−−−−−−−
Gi0/9                   loopback          205
Gi0/10                  loopback          205
Gi0/19                  loopback          205

过了300秒后,端口应该恢复正常,此时能够连接上主机了。

我觉得也可以在节点侧,在连接eth0的ovs桥br0加stp,以观后效

root@node4:~# ovs−vsctl get bridge br0 stp_enable
false
root@node4:~# ovs−vsctl set bridge br0 stp_enable=true

参考文章

Cisco交换机命令行配置(http://www.cisco.com/c/en/us/td/docs/switches/datacenter/nexus5000/sw/configuration/guide/cli_rel_4_0_1a/CLIConfigurationGuide/sm_syslog.html)

Cisco交换机端口出错处理(http://www.cisco.com/c/en/us/support/docs/lan-switching/spanning-tree-protocol/69980-errdisable-recovery.html)