运维咖啡吧

享受技术带来的乐趣,体验生活给予的感动

用Python写个Linux系统命令

这篇文章介绍如何写个系统命令以及我为什么要写命令

“一切皆文件”是linux的基本哲学之一,我们在linux下执行的诸如ls之类的命令实际上都是去执行了系统上的某个文件,which命令可以查看到我们执行的命令对应的是系统上的哪个文件,例如常用的ls命令实际上就是执行了/bin/ls这个文件

root@ops-coffee:~# which ls
/bin/ls

基于此,我们就知道了定义一个命令很简单,只需要写个可执行的文件就行了,python的标准模块argparse就可以帮助我们快速方便的构建一个用户友好的命令

argparse

相比于自己实现个命令文件,argparse模块能够自动生成帮助和使用手册,并在用户传入无效参数时报错。一个简单的示例如下

#!/usr/bin/env python3
# coding:utf8

import argparse

parser = argparse.ArgumentParser(description='整数处理')
parser.add_argument('integers', type=int, help='要处理整数')

args = parser.parse_args()
print(args.integers)

这个示例的意思是接收一个数字,并将这个数字输出,接下来看一下详细的解释

首先创建了一个ArgumentParser对象,ArgumentParser对象有很多参数可以选择,这里的description定义在参数帮助文档之前显示的文本,通常用来定义这个程序做什么以及怎么做

parser = argparse.ArgumentParser(description='整数处理')

然后通过add_argumentArgumentParser对象添加参数,第一个参数integers为参数名,type指定类型为inthelp指定这个字段的帮助信息

parser.add_argument('integers', type=int, help='要处理的整数')

通过调用ArgumentParser对象的parse_args方法返回一个具有所有参数属性的对象

args = parser.parse_args()

最后通过args.参数名获取到传入的参数值

print(args.integers)

执行

我们将以上文件命名为opscoffee,并赋予执行权限,放在系统环境变量/bin下,就可以当作命令直接执行了

# chmod +x opscoffee
# mv opscoffee /bin/

如果直接执行opscoffee命令的话将会收到一个报错,提示你必须有一个参数integers

# opscoffee
usage: opscoffee [-h] integers
opscoffee: error: the following arguments are required: integers

同时也通过usage告诉了你这个命令的用法,默认有一个-h参数可以打印命令帮助

# opscoffee -h
usage: opscoffee [-h] integers

整数处理

positional arguments:
  integers    要处理的整数

optional arguments:
  -h, --help  show this help message and exit

这就是使用argparse模块的好处,自动生成帮助,提供友好的使用体验

参数

argparse能实现的远不止于此,还有更加强大的功能,主要在于add_argument方法参数的运用,以下以一些例子来学习下一些常用的参数

可选参数

当我们在参数名前添加-或者--时,argparse会默认认为这是一个可选参数,可以不传值,例如

parser.add_argument('--age', type=int, help='年龄')

可选参数当没有传值时的默认值为None,可以通过default来设置默认值

parser.add_argument('--age', type=int, default=37, help='年龄')

当没有参数--age时,显示default设置的值(没有设置default则显示None),有--age则显示--age指定的值

# opscoffee
37
# opscoffee --age 38
38

如果你想让可选参数也变成必选的,则只需要设置required=True即可

parser.add_argument('--age', type=int, default=37, required=True, help='年龄')

type

type用来指定参数的类型,允许任何类型检查和类型转换,例如strintfloatopen甚至是你自定义的方法都可以。

#!/usr/bin/env python3
# coding:utf8

import argparse

def even(string):
    value = int(string)

    if value%2!=0:
        msg = "%r 不是偶数" % string
        raise argparse.ArgumentTypeError(msg)
    return value

parser = argparse.ArgumentParser(description='整数处理')
parser.add_argument('integers', type=even, help='要处理的整数')

args = parser.parse_args()
print(args.integers)

以上命令接收一个参数integers,并将其type设置为了自定义方法even,这个方法会判断用户输入的数字是否是偶数,如果不是则报错,执行结果如下

# opscoffee 1
usage: opscoffee [-h] integers
opscoffee: error: argument integers: '1' 不是偶数
# opscoffee 2
2

choices

choices参数可以限制参数的范围,例如我们只想让用户从opscoffee两个参数中选择一个输入,则可以这样用

