Author Archives: marvelliu

Linux hook函数简单实践

#起因
最近对hook函数比较感兴趣,希望能捕获所有某种系统调用,看网上有一篇文章(http://opensourceforu.com/2011/08/lets-hook-a-library-function/) 介绍hook malloc函数,但可能内核实现不同,4.4.0上有问题。特写一篇文章进行更新。

原文中,hook函数中调用printf,其实printf、fprintf等函数都会再调用malloc函数,导致segment fault。正确的方式应该是用snprintf写入内存,然后调用write函数输出。同理,如果在hook open函数的时候,调用了open函数,也会造成segment fault的问题。

#示例
a.c:

#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>
int main(void)
{
int p;
printf("calling from main...\n");
p=(int *)malloc(10);
if(!p)
{
printf("Got allocation error...\n");
exit(1);
}
printf("returning to main...\n");
free(p); /
freeing memory from heap */
printf("freeing memory...\n");
return 0;
}

prog2.c

#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <stdarg.h>
#include <string.h>
#include <dlfcn.h> /* header required for dlsym() */
#include <unistd.h>

/* lcheck() is for memory leak check; its code is not shown
here /
void lcheck(void);
void print(const char
format, ...);

void* malloc(size_t size)
{
static void* (my_malloc)(size_t) = NULL;
print("inside shared object...\n");
if (!my_malloc)
my_malloc = dlsym(RTLD_NEXT, "malloc"); /
returns the object reference for malloc /
void *p = my_malloc(size); /
call malloc() using function pointer my_malloc /
print("malloc(%lu) = %p\n", size, p);
lcheck(); /
calling do_your_stuff function */
print("returning from shared object...\n");

return p;

}

void free(void* p){
static void (my_free)(void ) = NULL;
if (!my_free)
my_free = dlsym(RTLD_NEXT, "free"); /* returns the object reference for free /
my_free(p); /
call malloc() using function pointer my_free */
print("free %p\n", p);
return;

}

int open(const char pathname, int flags, mode_t mode){
static int (
my_open)(const char *, int , mode_t ) = NULL;
if (!my_open)
my_open = dlsym(RTLD_NEXT, "open"); /* returns the object reference for open /
int res = my_open(pathname, flags, mode); /
call malloc() using function pointer my_open */
print("open %s, flags: %d, mode: %u\n", pathname, flags, mode);
return res;
}

ssize_t read(int fd, void buf, size_t count){
static int (
my_read)(int ,void*, size_t) = NULL;
if (!my_read)
my_read = dlsym(RTLD_NEXT, "read"); /* returns the object reference for open /
ssize_t size= my_read(fd, buf, count); /
call malloc() using function pointer my_open */
print("read fd %d, buf: %s, count: %u, read size: %u\n", fd, buf, count, size);
return size;
}

ssize_t recv(int sockfd, void buf, size_t len, int flags){
static int (
my_recv)(int ,void*, size_t, int) = NULL;
if (!my_recv)
my_recv = dlsym(RTLD_NEXT, "recv"); /* returns the object reference for open /
ssize_t size= my_recv(sockfd, buf, len, flags); /
call malloc() using function pointer my_open */
print("recv fd: %d, buf: %s, len: %u, flags: %d, read size: %u\n", sockfd, buf, flags, size);
return size;
}

int accept(int sockfd, struct sockaddr addr, socklen_t *addrlen){
static int (
my_accept)(int ,struct sockaddr*, socklen_t) = NULL;
if (!my_accept)
my_accept = dlsym(RTLD_NEXT, "accept"); /
returns the object reference for open /
ssize_t size= my_accept(sockfd, addr, addrlen); /
call malloc() using function pointer my_open */
print("accept fd: %d, addr: %u, len: %u, fd: %u\n", sockfd, addr, addrlen, size);
return size;
}

ssize_t write(int fd, const void buf, size_t count){
static int (
my_write)(int ,const void*, size_t) = NULL;
if (!my_write)
my_write = dlsym(RTLD_NEXT, "write");
ssize_t size= my_write(fd, buf, count);
print("write %d, buf: %s, count: %u, write size: %u\n", fd, buf, count, size);
return size;
}

void lcheck(void)
{
print("displaying memory leaks...\n");
/* do required stuff here */
}

void print(const char* format, ...){
static char buf[1024*2];
char tmp[1024];

static int logfd = -1;
if (!my_open)
    my_open = dlsym(RTLD_NEXT, "open");

if (!my_write)
    my_write = dlsym(RTLD_NEXT, "write");

if (!my_close)
    my_close = dlsym(RTLD_NEXT, "close");

if(logfd <0 || lseek(logfd, 0, SEEK_CUR)<0){
    logfd = my_open(logfile, O_RDWR | O_CREAT| O_APPEND, 600);
}


va_list argptr;
va_start(argptr, format);
vsprintf(buf, format, argptr);
va_end(argptr);
//int res = my_write(1, buf, strlen(buf));
int res = my_write(logfd, buf, strlen(buf));
if(res <0){
    perror("write error");
}

}

然后编译,运行:


gcc prog2.c -o libprog2.so -ldl -fPIC -shared
LD_PRELOAD=/home/liuwenmao/hook/libprog2.so /usr/sbin/sshd -D -p 9999

很奇怪的是sshd会有问题,root下面运行客户端会有错误:

