- 积分
- 16840
在线时间 小时
最后登录1970-1-1
|

楼主 |
发表于 2021-8-2 10:51:52
|
显示全部楼层
1 Ansible管理docker
$ p+ s$ E6 K. G近年来Linux容器技术越来越受欢迎,通过容器技术,可以保持程序运行环境的一致性,快速启动并高效率运行,涉及到的开销也比较小,此外,在系统层次上完成容器级别的资源隔离非常快速。, d* l; d' r4 G8 y
Docker是管理Linux容器最流行的工具,它为管理Linux容器提供了许多方便的工具,比如创建、销毁Linux容器,还提供了一些除管理Linux容器之外的工具,比如管理镜像、编排。通过它的易用性,Docker已经成为管理容器的最流行的方法之一。- Z3 ~( k% E" O1 k. |5 o: e/ b4 n
题外话:关于容器和Docker
1 q+ V! @% Q. S: U5 n# aLinux容器是内核的几种功能组合在一起实现的。换句话说,Linux容器技术是内核层次的功能,Docker只是提供了一系列工具,包括从底层和内核交互到高层和用户交互的一条龙。除了Docker外,也还有其它操作Linux容器的工具,只是对大众来说,Docker是最流行的。
2 i& P$ { @/ w+ C7 Y9 mAnsible为Docker提供了一整套工具,包括相关模块、连接插件(ansible_connection: docker)和inventory脚本,因此Ansible可在许多方面与Docker进行交互。例如Ansible可构建Docker镜像、启动或停止容器、组合多个容器服务、连接到活动容器并与之交互,甚至可以从容器中获取inventory。
, F s+ i3 Y8 a- D如下是Ansible官方目前提供的和Docker相关的模块:. U( Z1 G7 V% G" x
Code1 l. g+ `' K$ Y7 p
" ]9 R8 t/ l: J: E) t
docker_compose – Manage multi-container Docker applications with Docker Compose5 Q4 U, v8 @7 g4 Y1 C
docker_config – Manage docker configs# \# b0 |' i$ ~7 ]) d4 U
docker_container – manage docker containers9 c" S9 {$ o5 l: { u7 {3 g
docker_container_info – Retrieves facts about docker container
8 t; I0 ^! d |7 v. v, a) o9 Edocker_host_info – Retrieves facts about docker host and lists of objects of the services
0 V; U' c; V/ r3 e. b+ P, Tdocker_image – Manage docker images
0 `; x. t) L& }docker_image_info – Inspect docker images; F8 @0 o" g1 e+ P; i
docker_login – Log into a Docker registry
- z6 l2 n! S, P1 ldocker_network – Manage Docker networks1 i$ M* ?( b w) A
docker_network_info – Retrieves facts about docker network
W# {1 z$ O5 ^docker_node – Manage Docker Swarm node
2 S% ]9 \" {, D( u0 Qdocker_node_info – Retrieves facts about docker swarm node from Swarm Manager
& K* c- i, J! M% {+ M R wdocker_prune – Allows to prune various docker objects
7 ^, M' A& ^( T8 M2 ?& zdocker_secret – Manage docker secrets# L6 a4 F2 M, b1 {/ C' d' f
docker_stack – docker stack module
6 k8 F) D" I. ]( i, fdocker_swarm – Manage Swarm cluster( E% h- m) \9 V
docker_swarm_info – Retrieves facts about Docker Swarm cluster9 K. |; O( I3 w
docker_swarm_service – docker swarm service4 x; Q% n8 q% v/ {9 w9 C5 U, ?
docker_swarm_service_info – Retrieves information about docker services from a Swarm Manager9 g: q$ H2 U2 ~' n2 B
docker_volume – Manage Docker volumes/ D0 z, E8 a6 G
docker_volume_info – Retrieve facts about Docker volumes( W% @( Q1 ^- {/ [. X. v, g5 M
要使用Ansible管理连接Docker,要求安装如下包(注意:Ansible端和docker端都安装,这一点和其它模块不一样,如报错,请自行在两端安装、卸载、升级调试):/ J* w- h! F4 G5 L3 ?) X3 \1 e
Shell, i! h t, }4 j! J( O
" ?! h3 H. M' O# X8 @( Z
# 两端都安装,如果已经安装了,则在报错的情况下按需更新. m, J5 D# i0 k' s4 }4 R
# 此外,根据Ansible使用的python解释器版本,按需决定使用pip还是pip3,
0 Y% I4 i. ]& R& k: ]! {& u# 如果需要的是pip,则yum install python-pip
1 I1 a) q; w/ k- G$ pip3 install docker requests9 G1 r$ \, ?7 p
如下是其中两次报错信息,注意其中的结尾:No module named ‘XXX’。% u' s$ m, o, K3 T2 L
Code7 Y; W, W8 Q# `
) Y& P1 c+ |6 ?" T: x3 |
fatal: [192.168.8.65]: FAILED! => {"changed": false, "msg": "Failed to import the required Python library (Docker SDK for Python: docker (Python >= 2.7) or docker-py (Python 2.75)) on controller's Python /usr/bin/python3. Please read module documentation and install in the appropriate location. If the required library is installed, but Ansible is using the wrong Python interpreter, please consult the documentation on ansible_python_interpreter, for example via `pip install docker` or `pip install docker-py` (Python 2.75). The error was: No module named 'requests'"}
4 Z: K2 p- d& D: f, o- mfatal: [192.168.8.65]: FAILED! => {"changed": false, "msg": "Failed to import the required Python library (Docker SDK for Python: docker (Python >= 2.7) or docker-py (Python 2.75)) on controller's Python /usr/bin/python3. Please read module documentation and install in the appropriate location. If the required library is installed, but Ansible is using the wrong Python interpreter, please consult the documentation on ansible_python_interpreter, for example via `pip install docker` or `pip install docker-py` (Python 2.75). The error was: No module named 'docker'"}* B" z7 z" N% ~: v
2 Ansible构建并运行Docker镜像
I4 Q: i$ r# h7 k7 m0 }通Ansible提供的docker_image模块可管理Docker镜像(比如构建、移除、pull镜像),使用docker_container模块可管理容器,比如将镜像运行起来成为容器。
p: w4 T2 h5 K) `对我们而言,一般都是在已有镜像的基础上通过Dockerfile来定义新的操作,然后构建出自己的Docker镜像。所以需要提供两个文件:一个基础镜像和一个Dockerfile文件(基础镜像不存在时会自动下载)。如果使用Ansible来构建镜像,那么这个Dockerfile文件需要能够被Ansible读取,比如可以放在Ansible playbook文件的同目录下。% M# \3 j7 [/ Z+ {5 a8 @6 s
为了演示以下Ansible构建Docker镜像,此处已经写好了一个非常简单的Dockerfile,该Docker镜像是在CentOS 7镜像的基础上添加nginx,然后让nginx运行起来并提供cowsay页面。, W/ s: G% ?; g4 E/ l: h8 w2 w
Dockerfile内容如下:
! {- }: R" t S# _7 z, f8 XDockerfile8 T/ e2 _& P# C7 O" J" R- ]
: S/ F/ D" h/ B9 T
FROM centos:centos7' h7 V1 }8 ]" z% v" i& f0 u3 E t
LABEL maintainer="test.com"
7 D) `* D4 }& A2 b; h' \RUN rm -rf /etc/yum.repos.d/*.repo && \1 z1 ?- _8 W, E( S: }& D
echo -e ' \( ^ |" _) r! k& u, c x+ B
[base] \n\7 e4 `. I& T- g; L3 }3 _
name=os \n\
) n* ~8 `7 W& Xbaseurl=https://mirrors.163.com/centos/$releasever/os/$basearch/ \n\; M$ H5 z0 M' C3 }& e! J
enable=1 \n\9 O( _/ ~9 r3 e; U5 u K6 j
gpgcheck=0 \n\+ C( @/ D' @& V
[epel] \n\7 v% D8 q- l, s. T1 A
name=epel \n\
2 p9 ~6 ?) N! Jbaseurl=https://mirrors.163.com/epel/7Server/$basearch/ \n\
0 R7 A/ X4 B, I" U4 J: z! denable=1 \n\
8 H8 g; N, Y! a, q1 l9 ygpgcheck=0 \n\
( g9 U( I6 z2 a/ {$ {' >/etc/yum.repos.d/base.repo && \# s9 L7 ?0 q6 F" C1 I5 P. u
yum -y install cowsay nginx && \
" i1 h; v' ]8 d) u rm -rf /usr/share/nginx/html/index.html && \
; d3 y4 u# Z1 T+ f" M2 `$ s cowsay test >/usr/share/nginx/html/index.html && \
9 w# z; K; C: z9 A+ c4 D9 ? echo 'daemon off;' >>/etc/nginx/nginx.conf && \
( C8 I* F7 @$ j yum clean all
% S. U' U b' J) p7 W
% l' A9 |* Q+ K B" p% WEXPOSE 80
3 z" j2 l/ S- q( A, }& _CMD /usr/sbin/nginx
" k8 F% s* g* k& ~% X6 C8 U然后写一个Ansible任务文件,假设名为build_and_run_image.yaml,内容如下:
4 P, e- D# J6 ~ ~Yaml6 o1 `$ l6 v, y# I3 K) @
) ?/ V% t5 G3 V" w+ ?
- hosts: docker
8 k- q! d9 z' i1 ? gather_facts: no
% V% u7 X" @& L4 h: B tasks: 3 k5 K, E" k& A; i3 D
- name: scp Dockerfile
0 `- R! K1 q& P, G- t* H copy: * P( E: f H& g- J2 S" H( A
src: Dockerfile1 r1 t% W8 k& D! K; q3 H
dest: /tmp/Dockerfile
% a7 `% P; h' o4 H& f7 G - name: build Docker image centos_nginx:v0.1.1$ n/ g1 i) F7 K/ Q, S7 `
docker_image: $ @* r/ v, c$ k, M$ m1 M" ?
name: centos_nginx
; v7 {+ J3 T2 F source: build7 K8 K; z" r+ U) M O5 y* r
tag: v0.1.1
1 z* ^. U' K |1 V* I% I$ y/ n build:
- D) T% q. t% o; s# X' s6 K path: /tmp' X' }% e9 R0 V2 `& L
pull: yes
4 D, h* ^ d: p - name: start centos_nginx, V( _, I F+ v) Q5 G
docker_container:
' G3 |9 R, Z8 H. \7 R) d9 x3 }; j+ J name: cng
5 x u, r3 }" u$ B. d7 } image: centos_nginx:v0.1.1
4 g# Z- L5 D' ^: A ports: 8080:80
7 ]9 x' G/ f7 t6 `! c# z& ~ state: started
5 U" N* x4 u- ^6 X. x IAnsible执行完成上述任务后,可直接在Ansible端使用curl来测试页面是否可获取:
0 r5 D$ S3 J6 a. _Shell( {1 A ?! c1 h
9 c; ]. T! H `3 G
$ curl 192.168.8.65:8080( r: O& L0 }! ?0 M: b# G
______________$ C: z" x" u2 ]% j% k, U3 e
< Junmajinlong >& N; M, l* e( `
--------------# Y2 w" w3 Y5 r }" F: l& @; f
\ ^__^- y# \: }% j& A v8 e5 U
\ (oo)\_______& E# v' a4 [9 o; n9 m
(__)\ )\/\4 X& q9 z! A+ x& |) S
||----w |1 n" c6 Y" t4 [ H
|| ||
+ @9 S9 d& c* F {' x0 M这里对上述两个模块稍作解释。- N% H2 H- G" Y- T7 i
对于docker_image任务来说,通过sourec: build指令来表示这是一次镜像构建操作,source指令可包含如下值:. O" O% Y/ i) z$ J6 [
build:根据build指令的path参数指定的Dockerfile构建镜像
# e, t: S: L; g- _$ ]3 Xload:从镜像tar文件中提取镜像$ z: K, B# N# @; ^/ `
pull: 从registry中拉取镜像
2 Q% m+ D" ?' n, _% K' a1 P1 h0 r& A Elocal:保证docker端已经具备指定的镜像0 ?& m7 }1 w: b% e
对于build指令来说:1 f7 j$ `6 @ ]; k! c
path:指定构建时的上下文目录,该目录要求包含Dockerfile文件
; B) Z; z M8 _9 o& Qdockerfile:明确指定使用哪个Dockerfile文件,而不是默认的上下文目录中的Dockerfile文件
: b( c8 s X! w) n9 opull:构建时是否从registry中拉取基础和中间镜像镜像1 z/ D' l, U9 d+ k" ~
cache_from:构建时使用缓存的中间镜像
8 X C& M6 c: M5 C: t. Fargs:可按照key:value方式指定镜像的参数,对应于Dockerfile中的ARG指令,例如listen_port: 8080! G6 s. S6 N' s' [& G4 d: ^; \
其它指令应该都通俗易懂。
4 ?; I, D" \/ t' v( \构建镜像完成后,可使用docker_container模块启动该镜像。该模块指令非常非常多,几乎包含了docker container命令的所有选项功能,但是熟悉docker命令,这些指令的用法也通俗易懂。比如,指定卷和环境变量:4 i( C: h. {5 s. `0 d9 Z9 E6 }3 ^
Yaml2 x' b* o3 r+ c
# \" Q: @" I" q; u9 w# z+ a
% k1 ?/ k! V. l+ E' [" ^1 A' J- name: Create a data container
7 _$ J% o9 ~, _# Z2 Q' l docker_container:
( P9 @ K- E' l7 I/ J# ^ name: mydata
8 n- K% b8 J! }7 ^1 m image: busybox
8 ]- `/ `8 K7 ]3 W& ~! f9 N volumes:; N! A+ }( E' j0 M. r* D
- /data9 M( c5 Q9 ]: v6 P( ^
- name: Restart a container
' C' f" Q& K/ F- F/ z$ F( y5 m docker_container:) C! x) N/ u' d
name: myapplication4 E! t; [) c) j# e/ R2 o
image: someuser/appimage
! ~: j. s) z1 ?6 l3 p& p7 i) u state: started
. S2 {7 u' e7 F5 x g U restart: yes4 w D( ?( U i$ Q
devices:4 j p G2 _' U9 W* [4 i
- "/dev/sda:/dev/xvda:rwm"
4 w' W% ^ o0 P) Z ports:5 V7 N, i0 z1 a; `1 [
- "8080:9000"
9 w7 i4 f9 a; c- [5 j$ Z$ R. N9 S - "127.0.0.1:8081:9001/udp") I1 J' K4 `6 s1 @* [2 j
env:
2 ~2 R2 A" F4 ^9 x7 } SECRET_KEY: "ssssh"3 M1 j/ N8 b" S# t
BOOLEAN_KEY: "yes"6 _; l1 H. x9 I# m: W
3 无Dockerfile启动镜像并连接容器
: l2 z: z- V9 R8 ~# ^3 l2 z在上面的playbook中明确使用了Dockerfile来构建镜像并启动镜像提供服务,但因为Dockerfile自身也是基于基础镜像构建的,所以可以省略这个构建过程,而是直接启动基础镜像并连接到启动的容器进行操作。
! B0 W7 X3 H; d: C3 O& `下面实现与上述示例相同的效果,只是不使用Dockerfile构建。
; p4 \- C5 Y$ e( T2 ]" v. ^playbook文件内容如下:; t; Q0 b/ V% v" E+ c6 d* _
Yaml6 Q) ^. R! B- j2 v9 u- c8 w
. b0 ]& P, e2 i- D L0 ]
---
) ~) E3 ` c; y1 R( v/ T- name: start image
1 h6 p- I( m9 L7 A5 G$ r" n hosts: docker
4 I) }3 j y4 _( h: g( }: I# F& P gather_facts: no
/ f( X% U( d9 p! g a( C9 G) l vars: % u. {4 X" R. ~. h
container_name: "centos7"
7 O5 }, Y6 X8 A4 x6 g* G% n. _ tasks:
( Q% B/ O8 O- M - name: start basic container "centos7"
9 |* f; a' a h; v" W; T6 N) a docker_container:
( y, ? B+ \7 m4 s name: "{{container_name}}"
6 D9 [! L! ?& M hostname: "{{container_name}}"
9 g- P6 s: u$ i- ^4 c, w: l image: centos:centos7
w8 m, p0 C; x' n9 O ports: 8080:80
5 |8 f& [: R; i) C) V state: started
( [3 ^2 [- O- H auto_remove: yes: a& b( o+ \5 @& L/ o! H/ g" L! e0 B
command: bash4 Y0 u# G- I6 z: G F2 q4 a# h0 L
tty: yes- O+ u- ?0 d0 l8 @3 g
! H! @* j5 G2 I; [% `, U6 v
- name: add container to inventory+ r" U$ u6 S; e- r
add_host:
* q+ E' I" @/ M4 \; s name: "{{container_name}}"
3 H) G) i9 ~' C0 l7 Y/ e, l/ d* F ansible_connection: docker
( F9 }3 u7 x2 S$ `' F, H ansible_host: "{{container_name}}"
0 F& e" X7 o# [ ansible_user: root6 M9 m0 _6 {- @ @* U9 g
groups: containers, E& C- I. l% a, w
* ~2 w$ u) ^) ~8 t% l. ^- name: do something in container
8 M* i% X0 _% N; P4 y6 v* I Z hosts: containers3 K; c X- O2 F4 w) [
gather_facts: no! K" W$ l5 L) v* @5 r+ W
tasks:* m" F$ C! A% b e; n
- name: install python if needed3 E' Q$ F- g& `# i
raw: yum install -y python! q0 R, l5 w, k! v; L6 \4 V7 u2 Y
- name: remove all repos exists
' u8 R0 Q- X* ~6 j- C shell: rm -rf /etc/yum.repos.d/*; w9 q3 f7 E$ i
: [6 ^0 R+ L( a6 v+ m, E# k - name: add os repo and epel repo
+ f4 C. F$ R% N! N! x* q yum_repository: / P7 i, d- [7 J4 |; e4 K) O
name: "{{item.name}}"
1 g& g4 ~* ^+ P/ A. d description: "{{item.name}} repo"' e" W/ }5 C8 [- u* D8 P# Y
baseurl: "{{item.baseurl}}"
; @9 ]! Z: O. P file: "{{item.name}}"
- B4 i* }/ \! q7 V4 I0 r3 e enabled: 1
& n, Z. N8 i4 E& e( T$ ] gpgcheck: 0 T; |( r+ K6 F) Z9 [& U9 X, F
reposdir: /etc/yum.repos.d9 `6 k8 V0 M) @$ k% z
loop:8 m! S4 I4 U. O& T6 j
- name: os4 L3 F `/ G b
baseurl: "https://mirrors.163.com/centos/$releasever/os/$basearch". y8 R, i$ ?* V3 ~& a) E+ e
- name: epel
5 w% D# ?/ P& Q T baseurl: "https://mirrors.163.com/epel/$releasever/$basearch"
/ c. N/ [ _* _( {4 E" |6 _5 o - name: install nginx and cowsay 2 a3 c' H* U) M8 [3 }3 D! x
shell: yum -y install nginx cowsay2 N' R. f X* ?/ |/ R
2 y# Y! r% \7 g, W9 J% ]6 e/ X - name: configure nginx
" Q% e) I9 W3 Z. T0 B! d lineinfile:
% C5 i7 L; M! L0 Z" }8 m line: "daemon off;"8 ~2 l) p ^7 A) l0 L% ?" o" O# _
dest: /etc/nginx/nginx.conf
; v2 i4 S' u3 I/ F, L! p
! a3 J% g9 D% F& H5 Y - name: change index page. e% @* _# W- A. X8 S
block:
9 U* ]& D" h9 x7 ?( B5 b- h+ x # 先移除index.html,因为它可能是一个软链接) j$ U1 m1 B! G6 X$ |; a
- name: remove old index.html page
1 [. s3 h, N5 X& G6 G shell: |
* u) { S# s( g4 C3 l: M+ `! { rm -rf /usr/share/nginx/html/index.html6 m! q: b. W9 H; \* \4 F2 C. U; ?
cowsay test >/usr/share/nginx/html/index.html
- Q9 Y9 Y% L, r1 [2 j - name: run nginx7 H4 E4 C2 K9 u$ U7 y
shell: nginx &: |& U0 g7 H- H% P8 G/ }
在执行上述playbook之前,需要先在Ansible端导出连接方式docker所在主机的环境变量,比如Ansible连接到远程docker时,使用tcp:: y) v/ _8 f( h6 m: d8 b/ [
Shell
, y& x+ x/ g8 Z% w* N5 O' }& z0 \3 |
9 d7 r4 h+ j- O3 u+ N, }$ export DOCKER_HOST=tcp://192.168.8.65:2376
9 b9 N2 F/ P( M5 g7 X* |$ ansible-playbook -i inventoryname playbookname.yaml
0 s' g- S" w6 R4 A3 g- Q上面的playbook任务中,首先启动docker容器,然后使用了connection: docker连接器添加该容器到containers主机组中,以便后续的连接。之后在第二个play中连接到containers组,安装Python(CentOS系统都会带有python),配置yum源,安装Nginx和cowsay,并启动nginx。任务流程比较简单。 u1 ^+ i, g# q& m; D
唯一需要关注的是add_hosts添加容器节点到inventory时指定的docker连接方式,这里不能使用默认的ssh连接方式,因为目标容器不一定开启了ssh服务,也不一定能和外界通信,而使用connection: docker连接方式,Ansible将会先ssh连接到docker服务所在主机,然后通过docker container exec的方式连接到容器内部。
, x, I* U8 f! o/ b% {* X0 N docker inventory. v. P& f& D) R- f ~
Ansible为Docker提供了动态inventory的脚本。可下载该脚本:0 j; F) H: W; o. C T$ M
Shell
' H J+ l; ~ Z
9 y# w: t5 \3 c t: h5 D
( I" ]& Y n* g& d: R% y( i7 Qwget https://raw.githubusercontent.com/ansible/ansible/stable-2.9/contrib/inventory/docker.py6 V4 K, N( L# t9 e7 E
chmod +x docker.py' U! ?) K# c. h4 d/ D
执行该脚本测试:: A0 W) d* d) [' M0 i% {$ h+ G
Shell
7 X9 v3 R5 Z) O8 _! s1 w' s
2 {# ^1 |# ~, Y# T7 dDOCKER_HOST=tcp://192.168.8.65:2376 ./docker.py --pretty
) Q+ ^, i Y* ?% s或者直接在ansible命令或ansible-playbook命令中使用-i选项指定:
5 a. [* E% {2 Y% z& z ^Shell( e% c# D# b1 k' k& ^& h! I
8 x0 C2 Y( {2 E# T& q
ansible-playbook -i docker.py docker_containers.yml
: a& l! T& h) x, ^1 Q4 其它Ansible容器管理工具; b7 V/ F" p' {% g9 A
Ansible除了在官方提供了docker相关的模块外,还有一些第三方的工具可用来管理容器。
- o' Z4 R5 I7 o! h6 z0 {比如ansible-container、ansible-bender、Ansible Operator,它们需要单独安装,对于ansible-container来说,在之前几年比较知名,但是作者现在已经将该项目废弃,据作者本人所说,ansible-bender和Ansible Operator更好。- ]' Z0 g) Q# M$ ~
ansible-bender:
5 [" z& `% X, w简化Ansible Playbook构建容器(注:此容器是符合OCI标准的容器,docker所构建的底层容器也是OCI容器)
- O% @- W$ }1 c' `9 v地址:https://github.com/ansible-community/ansible-bender; Z- p3 Z3 \/ V: X# ]
Ansible Operator:
/ g* k( G( ]. Q$ w是Red Hat Ansible Automation和Red Hat OpenShift团队联合开发的用来将容器部署到K8s上的工具
}( E1 j+ C7 l; @" E2 Q/ o$ n0 f地址:https://learn.openshift.com/ansibleop: S2 h/ N$ h( z. X2 i" f
5 Ansible管理OpenStack
7 d% X* F5 r0 c4 B% lOpenStack可整合一台或多台物理计算机的资源来按需创建、管理、配置、删除虚拟机(在OpenStack中,虚拟机对应的术语是”计算实例”,但后文都以虚拟机来描述),对OpenStack提供者来说,提高了硬件资源的利用率;对受益用户来说,可按自己的需求申请带有各种性能、各种资源配置的操作系统,比如公有云的模式,就像去网吧上网一样,想上多久、想体验什么配置的主机都按需付费来享用。
- ?% u% H5 y+ x; ^' O6 e, l; m对于OpenStack来说,Ansible几乎是全程参与其发展的,因为从OpenStack很早的版本开始,就已经逐步支持通过Ansible来配置管理OpenStack,而Ansible管理OpenStack相关的模块也随着OpenStack的版本迭代在不断更新。目前为止,Ansible官方提供的关于OpenStack的模块已经有五十多个,下面是Ansible官方目前提供的模块列表信息简介:
3 m1 D; f5 u7 N! p+ P e; ]7 qCode: ? V5 c& c$ z/ o1 ~
, U/ K. M2 J" f2 D- q3 U( z' d3 Ios_auth – Retrieve an auth token
9 E( d9 r( M; L' kos_client_config – Get OpenStack Client config
$ O: ? q) \7 j! g- M/ X# bos_coe_cluster – Add/Remove COE cluster from OpenStack Cloud1 Y: j+ c4 T! ~0 b0 l0 R% w
os_coe_cluster_template – Add/Remove COE cluster template from OpenStack Cloud# x0 q- C) k' l! X) [( [
os_flavor_info – Retrieve information about one or more flavors
( B( M% g# j# Z/ v3 j& jos_floating_ip – Add/Remove floating IP from an instance
& d1 @* i' l- U0 L9 _5 Tos_group – Manage OpenStack Identity Groups
/ \! }3 c1 n1 ^ a- l: xos_group_info – Retrieve info about one or more OpenStack groups1 b C+ E" s% ]* u
os_image – Add/Delete images from OpenStack Cloud, O7 ~( @# t, I$ N8 V) z* a
os_image_info – Retrieve information about an image within OpenStack
# l8 e/ Y" G# Sos_ironic – Create/Delete Bare Metal Resources from OpenStack
; ]: i- I& e) k) wos_ironic_inspect – Explicitly triggers baremetal node introspection in ironic! H2 o7 P! d3 f) e. o+ j( Y
os_ironic_node – Activate/Deactivate Bare Metal Resources from OpenStack% i, x4 e9 q8 M) G
os_keypair – Add/Delete a keypair from OpenStack% b0 c- {' B0 n% B j8 ]) F
os_keystone_domain – Manage OpenStack Identity Domains; E0 j G/ [; L0 D" ?
os_keystone_domain_info – Retrieve information about one or more OpenStack domains
% ]% x5 T' I3 J+ }& ^os_keystone_endpoint – Manage OpenStack Identity service endpoints
8 h7 L) ^* ^# v" R: Z4 E9 i6 qos_keystone_role – Manage OpenStack Identity Roles' S7 X. |/ ^' I; {9 d
os_keystone_service – Manage OpenStack Identity services b1 n5 E2 f A% I" s Q& E
os_listener – Add/Delete a listener for a load balancer from OpenStack Cloud* A& u! S5 Z8 ^' Z' g4 c) m9 Y( B
os_loadbalancer – Add/Delete load balancer from OpenStack Cloud
* E* f0 O9 ~# q: ]! e" t: q' _4 ros_member – Add/Delete a member for a pool in load balancer from OpenStack Cloud8 A3 f/ ^6 n U8 o- |
os_network – Creates/removes networks from OpenStack- [7 }) K4 n6 r: ?! K
os_networks_info – Retrieve information about one or more OpenStack networks. X- J/ E# x | t
os_nova_flavor – Manage OpenStack compute flavors: w9 I9 D0 [8 s+ D4 H/ Y. Q" C
os_nova_host_aggregate – Manage OpenStack host aggregates
* z3 D% e1 Q3 H `os_object – Create or Delete objects and containers from OpenStack
, w& x+ q8 Y4 J, Z! {. [# x3 U& Wos_pool – Add/Delete a pool in the load balancing service from OpenStack Cloud
, i) b9 s# J6 Q' Mos_port – Add/Update/Delete ports from an OpenStack cloud
; w( Z. V0 X% o% I/ v. Z3 Fos_port_info – Retrieve information about ports within OpenStack( }/ e! j& l+ {% T- [6 _
os_project – Manage OpenStack Projects( O8 C8 Y! j, I' V0 m
os_project_access – Manage OpenStack compute flavors access9 g/ u% e/ g4 q! A( W
os_project_info – Retrieve information about one or more OpenStack projects4 X; ^+ _, ?/ P! Y ^ }8 W1 t; L
os_quota – Manage OpenStack Quotas
* ]7 L6 e0 ?5 ~2 g4 N" r- t$ nos_recordset – Manage OpenStack DNS recordsets
; H& f* s9 ^# _! F* Eos_router – Create or delete routers from OpenStack7 l. M1 [( `7 x* n' p9 q# s% v
os_security_group – Add/Delete security groups from an OpenStack cloud" `& ]. A% M" H% B- ]- e
os_security_group_rule – Add/Delete rule from an existing security group' x) \7 N- R# ^" ?
os_server – Create/Delete Compute Instances from OpenStack
* h0 i% b* I2 Eos_server_action – Perform actions on Compute Instances from OpenStack
7 B# K3 D7 T) o) [os_server_group – Manage OpenStack server groups3 B- K3 C% l, d) L- t1 z
os_server_info – Retrieve information about one or more compute instances7 ]0 Z5 _% z6 N" X% Z
os_server_metadata – Add/Update/Delete Metadata in Compute Instances from OpenStack
5 B1 O. ^# j' p6 ~3 _os_server_volume – Attach/Detach Volumes from OpenStack VM’s2 A4 z; p- Z" f2 u3 s
os_stack – Add/Remove Heat Stack$ [9 H9 D0 s4 r) j
os_subnet – Add/Remove subnet to an OpenStack network
- `% h* i6 ?! n" gos_subnets_info – Retrieve information about one or more OpenStack subnets
; v$ s* ?1 d% mos_user – Manage OpenStack Identity Users
' Y# H$ a( `+ b7 g4 o& Sos_user_group – Associate OpenStack Identity users and groups
?' j$ l1 W+ Z4 ?" A/ cos_user_info – Retrieve information about one or more OpenStack users
% |$ E: `. Q. ]3 k( dos_user_role – Associate OpenStack Identity users and roles
& F M" `! f( ?- z. G4 ~os_volume – Create/Delete Cinder Volumes
- n6 G- |0 N9 Y- c q1 Q& ^0 [os_volume_snapshot – Create/Delete Cinder Volume Snapshots
% o6 O( O& i/ Cos_zone – Manage OpenStack DNS zones
L. B7 e% s( T% ]9 Z# U, r* l虽然看上去很多,但大致可总结为Ansible可对以下资源做管理:1 ^# Z [( q& W! r; L0 _
(1).计算资源
& g. Q2 M/ N! e" T6 R(2).镜像管理
. F4 j8 t( ?8 A% t6 y% `1 c(3).账户管理和账户认证' h7 @& E/ v4 ~2 |
(4).网络管理
8 H3 y3 w' U8 s(5).对象存储管理
' e* |- { ?$ n. ]( T% x6 }(6).块存储管理
R" n5 f& A8 N- R0 r. Q对每种资源的管理可分为四类操作:
; s6 [0 W' I. d% K% l Q(1).获取管理目标的信息) k+ Y% ]$ `, b+ ?* O$ q
(2).添加管理目标4 s( S) g- e/ i
(3).修改管理目标的属性1 l& @1 ?/ i4 T( r+ [8 z
(4).删除管理目标
7 M" f" O; f/ y% f7 W7 N即增、删、改、查。* m7 `( I$ R j2 H
此外,由于OpenStack自身已经跟踪了其创建的每个虚拟机的信息,所以Ansible还可以直接从OpenStack中获取这些虚拟机的信息,比如从OpenStack取得某些虚拟机信息来构建动态inventory,这样就免去了手动提供虚拟机inventory的麻烦。8 Y3 w+ C, V5 J6 s7 x
本文不会介绍Ansible如何操作OpenStack自身(比如添加网络、上传镜像等),这和管理普通服务做的一些基本操作没任何区别,不同的仅仅只是做不同操作而已。本文会介绍Ansible管理OpenStack虚拟机时最常见的两种场景需求:
% ?4 l* z7 E0 K9 l% x5 `, ^2 V, y(1).使用Ansible创建虚拟机,然后像平时管理远程主机一样管理这些虚拟机,最后删除这些虚拟机3 t) u( I* @$ K
(2).从OpenStack生成动态inventory' J$ I( Y5 S/ q3 N& O+ r
14.2.1 创建虚拟机8 c' R% `$ m- ] p; c+ C
OpenStack管理虚拟机相关的模块都以os_server开头,目前包括如下6个模块:本文大概只会用到os_server模块# ^- t* K- V( B, w2 j( Z0 J& W
Code
$ }# A. E4 G& o/ l9 J: {8 l# e: C1 H5 ^0 n
os_server – 创建或删除虚拟机0 K! X/ |" z) R
os_server_action – 对虚拟机做一些操作,比如虚拟机的关机、开机、重启、暂停、恢复等操作
) S+ J4 C: M" Dos_server_group – 管理OpenStack虚拟机分组,比如测试环境的虚拟机可属于test组,生成环境的虚拟机可属于prod组
- q, M# c$ r& D; G% Sos_server_info – 检索一或多个虚拟机信息,在Ansible2.9之前,该模块名称为os_server_facts,用于检索虚拟机facts信息* f, y1 e3 u) t& X [! n
os_server_metadata – 增、删、改虚拟机的元数据信息,比如设置虚拟机的主机名、虚拟机设备信息,如网卡配置、磁盘路径/dev/sda % t/ Q7 ~. P* q! W
os_server_volume – 附加、剥离虚拟机的卷
, U; o! P% H% }9 F3 a4 J. B这6个模块都要求先安装好版本高于0.12的openstacksdk包,在CentOS 7中只需执行如下命令即可:* D% M1 ~4 x( R; N* M$ s
Shell* @, c5 B, n0 t3 p- \2 G2 b
( u, g9 h# `* L$ pip3 install openstacksdk( j3 C1 q1 I* d' ^4 d# v2 y
为了让Ansible连接到Controller进行管理,需要添加Controller的inventory信息。假如OpenStack的Controller的IP地址为192.168.8.65,可inventory文件openstack中添加如下内容:
9 K- s% F' \3 `( eUndefined
! O2 x6 T4 i/ g4 B8 ]1 ]/ R
. }8 B j, i9 w4 E2 e7 H& j[openstack_controller]
j% l4 Q# m) O! q+ m9 \# U# Z192.168.8.659 ?2 k* \: G+ P2 G
配置Ansible段和controller的ssh认证互信可自行配置,此处不赘述。. ~& Y$ r) r) {$ ]! j% H; b
然后就可以编写playbook来创建虚拟机,假如playbook文件名为create_vm.yml,其内容如下:
" M4 i) g2 D6 V4 eYaml
Q1 s1 [" n4 F& c! A% ~5 H: \) q; Y5 d( h9 s- x
- name: create vm/ U: l1 i/ C! |. N( r) Z- D' X
hosts: openstack_controller" W/ t1 \4 G& p, z* V/ @- C- B: g
gather_facts: no
0 p m- |% W2 h1 g6 x5 p" Z tasks:
2 H! \7 p9 F q9 N+ Z) T. I# U - name: Create a new instance$ X- P& F5 @6 V0 M& q
os_server:% w& \- m5 W5 M0 [$ v4 W) m+ S
state: present
$ q( P0 E& P( R" \& j auth:
9 |& ~2 @% |0 [ auth_url: http://192.168.8.65:5000/v3
2 U7 A9 M7 `( R1 a2 {) W username: admin! A. d! @1 e0 n8 H4 Q) Y
password: admin1237 U j# k% X- n& y6 s
project_name: admin2 r& X0 ~# F3 h# e* N
project_domain_name: "Default": J. W; Z) j5 T' p( T7 z0 T. A9 A
user_domain_name: "Default"# [5 b. K6 G- B7 q+ m
name: vm1
0 q8 o9 S% t3 j0 G' z( T T, I8 \$ W image: "CentOS-7.9-x86_64"* P4 r# A( @" O5 l% Z+ K- ~
key_name: ansible_key
; y1 T; k/ B8 `+ ^ timeout: 200: U7 S, c* K2 h. v' o
flavor: m1.small
9 j0 t" l7 `$ ?" l7 x1 F network: 'ext_net'/ z" K% a) \$ ]6 S! K) V
wait: yes; q0 T7 K8 R6 _# k2 v
meta:
, b2 c5 |0 k4 i H7 _* u2 g. [ hostname: test1; I, l. N( V9 E3 S- c o7 h2 F
group: test_group9 t3 b2 i1 x" t( v$ t- g( \
userdata: |; g/ L$ }- J. ~/ e! J) m3 S
{%- raw -%}#!/bin/bash4 q0 }( }2 Y$ Y4 K
cp /home/centos/.ssh/authorized_keys /root/.ssh/0 T+ M2 k+ X) y6 Z/ O/ P% z' n
{% endraw %}
$ T: F% ~; G& _; Z! W* Oauth部分是认证相关信息,name1表示创建一个名为vm1的虚拟机,image、key_name、falvor、network都是OpenStack中已经配置好的。这里还同时设置了虚拟机创建出来后的主机名为test1,并加入到了test_group主机组。
; C' q* @3 N8 B( V' F; ?6 o因为是CentOS镜像系统,该虚拟机创建出来后,默认登录用户名为”centos”,而且默认不支持root登录,为了后续可以使用root登录,上面使用userdata定义了该虚拟机创建后自定义的操作,即将保存的公钥信息拷贝到/root/.ssh目录下。
- Z( D: P" ^1 i" s. ~% m3 D注意os_server创建虚拟机任务中的一项wait: yes,它是默认选项,表示Ansible会等待虚拟机创建完成才会继续执行下面的任务。4 ^9 y7 x: k9 e: p( ^
因为所有的模块在连接OpenStack时都需要进行身份认证,为了简化playbook中的认证内容,将上面的auth选项段落的内容保存到OpenStack Controller的~/.config/openstack/clouds.yaml文件中。例如:
% t, P* N* l! T; a6 \% p) d- e! R/ i: a- GYaml
+ j, a, f! O5 `5 e/ N$ `& ]" @# \! e) u% `# n7 M, _
clouds:1 }& C4 d {3 n6 S$ f
mycloud:
5 X9 N$ ^# ]$ K* A' `( H: q auth: T+ `6 _! `. s! |& F! n
auth_url: http://192.168.8.65:5000/v3( H, Y U, W! d0 M# a
username: admin" u- ^$ m. y) N$ t
password: admin1231 x1 h) p& ?0 m4 {( {0 h
project_name: admin0 d& N0 ^( y' X, a1 J8 L) t
project_domain_name: "Default"1 c0 O W) @. C: H* [
user_domain_name: "Default"9 w' o+ b# T; a6 z' X# N( y6 G
以后在模块选项中就可以省略auth,而使用一个cloud: CLOUDNAME即可:1 d* H' |7 N2 K2 n/ y! e6 j$ l) _
Yaml& H# [( G$ D1 d! m4 k8 \1 z
( Z# F* [1 N% k) Z* F6 ?3 Y
tasks: ; o& m0 u d/ x/ ^9 r3 l. Z W
- name: Create a new instance* q. z9 ] ~9 B# V: K8 N8 W
os_server:3 v8 ^/ k% \/ W1 K; h
clouds: mycloud
6 [" s' J7 N$ H( C% U; b+ f9 j3 x( y state: present
8 c# u4 G6 j% [( b1 c: ^ name: vm1
6 d" @/ g3 R s* R. T7 ~ image: "CentOS-7-x86_64"
4 U) A! m0 t% R/ _2 R key_name: ansible_key
1 ^% H+ F8 G0 d/ Z3 a+ e timeout: 2008 P2 f% S. y$ F" U' N0 `$ x- |
flavor: m1.small# s/ _. G) b* x, x& q6 D
network: 'ext_net'
& h( Y( i# }8 O7 O wait: yes
' Z, g; x4 t! t! F meta:
" j0 i! a/ e; U4 E2 t1 ?: f hostname: test1
" n7 ]# H# X* O. r9 P | group: test_group7 q# c* X5 Z) S1 d. h9 d# T7 Q
但是要注意,将认证信息以明文方式写入文件是不安全的,可以使用Ansible的Valut加密。不过OpenStack的dashboard中也已经提供了一个环境配置文件,可以先按照如图所示的位置下载:$ g5 ?9 J7 C+ ?" {
1 q) w2 D# c6 p3 ~! e( m( {然后以source的方式执行下载到的admin-openrc.sh脚本文件:
N E! N8 Z0 m2 i3 |Shell
3 w5 S9 v! h* I8 Y, g, j+ f1 G* N$ l8 P4 @0 ?/ d
source admin-openrc.sh
# x2 ^3 K( x( z- U/ G6 K执行完后,Ansible的OpenStack相关模块执行时,auth和cloud指令都可以省略。+ T/ B7 [. x, V8 X- c' n
上面只是创建一个虚拟机实例,但很多时候可能需要一次性创建多个虚拟机。可以将每个虚拟机相关信息定义到一个变量文件中,然后去遍历想要创建的虚拟机实例。例如:9 ?" Z) ?! P4 {6 v1 Q6 q
Code
, ~- E7 G6 f5 t0 }2 E6 R7 F
! @$ q/ g/ s s---' r3 I* M2 |# n# ?: |
servers:
/ n% i% Q8 a( c - name: vm1& _) z7 K, k4 R& V% Z
image:
' \! r/ Z" ~8 f; U5 [: { flavor: , r; e% \. A& W \7 x8 N, {
key: 7 ^" c" q( m% I. }
nics:
$ A8 ^( n9 A! l4 j- `; k* j" s meta:
* [2 h, J" z' _9 c. Y! C3 D" s hostname:
+ J) l( _0 A# j, G group:
) e( O1 [ z$ z' C" T) _. m# y* _ - name: vm2
+ Q# Q/ _/ n# ^6 _9 z6 v2 v image: 3 a$ f, ^! S' t
flavor:
0 X( c+ I9 l& H" l2 X+ }3 _! P key: 2 i+ a8 P* O6 @- k- J E3 [
nics: # W3 ?6 Z9 D8 N2 d% f
meta:
* }( E( j) l/ J# O! [# I hostname: % |- x6 T& W; o* S2 E4 I
group:
6 s) y7 O$ ?7 V# }5 C+ E有了前面的Ansible基础后,此处批量创建虚拟机应该毫无难度。
' r2 {" d" |$ ^ o8 A7 C创建虚拟机后,可以将os_server的任务注册到一个变量,从而可以获取该虚拟机的信息,包括该虚拟机的IP地址:0 L! F" `1 u w4 k' n
Yml5 V: x! P* K/ `
- W, N% a0 |* P4 btasks:
( K' W0 I$ O" e - name: Create a new instance
* A" n- B, G6 {5 }0 o os_server:. p; V' w5 r; @. ]3 m+ T( I8 d2 I
cloud: mycloud8 J I5 i2 y8 _/ X! T V# M- G: t
state: present
) V7 ~% @4 z& s y4 C' f name: vm19 `0 M' q# n" |8 L ^0 g
image: "CentOS-7.9-x86_64"
8 N: Y" _8 c9 E3 x- A3 Z/ p) Y key_name: ansible_key
2 F, d: ?! V8 @! ? timeout: 200! ]" V* c- b- S* w$ [: F
flavor: m1.small, H; m& A9 ?$ k) O: t. }+ j
network: 'ext_net'
1 r2 n. ]) c' W: R! B wait: yes6 d& @$ S3 K1 o# V, g. p$ n. k
meta:$ `2 ~ ]/ R6 r1 ?9 v* U ?
hostname: test11 E; y1 |1 C1 Q* w# W, R3 g
group: test_group
. m+ J6 ~+ e, a register: newserver$ E. j2 E% a- a/ U2 I( w
$ ]. U/ K. y4 S3 t+ z) o$ I - name: get instance ip
) o+ |2 ]" ^! S' ?' O Z debug:
- H$ I/ {. \8 J( N$ F var: newserver.openstack.accessIPv4
; L3 F+ ~% [6 o# X4 s有了IP地址,对Ansible来说就获得了最关键的信息,因为只要将新虚拟机添加到Ansible inventory中,新虚拟机便像普通节点一样可接受Ansible的控制。
1 n% B; _, V" z- U- S- y8 ~9 W6、将新虚拟机动态添加到inventory5 O% F/ D% S& ^
获取到IP地址后,可以将该节点通过add_host模块动态加入到inventory中:
6 B" k- Z; o h: a+ q; \" S3 hYaml
& S* G* y! X: Y7 p1 F! W) V9 z. i
2 q Y1 C( q% T/ f; @' t& S7 u- name: add new vm to inventory; L+ G1 m/ T7 O3 L& V% q9 f( ?
add_host:
5 ^* U8 [8 U. D' g Z name: "{{ newserver.openstack.accessIPv4 }}"9 m# M& d& r$ ]! u/ Z+ j
ansible_host: "{{ newserver.openstack.accessIPv4 }}"
4 x: W' Q' G, D' P2 @7 t ansible_user: "centos"
1 E& \- M& G2 W% G ansible_port: 22
8 y% `" T& ^. i+ ]9 Q8 x groups: , {& ^8 j* ~. F: i* v. d- k
- vm_hosts
) j! m% m+ _0 Z+ K# v似乎这里的逻辑不太良好?如果虚拟机启动了但是不可连接呢?对于OpenStack创建的虚拟机来说,完全不用担心,因为os_server创建虚拟机成功后会等待该虚拟机可连接才真正返回。但对于非OpenStack的其它云主机实例则不一定,这时应当使用wait或wait_for_connection模块定义一个等待任务。尽管OpenStack中可以省略该步骤,但在脑海中应当要知道有这个步骤。
+ F j% w/ s1 b h$ D' P3 \, h此外,OpenStack安装的镜像系统可能是比较精简的系统,甚至没有安装Python,所以为了能管理这些虚拟主机,应先使用raw模块安装Python。4 d4 n! ]5 O1 I* \" m
Yaml
1 J+ L$ j8 e6 b' J" @8 ?- name: for new vm host
, C4 o/ D4 I6 ^( y, p" Y hosts: vm_hosts
0 \8 F9 x/ s2 G0 a/ ^ gather_facts: no
: ^# _+ O. {& [# A! _% B# e6 M( ~: W tasks:
/ B( Y+ y. n5 H1 B9 n5 a - name: install python if needed9 X* h( y& J& M2 E9 ^
raw: "sudo yum install -y python"8 h. _. i) ~9 d1 R8 ]* z
如此,Ansible便可以像管理普通主机一样管理OpenStack虚拟机。
1 ]1 |. F+ w/ K1 E$ n, `7、收集OpenStack虚拟机的动态inventory
9 C4 T( ~/ P- s7 _( X动态inventory一般需要写脚本(几乎是Python脚本)来收集,但即使不会Python也不用担心,因为对于OpenStack来说,官方已经提供了openstack插件,还提供了openstack_inventory.py脚本,该脚本位于Ansible官方github仓库的contrib/inventory目录下,查看文件时记得先选择对应Ansible版本的分支。
4 A; r9 ]8 u& M I' i- X下载openstack_inventory.py并设置可执行权限:
/ O- z7 N2 q$ f( d) h: CShell
; R/ g7 g# l: U2 n5 m7 s3 I" X! ~- ^; l) A2 z4 M
wget https://raw.githubusercontent.com/ansible/ansible/stable-2.9/contrib/inventory/openstack_inventory.py
' v9 _. [5 G% I q% L; Cchmod +x openstack_inventory.py
) C" E0 H$ |2 t/ x, R3 @然后source以下admin-openrc.sh脚本,再执行:. h& v; [, i5 h& R5 T" B
Shell
) O) w' l" g7 x4 A. a- k$ y$ ^* n' E) J ^9 E
source admin-openrc.sh/ P0 Q9 N" l& g* A# N
./openstack_inventory.py --list8 l, l7 i) P% g
可查看inventory信息。0 y8 C; N! z% a) _( E# E! m& M
之后要将该脚本在ansible或ansible-playbook中使用,使用-i选项指定即可:
- @, U+ Y0 y# r: t$ Q9 Q5 ~; `3 jShell. u, v# p0 T$ y
+ R$ x' F! c3 ? `0 |ansible-playbook -i openstack_inventory.py -m ping5 u# J; V3 V9 A
除了使用openstack_inventory.py脚本动态生成inventory外,还可以使用名为”openstack”的inventory插件。要使用openstack inventory插件,首先要在/etc/ansible.cfg中的[inventory]段的enable_plugins中开启script功能:
) k4 V7 @, f" y8 pShell$ {3 a9 W4 @9 `7 J& m
$ `6 O/ D* P7 h* M" e. `$ grep 'enable_plugins' /etc/ansible/ansible.cfg 3 f) l0 U8 Y+ Y$ ?! v( m; G
#enable_plugins = host_list, virtualbox, yaml, constructed$ M" a1 t4 i/ r+ ~5 w& J
取消上面的注释,加上script:
7 V' g* N x, V; wIni; z; [. I+ X) {
9 X' E2 o( [6 u
[inventory]8 S0 x5 ?3 \/ A$ W
enable_plugins = host_list, script, ini, virtualbox, yaml, constructed
0 [9 o+ Z- \2 E3 J' I5 w以后只需在yml文件中加上如下plugin指令即可:# [. C/ s F d2 C$ ^( v# {# A h
Code
8 ^5 p3 ]7 ~% G, ^/ V9 x! h7 P" z) c1 t* ? z
plugin: openstack. B Y5 |* w L0 h: B
3 n, v# C7 ]$ T0 P, {$ ^
|
|