批任务处理工具

shell

基本认识

shell 是一个用于和 linux 内核交互的工具,通常用来编写各种自动化脚本。

shell 有两种模式,交互式 和 脚本式,我们主要谈论脚本式。

shell 很强大,我认为强大来源于三方面:

  • 脚本语言,开发方便
  • 环境适应性非常非常广
  • 能够粘合各类工具

shell 也有一些问题,我认为主要在这两方面:

  • 编程范式和现代高级语言有较大差异 (几乎是纯命令式编程,且语言风格与其他语言差别较大)
  • 语言本身的功能较弱,工程能力较差 (缺乏统一标准库、模块管理能力弱)

现在大多数机器中都集成了 python,因此基于 python 在很多场景下是可以替代 shell 的,尤其是在十分需要 工程化 的项目中。

但由于 shell 和 linux 系统有着天生的兼容性,因此 shell 一定会大量运用于 linux 中的各个方面,甚至可以认为: **linux 不灭,shell 永存 **。

shell 主要难理解的地方

变量的差异

类型系统是非常非常弱的!!

在 shell 中,字面量默认都是 字符串 !

1
2
3
4
5
6
7
8
9
10
11
12
var1=123
var2=hello
var3="hello_quote"
var4='semi_quote'
var5="hello world 123"

echo hello # hello
echo "你好" # 你好
echo var2 # var2
echo $var2 # hello
echo $var3 # hello_quote
echo "$var4" # semi_quote
带空格的字符串也可以类似数组迭代
1
2
arr="hello world arr"
for i in $arr; do echo $i; done
拼字符串的时候会看起来很奇怪
1
2
var=nihao
echo \"hello world\" and $var are equal
数组的操作和其他语言不大一样
1
2
3
4
5
long=(longalong 18 "handsome")