root@node10:~/hook# LD_PRELOAD=/home/liuwenmao/hook/libprog2.so /usr/sbin/sshd -D -p 9999

#------客户端----
liuwenmao@node10:~$ ssh localhost -p 9999 -l test
test@localhost's password:
Permission denied, please try again.
test@localhost's password:
close[31596] 4
Welcome to Ubuntu 16.04.3 LTS (GNU/Linux 4.4.0-78-generic x86_64)

  • Documentation: https://help.ubuntu.com
  • Management: https://landscape.canonical.com
  • Support: https://ubuntu.com/advantage

1 package can be updated.
0 updates are security updates.

*** System restart required ***
Last login: Fri Sep 29 15:51:02 2017 from ::1
write error: Bad file descriptor
write error: Bad file descriptor
write error: Bad file descriptor
write error: Bad file descriptor
test@node10:~$

但是在普通用户下sudo是ok的:

liuwenmao@node10:~/hook$ LD_PRELOAD=/home/liuwenmao/hook/libprog2.so sudo /usr/sbin/sshd -D -p 9999

#------客户端----
liuwenmao@node10:~$ ssh localhost -p 9999 -l test
test@localhost's password:
Welcome to Ubuntu 16.04.3 LTS (GNU/Linux 4.4.0-78-generic x86_64)

  • Documentation: https://help.ubuntu.com
  • Management: https://landscape.canonical.com
  • Support: https://ubuntu.com/advantage

1 package can be updated.
0 updates are security updates.

*** System restart required ***
Last login: Fri Sep 29 15:51:20 2017 from ::1
test@node10:~$

猜测可能是ssh有一些suid等操作,对LD_PRELOAD有一些影响。

探讨

作为检测方来说,其实用户态函数hook并不是一种要的选择,因为
1. 攻击者可以检测环境变量,如果存在LD_PRELOAD,就可以认定有监测程序,作为处置方式也很简单,要不unset掉这个环境变量,这样检测方就无法hook了;要不直接放弃这台主机,避免被捕获下一步的攻击样本。
2. 检测方法并不可扩展,一方面,需要hook很多很多很多函数,例如新建文件描述符就涉及到open、accept、listen等函数,如果漏掉一个,就可能监控不全;另一方面,很多程序是非预期的,例如docker新启动一个容器,就可能会将所有的环境变量清空,这样默认场景下是无法监控到容器内部的操作的(虽然容器里的进程是docker run的子进程)
3. hook函数编写较为复杂,至少它调用的方法不能是hook函数之一。

软件定义安全一书勘误

写在篇首
《软件定义安全-SDN/NFV新型网络的安全揭秘》一书自推出后受到读者欢迎,很快就要重印,借此机会会修正文中一些错误。本文将持续更新,避免误导读者。

第一次印刷:

P44:本节阐述了OpenFlow等南北向协议中的安全问题
勘误:本节只讨论了南向协议。应为:“本节介绍了OpenFlow等SDN南向协议及其存在的安全问题,SDN北向协议目前还没有统一的标准,一般北向协议语义上采用如RESTful等HTTP协议,感兴趣读者可自行了解,其安全问题可参考2.1.3小节,本节不做详细讨论。”

P153: 该方案就是基于Openstack平台的,通过 Openstack 平台的 FWaaS 服务接口, 为云数据中心中的每个租户提供独立的虚拟防火墙,实现了租户间的安全隔离和保护,虚拟机间的东西流量也得到了有效地管控
勘误: FWaaS是实现南北向的访问控制,而非东西向,此处有误,应为“虚拟网络的南北向流量也得到有效管控”。具体如何实现南北向流量管控,可参见P67的“2.三层防火墙”小节,里面有介绍。

第一到第三重印

P127:5.3.2中第二段最后一句,应为:Web防护的策略为{match=”->S”, action=”redirect to WAF“, priority=5}
原文为ADS,此处应为WAF

Kubernetes对外服务

Kuberntets介绍

Kubernets是一个Google主导的机群管理系统,目前底层可以使用Docker,实现Docker实例的应用编排。Kubernets的介绍很多,本文简单介绍安装和使用的过程。更多资料可参考Kerbernets官网

Kuberntets安装

Kubernets可以在虚拟机VM或安装Linux的服务器上安装,本文以Ubuntu Server服务器为例,详细可参见官网的Ubuntu安装指南

先下载Kubernets源码,目前最新版为1.4.1

root@node3:/usr/src# git clone https://github.com/kubernetes/kubernetes.git
root@node3:/usr/src# cd /usr/src/kubernetes
root@node3:/usr/src# git checkout v1.4.1

本文中存在两个节点,node3(192.168.200.13)和node4(192.168.200.14),node3作为控制节点和计算节点,node4作为计算节点。于是修改kubernetes/cluster/ubuntu/config-default.sh

export nodes=${nodes:-"root@192.168.200.13 root@192.168.200.14"}
roles=${roles:-"ai i"}
export NUM_NODES=${NUM_NODES:-2}
export SERVICE_CLUSTER_IP_RANGE=${SERVICE_CLUSTER_IP_RANGE:-100.0.0.0/16}  # formerly PORTAL_NET
export FLANNEL_NET=${FLANNEL_NET:-172.16.0.0/16}
DNS_SERVER_IP=${DNS_SERVER_IP:-"100.0.0.2"}

以上就是对配置文件的全部改动,请放置在相应位置。然后进行安装:

