转自:http://blog.marvelworld.tk/?p=575
使用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:
1
2
3
4
5
6
7
8
|
$
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接下来的处理模块对应上。
那么我们的数据访问地址是这样的:
1
2
3
4
5
6
7
8
9
10
|
--
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为例说明下面过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
$
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字典中。
1
2
3
4
5
6
7
8
|
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方法:
1
2
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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
1
2
3
4
|
$
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
1
2
3
4
5
6
7
8
9
10
11
12
|
$
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时,作如下处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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
)
|
虚拟机启动之后,服务器收到请求,返回下面的内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
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或启动脚本的方式启动。
参考文献
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/