echo ${long[0]} ${long[2]}
echo ${long[*]} # 全部
echo ${#long[*]} # 数组长度!!

数组也可以这么赋值 ages=([3]=24 [5]=19 [10]=12)

数组拼接方式

1
2
3
array1=(23 56)
array2=(99 "hello world")
array_new=(${array1[@]} ${array2[*]})

删除数组一个值

1
2
3
4
arr=(23 56 99 "hello world")
unset arr[1]

echo ${arr[*]}
map (关联数组) 需要申明,其他可申可不申
1
2
3
4
5
6
7
declare -A testmap

testmap[name]=longsang
testmap[age]=18

echo ${testmap[age]}
echo ${testmap[*]}
1
2
3
4
5
6
7
8
9
-f [name]	列出之前由用户在脚本中定义的函数名称和函数体。
-F [name] 仅列出自定义函数名称。
-g name 在 Shell 函数内部创建全局变量。
-p [name] 显示指定变量的属性和值。
-a name 声明变量为普通数组。
-A name 声明变量为关联数组(支持索引下标为字符串)。
-i name 将变量定义为整数型。
-r name[=value] 将变量定义为只读(不可修改和删除),等价于 readonly name。
-x name[=value] 将变量设置为环境变量,等价于 export name[=value]。
true 和 false 有时候是 bool ,有时候又是 字符串
1
2
3
4
5
vart=true

echo $vart

if $vart; then echo hello; fi
所有变量的直接使用都得用 $xxx
1
2
3
name=longalong

echo $name
直接命名的变量都是全局变量 (包括 function 中!!)
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
name=long

changename() {
name=longalong
}

echo $name # long
changename
echo $name # longalong


# 函数内可以使用 local name 重新申明
name=long

changename() {
echo $name # long
local name
echo $name # ""
name=longalong
echo $name # longalong
}

echo $name # long
changename
echo $name # long
大量使用子命令
1
2
3
4
5
now=`date`
echo $now

now=$(date)
echo $now
类型很弱,也就别期待 结构体、类型、对象 这类东西了

运算符差异

语言层没有算数运算符!!!
1
2
3
4
5
6
7
var1=123
var2=321
var3=123+321
var4=$var1+$var2

echo var3 : $var3 # var3 : 123+321
echo var4 : $var4 # var4 : 123+321

要使用指令进行计算

1
2
3
4
5
6
7
8
9
10
var1=123
var2=321

# 用 let 做运算
let var3=$var1+$var2
echo $var3 # 444

# 用 $(()) 做运算
var4=$(($var1 + $var2))
echo $var4 # 444

或者使用单独的命令来实现

1
2
3
4
5
var1=123
var2=321

var3=`expr $var1 + $var2`
echo $var3

需要注意的是:

以上方法都仅适用于 整数运算 ,要想做浮点运算,需要用 bc 或者 awk

关系运算符(比较运算符) 的样式很怪异
1
2
3
4
5
6
7
8
9
a=123
b=123

if [ $a -eq $b ]
then
echo "$a -eq $b : a 等于 b"
else
echo "$a -eq $b: a 不等于 b"
fi

当然也可以用 > 、< 、 >= 这种

1
2
3
4
5
6
7
8
9
a=124
b=123

if (( $a >= $b ))
then
echo "$a >= $b : a 大于等于 b"
else
echo "$a < $b: a 小于 b"
fi

需要注意的是, = 这个符号,在比较中,代表的是 “等于”,而不是赋值。

逻辑运算 && || 和布尔运算含义是一样的,但布尔运算形式和关系运算符类似
1
2
3
4
5
a=false
b=true
if [ $a -o $b ]; then
echo a or b
fi
看起来很不习惯的字符串运算符
1
2
3
4
5
6
7
假设  a=abc  b=def

= 检测两个字符串是否相等,相等返回 true。 [ $a = $b ] 返回 false。
!= 检测两个字符串是否不相等,不相等返回 true。 [ $a != $b ] 返回 true。
-z 检测字符串长度是否为0,为0返回 true。 [ -z $a ] 返回 false。
-n 检测字符串长度是否不为 0,不为 0 返回 true。 [ -n "$a" ] 返回 true。
$ 检测字符串是否不为空,不为空返回 true。 [ $a ] 返回 true。
有文件运算符这种东西
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
文件类型判断
-b filename 判断文件是否存在,并且是否为块设备文件。
-c filename 判断文件是否存在,并且是否为字符设备文件。
-d filename 判断文件是否存在,并且是否为目录文件。
-e filename 判断文件是否存在。
-f filename 判断文件是否存在,井且是否为普通文件。
-L filename 判断文件是否存在,并且是否为符号链接文件。
-p filename 判断文件是否存在,并且是否为管道文件。
-s filename 判断文件是否存在,并且是否为非空。
-S filename 判断该文件是否存在,并且是否为套接字文件。

文件权限判断
-r filename 判断文件是否存在,并且是否拥有读权限。
-w filename 判断文件是否存在,并且是否拥有写权限。
-x filename 判断文件是否存在,并且是否拥有执行权限。
-u filename 判断文件是否存在,并且是否拥有 SUID 权限。
-g filename 判断文件是否存在,并且是否拥有 SGID 权限。
-k filename 判断该文件是否存在,并且是否拥有 SBIT 权限。

文件比较
filename1 -nt filename2 判断 filename1 的修改时间是否比 filename2 的新。
filename -ot filename2 判断 filename1 的修改时间是否比 filename2 的旧。
filename1 -ef filename2 判断 filename1 是否和 filename2 的 inode 号一致,可以理解为两个文件是否为同一个文件。这个判断用于判断硬链接是很好的方法

作用域分隔符的差异

使用反过来的方式表示结束,和大多数用 {} 或者用 缩进 做区分的方式不同

1
2
3
if 和 fi
do 和 done
case 和 esac

函数调用方法的差异

  • 调用不用加小括号
1
2
3
4
5
sayhello() {
echo hello
}

sayhello
  • 参数直接跟在调用的后面
1
2
3
4
5
greet() {
echo hello $1, welcome
}

greet longalong
  • 没有形参进行接收,全部用位置表示,且从 1 开始计数
1
2
3
4
5
6
7
8
sayit() {
echo $1 $2 $3 $4 $5
echo ${11} # 超过 10 以上
echo 参数总数为 : $#
echo 所有参数为 : $*
}

sayit longalong IS VERY HANDSOME
  • return 仅返回 0-255 的数字,一般表示运行情况,而真正想要被变量接收的,则用 echo 实现
1
2
3
4
5
6
7
8
9
10
whatisreturn() {
echo hello
echo world
return
}

re=`whatisreturn`

echo $? # 0
echo $re # hello world

for 循环的差异

有一些和其他语言不一样的地方

1
for (( i=0;i<=5;i++ )); do echo $i; done
1
for i in {0..5}; do echo $i; done
1
for i in `seq 0 5`; do echo $i; done
1
for i in `cat xxx`; do echo $i; done
1
2
3
4
5
6
int=1
while(( $int<=5 ))
do
echo $int
let "int++"
done
1
2
3
4
5
6
7
a=0

until [ ! $a -lt 10 ]
do
echo $a
a=`expr $a + 1`
done
1
2
3
4
for filename in *.sh
do
echo $filename
done

分支语句的差异

  • if 语句的差别主要在关系运算符上

  • 多分支语句

1
2
3
4
5
6
7
8
9
10
num=$1

case $aNum in
1) echo '你输入了 1'
;;
2) echo '你输入了 2'
;;
*) echo '你没有输入 1 和 2'
;;
esac