$ cd kubernetes/cluster
$ KUBERNETES_PROVIDER=ubuntu ./kube-up.sh
...........
Validate output:
NAME                 STATUS    MESSAGE              ERROR
controller-manager   Healthy   ok
scheduler            Healthy   ok
etcd-0               Healthy   {"health": "true"}
Cluster validation succeeded
Done, listing cluster services:

Kubernetes master is running at http://192.168.200.13:8080

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

如果不出错则会提示安装完毕。此时将Kubernets的命令放于PATH中。

root@node3:/usr/src# export PATH=/usr/src/kubernetes/cluster/ubuntu/binaries/:$PATH
root@node3:/usr/src# which kubectl
/usr/src/kubernetes/cluster/ubuntu/binaries//kubectl

然后安装dashboard和dns组件:

$ cd cluster/ubuntu
$ KUBERNETES_PROVIDER=ubuntu ./deployAddons.sh

可能存在的问题:

  • 如果需要重装,请运行KUBERNETES_PROVIDER=ubuntu ./kube-down.sh,停掉相关服务,然后要还原/etc/default/docker配置文件。
  • Kubernets会从Google的镜像仓库(gcr.io)获取某些镜像,但国内被墙了,所以可以选择一个http代理服务器,并在需要启动这些镜像的主机上为docker添加代理,方法是在/etc/default/docker中的开头添加:
export HTTP_PROXY=...

然后重启docker:

service docker restart

完毕需要将代理去掉再重启docker,具体可参考这篇文章

确保所有需运行这些镜像的节点本地都要有这些镜像!!,可以先在一个节点上用代理下载所有镜像,然后上传到私有仓库,再在其他节点上下载这些镜像即可。

  • 在运行带有运行 google镜像时,如果本地已经有该镜像的时候,但配置文件中带有
    imagePullPolicy: Always
    时,则仍会从Google仓库去获取,一种方法是将其变为:
imagePullPolicy: IfNotPresent

另一种方法是放到私有仓库中。
* 如果配置文件中没有指定imagePullPolicy,老版本会优先从本地找该版本的镜像,如有则直接启动;但发现1.4.1版本会优先pull。测试需要在配置文件中加入:

然后再运行安装或其他的命令。

  • Kubernetes会修改/etc/default/docker,请注意不要被覆盖原来的一些配置,否则docker pull私有仓库可能有问题,我的配置是:
DOCKER_OPTS=" --registry-mirror=http://2687282c.m.daocloud.io -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock --bip=172.16.69.1/24 --mtu=1450"

Kubernets的一些概念

Pod

每一个Pod都是运行在某个节点上的实例或实例集合。可以对应于docker的实例或实例集合。
通过下面的命令可以查看运行的实例:

root@node3:~# kubectl get pods
NAME                   READY     STATUS    RESTARTS   AGE
django-default-yxm7u   1/1       Running   0          15m
django-x-q9twt         1/1       Running   0          15m
django-y-wgy0c         1/1       Running   0          15m
nginx-ingress-e049x    1/1       Running   0          14m

还可以看一些系统的实例:

root@node3:/usr/sr# kubectl get pods --namespace=kube-system
NAME                                READY     STATUS    RESTARTS   AGE
kube-dns-v20-h35xt                  3/3       Running   3          1h
kubernetes-dashboard-v1.4.0-5g12f   1/1       Running   6          13h

还可以看这些pod运行在什么节点,这对排查问题比较有用。

root@node3:/usr/src# kubectl get pods -o wide
NAME                   READY     STATUS    RESTARTS   AGE       IP            NODE
django-default-yxm7u   1/1       Running   0          20m       172.16.77.4   192.168.200.13
django-x-q9twt         1/1       Running   0          20m       172.16.9.2    192.168.200.14
django-y-wgy0c         1/1       Running   0          20m       172.16.9.3    192.168.200.14
nginx-ingress-e049x    1/1       Running   0          19m       172.16.77.5   192.168.200.13

Service

Service是提供对外可见的服务,可以使用下面的配置文件service.yaml新建服务。

# 3 Services for the 3 endpoints of the Ingress
apiVersion: v1
kind: Service
metadata:
  name: django-x
  labels:
    app: django-x
spec:
  type: NodePort
  ports:
  - port: 18111
    #nodePort: 30301
    targetPort: 8111
    protocol: TCP
    name: http
  selector:
    app: django-x

---
apiVersion: v1
kind: Service
metadata:
  name: django-default
  labels:
    app: django-default
spec:
  type: NodePort
  ports:
  - port: 18111
    #nodePort: 30302
    targetPort: 8111
    protocol: TCP
    name: http
  selector:
    app: django-default
---
apiVersion: v1
kind: Service
metadata:
  name: django-y
  labels:
    app: django-y
spec:
  type: NodePort
  ports:
  - port: 18111
    #nodePort: 30284
    targetPort: 8111
    protocol: TCP
    name: http
  selector:
    app: django-y

可以用kubectl查看服务(svc是services的缩写):

root@node3:/usr/src# kubectl create -f service.yaml
root@node3:/usr/src# kubectl get svc
NAME             CLUSTER-IP     EXTERNAL-IP   PORT(S)     AGE
basic            100.0.53.240    <nodes>       18112/TCP   8s
django-default   100.0.53.222   <nodes>       18111/TCP   21m
django-x         100.0.34.47    <nodes>       18111/TCP   21m
django-y         100.0.95.86    <nodes>       18111/TCP   21m
kubernetes       100.0.0.1      <none>        443/TCP     13h