parser.add_argument('site', choices=['ops','coffee'], help='Site')

那么当我们输入的内容不是opscoffee时,则报错

# opscoffee cn
usage: opscoffee [-h] {ops,coffee}
opscoffee: error: argument site: invalid choice: 'cn' (choose from 'ops', 'coffee')

nargs

通常我们在argparse中定义的参数数量与传入的参数数量应当相等,但有些时候我们需要接收未知数量的参数,nargs就可以帮助我们

nargs支持以下值:N(整数)、'?''*''+'argarse.REMAINDER

N: 表示N个参数会被聚集到一个列表中,例如

import argparse

parser = argparse.ArgumentParser(description='整数处理')
parser.add_argument('integers', type=int, nargs=2, help='要处理的整数')

args = parser.parse_args()
print(args.integers)

将输出

# opscoffee 9 10 
[9, 10]

'?': 表示使用一个或不使用参数,当不传参数时,默认为None

parser.add_argument('integers', type=int, nargs='?', help='要处理的整数')

'*': 表示使用所有参数,参数个数可以为0

parser.add_argument('integers', type=int, nargs='*', help='要处理的整数')

'+':'*'类似,但至少要有一个参数,否则将会报错

parser.add_argument('integers', type=int, nargs='+', help='要处理的整数')

自定义命令

先说我为什么要写个系统命令?

文章『Probius:一个功能强大的自定义任务系统』中介绍了我们的自定义任务系统,这个系统可以用来编排任务,而在编排CICD任务中会用到配置文件,我们的配置文件都是通过Kerrigan配置中心来管理的,目前获取配置中心的配置主要有两种方法

  1. 配合confd服务自动拉取更新,详细内容可以查看这篇文章:中小团队落地配置中心详解
  2. 配置中心提供API,可以通过API获取配置内容

脚本里如果想要使用配置中心的配置,则只能通过API的方式去获取,这样就要在每个需要用到配置的地方写一段代码来获取及处理,不仅会出现大量的重复代码,并且非常的不优雅,更为重要的是请求API的Token将会出现在脚本里,带来一定的安全风险

基于以上考虑,写个自定义命令来做这件事情更为妥当,于是便写了下边这个命令

#!/usr/bin/env python3
# coding:utf8

# 这是一个系统命令用来获取kerrigan配置中心的配置并写入本地文件,需要将此文件copy到目录/bin下

import sys
import argparse
import requests

parser = argparse.ArgumentParser(description='获取配置中心Kerrigan配置')

parser.add_argument('configkey', type=str, help='配置中心中文件的Key')
parser.add_argument('localfile', type=str, help='保存到本地文件的路径')

args = parser.parse_args()

# 获得传入的参数
configkey = args.configkey
localfile = args.localfile

header = {
    'Authorization': 'Token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.JTdCJTIyZXhwJTIyJTNBMTkwNjcwNzAyMCUyQyUyMmlhdCUyMiUzQTE1OTEzNDcwMjAlMkMlMjJkYXRhJTIyJTNBJTdCJTIydXNlcm5hbWUlMjIlM0ElMjJwcm9iaXVzQG9wcy1jb2ZmZWUuY24lMjIlN0QlN0Q.ops1ZNhq19XSEL2PUo-iQqzbhimDnpFiYc_7EUXftF4'}

uri = 'http://kerrigan.ops-coffee.cn/api/config/?key=' + configkey
r = requests.get(uri, headers=header)

if r.json()['state']:
    content = r.json()['message']['content']

    try:
        with open(localfile, 'w') as f:
            f.write(content)

        sys.exit(0)
    except Exception as e:
        print('write local file failed: ', str(e))
        sys.exit(3)
else:
    print('get config failed: ', r.json()['message'])
    sys.exit(1)

以上代码的意思是根据传入的key和file路径,去配置中心获取对应配置文件的内容并写入到本地file中。需要注意的是exit,返回合适的退出状态是个很好的习惯,这样我们就可以通过$?来获取命令执行成功还是失败

将此文件命名为getconfig并移动到/bin目录下添加执行权限,就可以在系统任何地方使用getconfig命令了

getconfig /conf/coffee/prod/docker/Dockerfile  /home/project/coffee/Dockerfile