文本处理的差异

  • 字符串操作
1
2
3
str="hello world"
echo ${#str} # 井号取长度,和 arr 一样,大多数语言会用函数如 len() 或者属性如 .length
echo ${str:2:-2} # 冒号为截取子串,大多数语言会用中括号加冒号,如 str[2:-2]

其他难理解的地方

  • 在 shell 交互式命令行中经常需要转义,有些转义很难理解

  • 有很多内建命令,具体参考 C语言网教程

  • 一些场景下不允许空格,一些场景下又必须空格

  • 多种重定向易蒙圈

  • 几乎没有模块系统

可以过一遍菜鸟
可以看下一个同事写的
C语言网教程 的高级部分还是值得看一下,但要付费,姑且按照相关话题,在网上搜搜资料。

ansible

先把 官方文档 看一遍
然后把 ansible-for-devops 里面的案例看一遍,这个项目的作者专门在搞 ansible,可以看 他的主页

基本理解

  • ansible 是一个 linux 批处理任务工具,通过提供 分类、批量、顺序、选择、触发 等编排能力,实现远程任务管理能力。

  • ansible 主要用来做 基础设施准备 (配置管理+置备)、 服务CICD (部署自动化)、任务编排 (构建存储系统、构建集群等等)。

  • ansible 命令方式执行,类似于在多台机器上执行命令。(和 iterm 中使用 cmd + alt + i 类似)

  • 最强大的地方在于其生态,可以从 https://galaxy.ansible.com/ 找到大量别人写好的 roles 、plugins 等等,可以很简便就集成使用。 实际上,和使用 docker-compose 起服务、使用 helm charts 起服务 一样,都可以在他人成果的基础上使用。

官方文档阅读笔记

  • ansible 使用 ini 文件 或者 yaml 文件 作为配置文件格式

  • inventory : 用来标识要管理的机器,对机器进行 分组、配置连接方式(user、jump等)、动态监控云上机器并自动修改要管理的机器实例等。 官方文档

  • playbook: 用来描述一系列任务的方式,分为 角色(role: 不同的系列任务)、任务(tasks: 一系列任务),这是大多数项目使用 ansible 的方式。 roles 主要概念

  • templates: 用来动态生成 yaml/ini 配置文件的模板,使用 jinja2 (python)。jinja文档 ansible 中的变量 ansible 中特定的变量 [重要]

  • module: 模块,ansible 的要执行的各种功能是以模块呈现的,例如 copy、创建文件 等等,不同的模块有不同的参数,ansible 的各种 tasks (实际执行的任务),都是用 模块 + 参数 的方式构成的。 ansible 的默认模块 [重要]

  • ansible 的概念结构可以参考 ansible architecture [重要]

  • playbook 是一个 任务编排 工具,抽象来看,市面上有非常多的任务编排系统,他们所面对的问题域基本都是一样的,可以以此为例子对任务编排进行梳理。

    • 调研多种任务编排系统,找到他们的共性问题

简单试用

  • 安装 ansible

    1
    yum install -y ansible
  • 设置要管理的机器 (inventory)

    1
    2
    3
    4
    5
    6
    vim /etc/ansible/hosts

    [chaos]
    172.17.5.245
    172.17.5.246
    172.17.5.247
  • 使用 shell 查看信息

    1
    2
    3
    4
    5
    6
    7
    8
    ansible chaos -m shell -a "uname -a"

    172.17.5.245 | CHANGED | rc=0 >>
    Linux prichaos003 3.10.0-1127.19.1.el7.x86_64 #1 SMP Tue Aug 25 17:23:54 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
    172.17.5.247 | CHANGED | rc=0 >>
    Linux prichaos002 3.10.0-1127.19.1.el7.x86_64 #1 SMP Tue Aug 25 17:23:54 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
    172.17.5.246 | CHANGED | rc=0 >>
    Linux prichaos001 3.10.0-1127.19.1.el7.x86_64 #1 SMP Tue Aug 25 17:23:54 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

mac 下的使用

安装 ansible

1
2
3
4
5
6
7
# 如果报错 No such file or directory @ xxx, 
# 大概率是因为设置了国内源,这是由于国内源没有同步某些包,要不再换个源,要不就:
# export HOMEBREW_BOTTLE_DOMAIN=''

brew install ansible

# pip install ansible

基本文件

config file

这是 ansible 的配置文件,会按照 $ANSIBLE_CFG、./ansible.cfg、~/.ansible.cfg、/etc/ansible/ansible.cfg 几个路径搜寻。

用来配置 ansible 的默认行为。常用的配置说明可以参考 ansible.cfg常用配置

也可以查看 官方文档

我的一个简单的配置长这样:

1
2
3
4
5
6
7
8
9
10
11
12
[defaults]
remote_port = 22
remote_user = root
forks = 10
retry_files_save_path = /tmp/.ansible-retry

host_key_checking = False
deprecation_warnings = False
command_warnings = False
gathering = smart

inventory = inventory/hosts
inventory

这是要被管理的机器的配置,主要是 host 和连接方式的配置。
文件大概长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[nfs]
10.10.10.4

[server_init]

[server]
10.10.10.5 ansible_ssh_user=root

[agent]
10.10.10.6
10.10.10.7

[remove_server]
10.10.10.5

[remove_agent]
10.10.10.6
10.10.10.7

[k3s:children]
server_init
server
agent

更多用法可以查看 官方文档

简单使用

  • 执行一个简单的 shell 命令

    1
    2
    3
    ansible nfs  -m shell -a 'uptime'
    # 10.10.10.4 | CHANGED | rc=0 >>
    # 11:57:12 up 418 days, 13:03, 1 user, load average: 0.04, 0.04, 0.05
  • 打开调试

    1
    2
    3
    4
    5
    # 调试常用 debug 模块,可以定向打印出一些信息,自己学习的时候可以看更多的信息,用 -v 或 -vvv 打开 ansible 的日志。

    ansible all -m shell -a 'uptime' -vvv

    # 一大堆信息,可以用于帮助理解 ansible 究竟干了啥

调试一下 template

ansible 的 template 在 playbook 下使用,极大扩展了 ansible 的灵活性,有啥是一个模板解决不了的呢?毕竟模板意味着编程能力。

tempalte 使用的是 python 的 jinja2 作为模板语言。就开发而言,用模板生成内容有两种模式,通过编程拼接模板 和 在模板中编程,毫无疑问,ansible 是 在模板中编程 的路子。

template 需要两个主题: 模板、变量。

变量的来源在 ansible 中比较多,主要分为两类: 内置变量、用户生成变量。
变量的使用主要有两个方面: tempalte 中, task 的逻辑语句中。

用户生成变量的地方主要有这么几个: 参考官方文档

  • 在特定文件中生成,例如 inventory 文件、playbook文件、role文件等。
  • 在命令行中传入 --extra-vars xx=xxx xx2=xxxx (简写为 -e)

具体可以查看 变量使用官方文档

写 ansible 的 playbook 需要两方面的核心知识:

  • 理解具体的业务逻辑,这样才能抽象出合适的 role
  • 理解 ansible 的运行逻辑,主要包括 各配置文件及字段含义 ( config字段palybook的字段inventory字段)、变量plugin (plugin极其重要,以后写配置时经常去里面查询各种字段含义)

需要注意的是,ansible 新版本在 plugin/module 的使用上和之前 2.9 的版本有一些差别,主要是没有命名空间的概念,例如,都是使用 file 模块(管理远端文件及文件夹),现在的版本是这样写

1
2
3
4
5
- name: Create a directory if it does not exist
ansible.builtin.file:
path: /etc/some_directory
state: directory
mode: '0755'

而 2.9 的版本是这样写

1
2
3
4
5
- name: Create a directory if it does not exist
file:
path: /etc/some_directory
state: directory
mode: '0755'

看到这样的写法,找不到对应的字段说明先不要慌张,说明是 2.9 之前的版本,具体可以查看 module index

为了简单调试,我们直接使用 命令行传入。;

使用了 templatecopyshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
---
- name: ping test send files
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "{{ item.mode }}"
with_items:
- { src: "test.sh", dest: "/tmp/longtest/test.sh" ,mode: "0755" }
- { src: "hello.txt", dest: "/tmp/longtest/hello.txt" ,mode: "0644" }

- name: ping test cat run
ansible.builtin.shell:
cmd: cat /tmp/longtest/hello.txt

- name: ping test sh run
ansible.builtin.shell:
cmd: sh /tmp/longtest/test.sh

- name: ping test template send files
ansible.builtin.template:
src: pingpong.yaml.j2
dest: "/tmp/longtest/pingpong.yaml"
mode: 0640

以下是 templates/pingpong.yaml.j2 的一部分内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---
apiVersion: v1
kind: ConfigMap
metadata:
name: pingpong
namespace: kube-system
data:
config.yaml: |-
app_name: "pingpong"
server:
port: 8080
log:
level: "info"
companyName: "longtest"
ping:
Enable: {{EXT_PINGPONG_ENABLE}}
CronPattern: "0 */2 * * * *"
Host: ""
Auth:
Enable: false

运行以下:

1
ansible-playbook -e 'EXT_GLOBAL_DOMAIN=https://longalong.cn EXT_PINGPONG_ENABLE=true' ping.yaml  -vv
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
PLAYBOOK: ping.yaml **************************************************************************************************************************************************************************
1 plays in ping.yaml

PLAY [server] ********************************************************************************************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************************************************************************************
task path: /Users/andyhu/code/mastergo-deployer/dockerfile/ping.yaml:2
ok: [101.42.240.235]

TASK [ping : ping test send files] ***********************************************************************************************************************************************************
task path: /Users/andyhu/code/mastergo-deployer/dockerfile/roles/ping/tasks/main.yaml:2
ok: [101.42.240.235] => (item={'src': 'test.sh', 'dest': '/tmp/longtest/test.sh', 'mode': '0755'}) => {"ansible_loop_var": "item", "changed": false, "checksum": "fd4ef272800af37c1e5a82235c4f1fdf35d10cfb", "dest": "/tmp/longtest/test.sh", "gid": 0, "group": "root", "item": {"dest": "/tmp/longtest/test.sh", "mode": "0755", "src": "test.sh"}, "mode": "0755", "owner": "root", "path": "/tmp/longtest/test.sh", "size": 71, "state": "file", "uid": 0}


ok: [101.42.240.235] => (item={'src': 'hello.txt', 'dest': '/tmp/longtest/hello.txt', 'mode': '0644'}) => {"ansible_loop_var": "item", "changed": false, "checksum": "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed", "dest": "/tmp/longtest/hello.txt", "gid": 0, "group": "root", "item": {"dest": "/tmp/longtest/hello.txt", "mode": "0644", "src": "hello.txt"}, "mode": "0644", "owner": "root", "path": "/tmp/longtest/hello.txt", "size": 11, "state": "file", "uid": 0}

TASK [ping : ping test cat run] **************************************************************************************************************************************************************
task path: /Users/andyhu/code/mastergo-deployer/dockerfile/roles/ping/tasks/main.yaml:11


changed: [101.42.240.235] => {"changed": true, "cmd": "cat /tmp/longtest/hello.txt", "delta": "0:00:00.053376", "end": "2023-02-15 15:01:48.824142", "msg": "", "rc": 0, "start": "2023-02-15 15:01:48.770766", "stderr": "", "stderr_lines": [], "stdout": "hello world", "stdout_lines": ["hello world"]}

TASK [ping : ping test sh run] ***************************************************************************************************************************************************************
task path: /Users/andyhu/code/mastergo-deployer/dockerfile/roles/ping/tasks/main.yaml:15
changed: [101.42.240.235] => {"changed": true, "cmd": "sh /tmp/longtest/test.sh", "delta": "0:00:00.055170", "end": "2023-02-15 15:01:51.329984", "msg": "", "rc": 0, "start": "2023-02-15 15:01:51.274814", "stderr": "", "stderr_lines": [], "stdout": "hi there, i am an ansible test now is 2023年 02月 15日 星期三 15:01:51 CST", "stdout_lines": ["hi there, i am an ansible test now is 2023年 02月 15日 星期三 15:01:51 CST"]}

TASK [ping : ping test template send files] **************************************************************************************************************************************************
task path: /Users/andyhu/code/mastergo-deployer/dockerfile/roles/ping/tasks/main.yaml:19
changed: [101.42.240.235] => {"changed": true, "checksum": "c6f37e5b898f9bcd1033afe74751741eee722ab0", "dest": "/tmp/longtest/pingpong.yaml", "gid": 0, "group": "root", "md5sum": "50ad443f786f2ff265c5f967710a1103", "mode": "0640", "owner": "root", "size": 2755, "src": "/root/.ansible/tmp/ansible-tmp-1676444511.604792-82491-153536252047673/source", "state": "file", "uid": 0}

PLAY RECAP ***********************************************************************************************************************************************************************************
101.42.240.235 : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

可视化管理工具 awx

awx 是 ansible 的可视化管理工具,是 tower 的开源版。

不得不说,awx 的文档真的是有点……

还没跑通,回头再看下吧

awx github 地址

其他


Don’t turn away from possible futures before you’re certain you don’t have anything to learn from them.
Richard Bach