正常情况下,在集群内部,是可以通过CLUSTER_IP : PORT来访问服务的,如在node3或node4上运行curl http://100.0.34.47:18111来访问django-x的服务的。

CLUSTER是Kubernets内部维护的机群,所以CLUSTER-IP和PORT是服务面向集群提供的内部ip和端口,互联网是访问不到的,例如上面的kubernets和basic服务就是这种情况。如果要从互联网访问这些服务,我们会在下面讲到,可用ingress的方法,使用一个代理将请求转到CLUSTER-IP : PORT。

如果每个物理节点是互联网可以直接访问到的话,那么也可以使用NodePort的类型,如上面的三个django都是这类服务,所以其外部IP是nodes。这样每个节点上的kube-proxy服务都会开一个端口P,外部网络可以通过访问任意一个节点的端口P进行访问。
那么P如何获得呢?可以通过查询服务获知:

root@node3:~/k8s-test# kubectl describe svc django-x
Name:           django-x
Namespace:      default
Labels:         app=django-x
Selector:       app=django-x
IP:         100.0.34.47
Port:           http    18111/TCP
NodePort:       http    32400/TCP
Endpoints:      172.16.76.3:8111
Session Affinity:   None
No events.

那么就可以通过curl http://192.168.200.13:32400来访问该服务了。

Replication Controller

Replication Controller(RC)是控制Pod数量和部署的控制器,Kubernets区别原生Docker很重要的一点是它实现了对资源的监控、弹性部署。假设一个pod挂了,rc可以再启动一个;或者机群要扩容,rc也可以很快增加pod实现。

以下配置文件rc.yaml可以新建三个RC:


# A single RC matching all Services apiVersion: v1 kind: ReplicationController metadata: name: django-x spec: replicas: 1 template: metadata: labels: app: django-x spec: containers: - name: django-x image: appstore:5000/liuwenmao/django-hello ports: - containerPort: 8111 --- apiVersion: v1 kind: ReplicationController metadata: name: django-default spec: replicas: 1 template: metadata: labels: app: django-default spec: containers: - name: django-default image: appstore:5000/liuwenmao/django-hello ports: - containerPort: 8111 --- apiVersion: v1 kind: ReplicationController metadata: name: django-y spec: replicas: 1 template: metadata: labels: app: django-y spec: containers: - name: django-y image: appstore:5000/liuwenmao/django-hello ports: - containerPort: 8111

如果要获取rc,可运行:

root@node3:/usr/src# kubectl create -f rc.yaml
root@node3:/usr/src# kubectl get rc
NAME             DESIRED   CURRENT   AGE
django-default   1         1         2h
django-x         1         1         2h
django-y         1         1         2h
nginx-ingress    1         1         2h

可能存在的问题
* 当要查看某个pod、service或rc的详细信息,可以用describe。如某个pod挂了,可以详细查看具体日志:

root@node3:/usr/src/kubernetes/cluster/ubuntu# kubectl get pods --namespace=kube-system
NAME                                READY     STATUS         RESTARTS   AGE
kube-dns-v20-0gnu3                  0/3       ErrImagePull   0          9m
kubernetes-dashboard-v1.4.0-5g12f   1/1       Running        4          11h

其中kube-dns看似有问题,继续检查:

root@node3:/usr/src/kubernetes/cluster/ubuntu# kubectl describe pods kube-dns-v20-0gnu3 --namespace=kube-system
Name:           kube-dns-v20-0gnu3
Namespace:      kube-system
Node:           192.168.200.14/192.168.200.14
Start Time:     Thu, 13 Oct 2016 09:56:24 +0800
Labels:         k8s-app=kube-dns
                version=v20
Status:         Pending
IP:             172.16.9.2
Controllers:    ReplicationController/kube-dns-v20
Containers:
  kubedns:
    Container ID:
    Image:              gcr.io/google_containers/kubedns-amd64:1.8
    Image ID:
    Ports:              10053/UDP, 10053/TCP
    Args:
      --domain=cluster.local.
      --dns-port=10053
    Limits:
      memory:   170Mi
    Requests:
      cpu:                      100m
      memory:                   70Mi
    State:                      Waiting
      Reason:                   ErrImagePull
    Ready:                      False
    Restart Count:              0
    Liveness:                   http-get http://:8080/healthz-kubedns delay=60s timeout=5s period=10s #success=1 #failure=5
    Readiness:                  http-get http://:8081/readiness delay=3s timeout=5s period=10s #success=1 #failure=3
    Environment Variables:      <none>
  dnsmasq:
    Container ID:
    Image:              gcr.io/google_containers/kube-dnsmasq-amd64:1.4
    Image ID:
    .......(略)
Events:
  FirstSeen     LastSeen        Count   From                    SubobjectPath                   Type            Reason          Message
  ---------     --------        -----   ----                    -------------                   --------        ------          -------
  6m            6m              1       {default-scheduler }                                    Normal          Scheduled       Successfully assigned kube-dns-v20-0gnu3 to 192.168.200.14
  5m            5m              1       {kubelet 192.168.200.14} spec.containers{kubedns}        Warning         Failed          Failed to pull image "gcr.io/google_containers/kubedns-amd64:1.8": image pull failed for gcr.io/google_containers/kubedns-amd64:1.8, this may be because there are no credentials on this request.  details: (Error response from daemon: {"message":"Get https://gcr.io/v1/_ping: dial tcp 64.233.188.82:443: i/o timeout"})
  4m            4m              1       {kubelet 192.168.200.14} spec.containers{dnsmasq}        Warning         Failed          Failed to pull image "gcr.io/google_containers/kube-dnsmasq-amd64:1.4": image pull failed for gcr.io/google_containers/kube-dnsmasq-amd64:1.4, this may be because there are no credentials on this request.  details: (Error response from daemon: {"message":"Get https://gcr.io/v1/_ping: dial tcp 64.233.188.82:443: i/o timeout"})
  3m            3m              1       {kubelet 192.168.200.14}                                 Warning         FailedSync      Error syncing pod, skipping: [failed to "StartContainer" for "kubedns" with ErrImagePull: "image pull failed for gcr.io/google_containers/kubedns-amd64:1.8, this may be because there are no credentials on this request.  details: (Error response from daemon: {\"message\":\"Get https://gcr.io/v1/_ping: dial tcp 64.233.188.82:443: i/o timeout\"})"
, failed to "StartContainer" for "dnsmasq" with ErrImagePull: "image pull failed for gcr.io/google_containers/kube-dnsmasq-amd64:1.4, this may be because there are no credentials on this request.  details: (Error response from daemon: {\"message\":\"Get https://gcr.io/v1/_ping: dial tcp 64.233.188.82:443: i/o timeout\"})"
, failed to "StartContainer" for "healthz" with ErrImagePull: "image pull failed for gcr.io/google_containers/exechealthz-amd64:1.2, this may be because there are no credentials on this request.  details: (Error response from daemon: {\"message\":\"Get https://gcr.io/v1/_ping: dial tcp 64.233.188.82:443: i/o timeout\"})"
]
  3m    3m      1       {kubelet 192.168.200.14} spec.containers{healthz}        Warning Failed          Failed to pull image "gcr.io/google_containers/exechealthz-amd64:1.2": image pull failed for gcr.io/google_containers/exechealthz-amd64:1.2, this may be because there are no credentials on this request.  details: (Error response from daemon: {"message":"Get https://gcr.io/v1/_ping: dial tcp 64.233.188.82:443: i/o timeout"})
  1m    1m      1       {kubelet 192.168.200.14} spec.containers{dnsmasq}        Warning Failed          Failed to pull image "gcr.io/google_containers/kube-dnsmasq-amd64:1.4": image pull failed for gcr.io/google_containers/kube-dnsmasq-amd64:1.4, this may be because there are no credentials on this request.  details: (Error response from daemon: {"message":"Get https://gcr.io/v1/_ping: dial tcp 64.233.189.82:443: i/o timeout"})
  4m    1m      2       {kubelet 192.168.200.14} spec.containers{healthz}        Normal  Pulling         pulling image "gcr.io/google_containers/exechealthz-amd64:1.2"
  59s   59s     1       {kubelet 192.168.19.14} spec.containers{healthz}        Warning Failed          Failed to pull image "gcr.io/google_containers/exechealthz-amd64:1.2": image pull failed for gcr.io/google_containers/exechealthz-amd64:1.2, this may be because there are no credentials on this request.  details: (Error response from daemon: {"message":"Get https://gcr.io/v1/_ping: dial tcp 64.233.189.82:443: i/o timeout"})
  59s   59s     1       {kubelet 192.168.200.14}                                 Warning FailedSync      Error syncing pod, skipping: [failed to "StartContainer" for "kubedns" with ErrImagePull: "image pull failed for gcr.io/google_containers/kubedns-amd64:1.8, this may be because there are no credentials on this request.  details: (Error response from daemon: {\"message\":\"Get https://gcr.io/v1/_ping: dial tcp 64.233.189.82:443: i/o timeout\"})"
, failed to "StartContainer" for "dnsmasq" with ErrImagePull: "image pull failed for gcr.io/google_containers/kube-dnsmasq-amd64:1.4, this may be because there are no credentials on this request.  details: (Error response from daemon: {\"message\":\"Get https://gcr.io/v1/_ping: dial tcp 64.233.189.82:443: i/o timeout\"})"
, failed to "StartContainer" for "healthz" with ErrImagePull: "image pull failed for gcr.io/google_containers/exechealthz-amd64:1.2, this may be because there are no credentials on this request.  details: (Error response from daemon: {\"message\":\"Get https://gcr.io/v1/_ping: dial tcp 64.233.189.82:443: i/o timeout\"})"

能看到是因为向google的仓库下载超时导致的,可参考上面的代理方法解决。

比如现在我需要运行一个django的web服务,那么可以通过运行下面的配置文件即可实现。

为Service添加互联网入口

Load balance

可以expose deployment(带–type=”LoadBalance”)的方式将服务暴露出去,但是目前这种方式支持公有云,如Google Container Engine等,貌似不能应用于私有的数据中心。具体可以参考官网Hello World的Allow external traffic一节

Ingress

在内网部署服务,希望对外暴露,可以使用Ingress的方式,以下配置文件为将上述服务在80端口上做映射,实现虚拟主机的功能。

# An Ingress with 2 hosts and 3 endpoints
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: echomap
spec:
  rules:
  - host: foo.bar.com
    http:
      paths:
      - path: /foo
        backend:
          serviceName: django-x
          servicePort: 18111
  - host: bar.baz.com
    http:
      paths:
      - path: /bar
        backend:
          serviceName: django-y
          servicePort: 18111
      - path: /foo
        backend:
          serviceName: django-x
          servicePort: 18111

然后运行并查看Ingress

root@node3:/usr/src/gohome/src/k8s.io/contrib/ingress/controllers/nginx-alpha# kubectl create -f rc.yaml
replicationcontroller "nginx-ingress" created
root@node3:~$ kubectl get ing
NAME      HOSTS                     ADDRESS   PORTS     AGE
echomap   foo.bar.com,bar.baz.com             80        3d
root@node3:~$ kubectl describe ing echomap
Name:           echomap
Namespace:      default
Address:
Default backend:    default-http-backend:80 (<none>)
Rules:
  Host      Path    Backends
  ----      ----    --------
  foo.bar.com
            /foo    django-x:18111 (<none>)
  bar.baz.com
            /bar    django-y:18111 (<none>)
            /foo    django-x:18111 (<none>)
Annotations:
No events.

Ingress是Kubernets的一种用于访问服务的机制,可以通过api获得这些映射关系,不过如何实现具体的功能,例如上例中的虚拟主机功能,可以使用Kubernets的contrib中的ingress-controller实现,本文给出的是使用nginx实现。

contrib是一大堆未进入Kubernets核心的代码集合,代码在(https://github.com/kubernetes/contrib),安装请按照项目的README进行(需要在$GOPATH里面,不是随便安装即可的),假设$GOPATH=/usr/src/gohome,那么Kubernets Contrib在/usr/src/gohome/src/k8s.io/contrib/,而我们说的nginx Ingress控制器则在/usr/src/gohome/src/k8s.io/contrib/ingress/controllers/nginx-alpha。

查看一下nginx-alpha目录下面的rc.yaml可知,Ingress Controller后台使用了gcr.io/google_containers/nginx-ingress镜像,不过这个镜像在笔者测试时有问题,所以实验中还是根据同目录下的Dockerfile重新生成的。查看Dockerfile可知,这个镜像是基于nginx的,并通过运行controller程序将Kubernets API获得的Ingress映射翻译成nginx的配置文件,从而实现了反向代理到运行不同网站的Service的功能。

当运行完创建rc.yaml后,我们可以找到相应的docker容器,观察其中的映射规则:

root@node3:/usr/src# kubectl get pods -o wide|grep nginx
nginx-ingress-g518r    1/1       Running   2          3d        172.16.66.3   192.168.200.13
root@node3:/usr/src# docker ps |grep nginx
4374a4965333        gcr.io/google_containers/nginx-ingress:0.1                                    "/controller"            46 hours ago        Up 46 hours                                                                                                                                                            k8s_nginx.a9cb3eb9_nginx-ingress-g518r_default_71b457b9-914e-11e6-821d-c81f66f3c543_f9c7501f
0051bb8806d1        gcr.io/google_containers/pause-amd64:3.0                                      "/pause"                 46 hours ago        Up 46 hours         0.0.0.0:80->80/tcp                                                                                                                                 k8s_POD.6cfd0339_nginx-ingress-g518r_default_71b457b9-914e-11e6-821d-c81f66f3c543_e01f441e
root@node3:/usr/src# docker exec -it 4374a4965333 /bin/bash
[ root@nginx-ingress-g518r:/etc/nginx ]$ ls
certs/   fastcgi.conf    koi-utf  mime.types  proxy_params  sites-available/  snippets/     win-utf
conf.d/  fastcgi_params  koi-win  nginx.conf  scgi_params   sites-enabled/    uwsgi_params
[ root@nginx-ingress-g518r:/etc/nginx ]$ cat nginx.conf

events {
  worker_connections 1024;
}
http {
  # http://nginx.org/en/docs/http/ngx_http_core_module.html
  types_hash_max_size 2048;
  server_names_hash_max_size 512;
  server_names_hash_bucket_size 64;



  server {
    listen 80;
    server_name foo.bar.com;

    location /foo {
      proxy_set_header Host $host;
      proxy_pass http://django-x.default.svc.cluster.local:18111;
    }
  }
  server {
    listen 80;
    server_name bar.baz.com;

    location /bar {
      proxy_set_header Host $host;
      proxy_pass http://django-y.default.svc.cluster.local:18111;
    }
    location /foo {
      proxy_set_header Host $host;
      proxy_pass http://django-x.default.svc.cluster.local:18111;
    }
  }
[ root@nginx-ingress-g518r:/etc/nginx ]$ ping django-x.default.svc.cluster.local
PING django-x.default.svc.cluster.local (100.0.87.12) 56(84) bytes of data.
64 bytes from django-x.default.svc.cluster.local (100.0.87.12): icmp_seq=1 ttl=47 time=265 ms
64 bytes from django-x.default.svc.cluster.local (100.0.87.12): icmp_seq=2 ttl=47 time=253 ms

到此为止,背后的原理已经清楚了。我们可以看一下运行效果。因为三个服务运行的都是django,所以可以查看django的输出日志(如果用supervisor运行的,可以进容器查看/var/log/supervisor里面相关的log文件)查看真实的Web访问情况。

此时,如果访问http://bar.baz.com/bar,就会转到django-y的服务(http://django-y.default.svc.cluster.local:18111),如果访问http://bar.baz.com/foo或http://foo.bar.com/foo,就转到django-x,其余的则转到ngix的404页面。

上述思路不仅可以使用在web服务,还可以用于如外部接入ssh等。

Django国际化

本文在django 1.7验证过

1 在项目settings.py中添加两行(+表示添加行):


+ LOCALE_PATHS = (BASE_DIR+ "/locale",)

MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.locale.LocaleMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',

2 在views.py中标记要翻译的字符串,格式为{% trans ‘字符串’%} 如

{% load i18n %}


  • {% trans 'User' %}
  • 3 建立locale目录结构

    $ mkdir locale/en -p
    $ mkdir locale/zh_CN -p
    $ python manage.py makemessages -a
    processing locale zh_CN
    processing locale en

    此时会新建./locale/{lang}/LC_MESSAGES/django.po文件,lang表示语言,中文为zh_CN。
    其中django.po文件包含了django目录中所有需要翻译的字符串。如

    #, fuzzy
    msgid ""
    msgstr ""
    "Project-Id-Version: PACKAGE VERSION\n"
    "Report-Msgid-Bugs-To: \n"
    "POT-Creation-Date: 2016-04-07 10:33+0800\n"
    "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    "Last-Translator: FULL NAME \n"
    "Language-Team: LANGUAGE \n"
    "Language: \n"
    "MIME-Version: 1.0\n"
    "Content-Type: text/plain; charset=UTF-8\n"
    "Content-Transfer-Encoding: 8bit\n"

    #: templates/base.html:138
    msgid "User"
    msgstr ""

    4 翻译相关语言下的django.po。对于每个msgid的字符串,修改对应的翻译语句,如上例为

    #: templates/base.html:138
    msgid "User"
    msgstr ""

    5 编译:
    $ python manage.py compilemessages
    processing file django.po in /usr/src/appstoreserver/appstore/locale/zh_CN/LC_MESSAGES
    processing file django.po in /usr/src/appstoreserver/appstore/locale/en/LC_MESSAGES

    6 done

    创新沙盒:“软件定义安全”不再是实验室产物

    自从著名咨询机构Gartner在《The Impact of Software-Defined Data Centers on Information Security》 一文中提出软件定义安全(Software Defined Security,SDS)的概念后,软件定义与安全的结合已成为业界的前沿发展热点。背后的原因很直观:软件定义安全强调了安全控制平面与数据平面分离,从而在控制平面上可灵活调整应用策略,快速变更安全业务。

    在安全可被软件定义后,新的安全业务可在企业网络中可新上线,特别是在数据中心中,可实现计算、存储、网络和安全的弹性控制,实现软件定义的数据中心SDDC。正是因为这些优秀的特性,解决了企业客户长期面临的安全管理和运营“痛点”,软件定义安全自从开始就引起了学术界和工业界极大的关注。

    各大厂商都开始做相关的研究和研发工作,RSA大会一直是厂商们展现自己最新工作的舞台。如Check Point在RSA 2014大会上宣布推出软件定义防护(Software Defined Protection,SDP)革新性安全架构,可在当今日新月异的IT和威胁环境中为企业提供虚拟化的边界防护。赛门铁克也在RSA 2015提出使用软件定义网络技术对APT攻击进行取证的话题也提供了一种安全事件事后快速分析的新思路 。

    而RSA大会开到了第25个年头时,我们惊喜地发现更多的公司在展示在软件定义安全的领域的工作,特别是在体现创新的Innovation Sandbox(创新沙盒)竞赛中,10家经过专业评审的公司,居然有3家与这个话题有关,分别在不同的方面做出了开创性的工作。
    如Versa Networks公司,强调在软件定义广域网(SD-WAN)和分支(Branch)网络的环境中,通过虚拟化网络功能(VNF)技术,将各种各样异构的网络功能编程通用的组件,可快速在相应的网络中部署,大大减少了企业部署相应业务的开销,提高了整个过程的敏捷程度。

    Skyport Systems公司同样也是为企业提供高效的安全计算基础设施,但按照传统建立边界思维,攻击者在进入系统内部后就容易进一步攻击内部其他重要资源。该公司的逻辑是,所有的资源都是零信任,这样即便内部某资源被攻破,那么从该点作为跳板进一步攻击也是困难的。那么这里就涉及到软件定义的访问控制,例如如何做到“零信任”条件下各处的访问控制策略快速调整。该公司在B轮融资中获得3000万美元。

    再如Phantom Cyber公司认为在大量出现攻击的场景下,花费大量的人力去发现解决问题已不太现实。与前两个公司不同,Phantom Cyber从应用层入手,构建自动化、可编排的安全应用体系。它支持多种主流的数据分析平台,可利用较为高层的脚本实现安全运维自动化。

    当然除了这些初创公司,还有很多公司也在基于自身产品做相关的工作。如在29日的Session环节,VMWare的安全产品部门SVP Tom Corn就演示了在NSX的环境中,如何可按需定义微分段(MicroSegmentation),并对任意APP间快速添加加密处理。厂商展示区域,Catbird公司的软件定义安全架构 通过微分区(Micro-Segmentation)在虚拟环境中划分不同的区域,并通过编排将安全策略下发给多种类型的安全设备,并作用在区域级别或虚拟机级别。这些工作都体现了各家在成熟产品线通过软件定义做了很多延展性的工作。

    绿盟科技自2013年开始研究SDN和软件定义安全,研发了包括软件定义的抗DDoS、流量异常检测和Web安全等原型系统,并在2015年发布了软件定义安全的白皮书,探讨在该领域的进展。

    创新沙盒中10个产品中出现了三个能体现SDS的产品,笔者认为其背后的原因有几个:其一,作为软件定义安全的支撑技术,如VNF/NFV、SDN方案,在国外已经有一些成熟的应用,如NSX已经代替Vsphere成为VMWare成长最快的产品,Cisco的ACI方案也与很多安全厂商有合作;其二,企业的高效安全运营需求,直接催生了安全编排这些应用层面的创新;其三,也是最重要的,出于企业对降低成本的天然需求,软件定义的理念转换为实际产品的动力十足。

    RSA大会的创新沙盒一直是硅谷安全行业的风向标,今年的沙盒竞赛体现了软件定义安全确实不只是一些实验室的原型系统,一些初创企业已经开始将其作为重点,根据企业在安全运营方面出现的存在各种问题,有针对性的提出了自己的解决方案。我们有理由做出判断,软件定义安全恐怕离真正的产品化和商用已经不远了。

    UPDATE:创新沙盒的获胜者已公布,即Phantom。除去其他领域的竞争者,Phantom能在上述其他两家公司脱颖而出,笔者认为主要是因为它从客户的业务作为切入点,强调了业务层面的编排,且一开始便是开放架构设计,可支持第三方的VNF组成服务链。给客户安全运维提供高效手段,降低运维成本的同时,打消了客户被厂商锁定(Vendor Lock-in)的顾虑,从而拥有更好的商业模式(事实上,该公司在种子轮融资中获得了270万美元)。

    ssh翻墙简单说明

    1 购买vps。设置用户名user和密码pass
    2 新建ssh profile。以xshell为例,新建一个profile,输入vps的ip、port、user和pass
    3 建立隧道。在profile属性->连接->SSH->隧道->添加,类型为Dynamic,侦听端口1080。同时启用转发x11(不知是否需要),保存profile。

    vps-tunnel

    4 连接vps,默认情况下连接成功。存在两 会等时候,可能被封掉…
    5 安装浏览器扩展,如firefox的foxyproxy
    6 以foxyproxy为例,新建一个proxy,URL匹配模式为要翻墙的url,如*google*,代理服务器细节中选择手动配置代理服务器,主机为127.0.0.1,端口1080,socks代理。确认
    foxyproxy

    7 启用该proxy,即可正常翻墙

    Ubuntu下openvswitch添加tcp监听

    本来这不应该是什么值得写的内容,不过有些需要hack一下,所以还是记一下为好。

    Ubuntu下面的openvswitch默认是开启punix(被动监听unix)进行管理ovsdb的,那么就不能查看和控制远程主机的ovsdb,openvswitch其实还可以开启ptcp或pssl模式,就可以打开远程的访问(当然存在风险,需假设控制网络是可信的)。

    不过配置文件(/etc/default/openvswitch-switch,/etc/init.d/openvswitch-switch)都没有该配置项,后来找了找,相关脚本在/usr/share/openvswitch/。(dpkg -L openvswitch-switch)

    其中启动ovsdb-server的脚本在/usr/share/openvswitch/scripts/ovs-ctl里面。

    187         set "$@" -vconsole:emer -vsyslog:err -vfile:info
    188         set "$@" --remote=punix:"$DB_SOCK" --remote=ptcp:6640
    

    这里面添加一个tcp端口即可。

    p.s. 如果去掉punix应该不行,因为ovs-vsctl默认使用了punix的方式,所以这里使用了同时启用punix和ptcp模式。

    另,这个py-ovsdb-client项目还不错,封装了多种ovsdb的api,直接在python中调用即可。

    在宿主机中用ovs连接内网vm

    经常需要做实验验证VM的性能,那么实验环境为:H是一个物理主机(物理网卡网段为192.168.19.0/24),H中启动一台VM,VM的一个管理口eth-M配有私网ip(网段100.100.100.0/24),并连接到一个OVS桥br-test上。现在需要在H中直接连接VM。

    一个直观的想法是直接ping VM,但VM与宿主机的主IP不是一个网段,所以不能直接通信,需要一个网关GW。那就直接在br-test上新建一个类型为internal的接口做网关。

    ovs-vsctl add-port br-test gw -- set Interface gw type=internal
    ifconfig gw 100.100.100.1
    iptables -t nat -I POSTROUTING -j SNAT  -s 192.168.19.0/24 --to-source 100.100.100.1
    iptables -t nat -I POSTROUTING -j SNAT  -s 100.100.100.0/24 --to-source 192.168.19.13
    

    并使用iptables做NAT,但发现还是ping不通。我印象中iptables的访问规则与openvswitch不兼容,不知道nat是不是也受到影响。

    既然如此,gw的路由应该不能放在openvswitch中,只能在namespace或原linux系统中。换一个思路,不将网关直接放在br-test上,而是使用veth对的形式:

     ip link add gw type veth peer name gw-o
    ovs-vsctl add-port br-test gw
    ifconfig gw 100.100.100.1
    iptables -t nat -I POSTROUTING -j SNAT  -s 192.168.19.0/24 --to-source 100.100.100.100
    iptables -t nat -I POSTROUTING -j SNAT  -s 100.100.100.0/24 --to-source 192.168.19.13
    

    那么数据从gw传到了gw-o,此时可以正常路由。

    使用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/