Ansible Config 和 Playbook

Config

Ansible 的配置一般不需要更改,如果需要定制,自定义配置也很简单,在 Ansible 中,寻找配置按照如下顺序:

  • ANSIBLE_CONFIG (一个环境变量)
  • ansible.cfg (位于当前目录中)
  • .ansible.cfg (位于家目录中)
  • /etc/ansible/ansible.cfg

因此只需要按照文档自定义配置即可:http://ansible-tran.readthedocs.io/en/latest/docs/intro_configuration.html

Playbook

为了方便保存执行的操作,Ansible 使用了 Playbook 剧本。剧本使用 yml 格式,来避免成为了一种新语言或者脚本。

Playbook 是有 play 组成的,每个 play 包含了 host,user,tasks。

比如一个 playbook:

---
- hosts: webservers
  vars:
    http_port: 80
    max_clients: 200
  remote_user: root
  tasks:
  - name: ensure apache is at the latest version
    yum: pkg=httpd state=latest
  - name: write the apache config file
    template: src=/srv/httpd.j2 dest=/etc/httpd.conf
    notify:
    - restart apache
  - name: ensure apache is running
    service: name=httpd state=started
  handlers:
    - name: restart apache
      service: name=httpd state=restarted

host 便是指定的 hosts 文件中的主机,可以通过 remote_user 指定在远程使用的用户,也可以用 sudo 为远程操作添加 root 权限。

Task

作为远程部署工具,task 是整个 playbook 的重点。每个 task 都会在指定的所有远程主机执行,如果有执行失败的主机,将会被跳过。

每个 task 目标在于执行一个幂等(moudle)的操作,因此即使是多次执行也会很安全。一个 task 类似于下面的格式:

tasks:
  - name: make sure apache is running
    service: name=httpd state=running

一个 task 包含了名称,model,以及参数。更多的 task 写法见: http://ansible-tran.readthedocs.io/en/latest/docs/playbooks_intro.html

2017/7/26 posted in  坏笔记不如好记性

Ansible 初体验

记得刚来上海的时候,叶神给了我一本奶牛书:《奔跑吧, Ansible》。时隔一年,我居然有了写 Ansible 脚本的需求。

安装

pip install ansible

配置主机

在安装完 ansible 之后,需要在 /etc 下创建一个 ansible 文件夹,并在里面添加一个 hosts 文件,因为 ansible 会默认在 /etc/ansible/hosts 中寻找主机的配置。

因为 macOS 下 etc 文件夹不能编辑或者因为其他原因,可以通过 -i 指定 hosts 的位置。

可以创建一个 hosts 文件:

[local]
10.8.0.164

[blog]
150.95.155.202 ansible_user=user ansible_port=2222

[] 内为 target, 可以通过 targets 来对主机进行分组管理。

每一行包含一个 ip 或者域名,host 好支持一些表达式。

在每个 ip 后面可以加一些特殊的参数,比如 SSH 用户名,SSH 端口,密钥等。

详细配置可见: http://docs.ansible.com/ansible/latest/intro_inventory.html

配置完 hosts 文件之后便可以进行 ping:

$ ansible -i hosts all -m ping
10.8.0.164 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
150.95.155.202 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

也可以对某一组的主机进行 ping:

$ ansible -i hosts blog -m ping
150.95.155.202 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

执行

当能够连接上主机之后,便能对某个或者某组主机执行命令了:

$ ansible -i hosts local -m service -a "name=docker state=restarted" --become --ask-sudo-pass
SUDO password:
10.8.0.164 | SUCCESS => {
    "changed": true,
    "name": "docker",
    "state": "started"
}

其中 -m 是选择使用的模块, ansible 有大量的模块可以使用, 不同的模块负责管理不同的功能,对 ansible 的学习也就是对这些模块熟悉的过程。

--become 是作为某个用户来执行,如果添加整个参数,或默认尝试使用 root, 如果需要密码的话,还需要输入密码相关的参数, 权限相关的参数见:

  Privilege Escalation Options:
    control how and which user you become as on target hosts

    -s, --sudo          run operations with sudo (nopasswd) (deprecated, use
                        become)
    -U SUDO_USER, --sudo-user=SUDO_USER
                        desired sudo user (default=root) (deprecated, use
                        become)
    -S, --su            run operations with su (deprecated, use become)
    -R SU_USER, --su-user=SU_USER
                        run operations with su as this user (default=root)
                        (deprecated, use become)
    -b, --become        run operations with become (does not imply password
                        prompting)
    --become-method=BECOME_METHOD
                        privilege escalation method to use (default=sudo),
                        valid choices: [ sudo | su | pbrun | pfexec | doas |
                        dzdo | ksu | runas ]
    --become-user=BECOME_USER
                        run operations as this user (default=root)
    --ask-sudo-pass     ask for sudo password (deprecated, use become)
    --ask-su-pass       ask for su password (deprecated, use become)
    -K, --ask-become-pass
                        ask for privilege escalation password

使用 root 用户:

Hypo-MBP:~ hypo$ ansible -i hosts local -a "whoami"
10.8.0.164 | SUCCESS | rc=0 >>
hypo

Hypo-MBP:~ hypo$ ansible -i hosts local -a "whoami" --become --ask-su-pass
SUDO password:
10.8.0.164 | SUCCESS | rc=0 >>
root

Playbook

通过命令行总归不是优雅的姿势,还是需要能存储的固定的脚本,每次部署的时候运行这个脚本,在 ansible 里便是 playbook。

NEXT 见:Ansible Playbook

2017/7/25 posted in  坏笔记不如好记性

Werkzeug 常用中间件

昨天记了手脚架 Werkzeug 的几个小工具( Werkzeug 的功能函数 ),今天再补几个中间件。之前在 HackerProxy (原gateway)中,便是通过自定义了一堆 Middleware,来把用户验证等常用逻辑完成的。第一次看到这种俄罗斯套娃式的代码我是震惊的,不过现在认为 Middleware 的确可以适当做一些比较轻的处理。

SharedDataMiddleware

SharedDataMiddleware 是提供一个静态文件分享(下载)的路由。和 flask 中默认的 static 不同,flask 是利用的 send_file ,而 SharedDataMiddleware 可以直接在 app 里注册相关的路由,并绑定一个磁盘路径,并分享这个路径下的文件。

import os
from werkzeug.wsgi import SharedDataMiddleware

app = SharedDataMiddleware(app, {
    '/shared': os.path.join(os.path.dirname(__file__), 'shared')
})

ProfilerMiddleware

ProfilerMiddleware 是一个查看性能的中间件,它会在 profile_dir 下写入访问页面的程序运行状况,包括执行了那些函数,以及运行时间等。

from werkzeug.contrib.profiler import ProfilerMiddleware

app.wsgi_app = ProfilerMiddleware(app, profile_dir="/tmp")

但是如果使用 flask-debugtoolbar 这个拓展,便可以在 Web 上直接查看结果,因此不太建议直接用这个中间件。

DispacherMiddleware

DispatcherMiddleware 是个蛮好玩的中间件,他可以向一个 app 注册其他 app:

from werkzeug.wsgi import DispatcherMiddleware

app.wsgi_app = DispatcherMiddleware(app, {
    '/app2':        app2,
    '/app3':        app3
})

这时,访问 /app2 前缀的 url 就会使用 app2 的相关逻辑。如果对于一个程序可能有分开的需求,可以利用 DispacherMiddleware 做一些插拔操作。

2017/7/13 posted in  Python 黑魔法

Werkzeug 的功能函数

之前在 v2ex 上看到,有人问什么是手脚架,有人举例子,在 Python 中 Werkzeug 便像手脚架,而 flask 就像毛坯房。以此形容 Werkzeug 中有一堆还不错的工具。当然,如果在 flask 中使用 Werkzeug 的话,中间件更是个好东西:Werkzeug 常用中间件

数据结构

Werkzeug 内置里几个常用的数据结构,如果有相关需求就不用重复造轮子了。

TypeConversionDict

如字面意,这是一个类型转换字典,该字典提供一个 get 方法,接受三个参数,key, default, type。

>>> from werkzeug.datastructures import TypeConversionDict
>>> d = TypeConversionDict(foo='42', bar='blub')
>>> d.get('foo', type=int)
42
>>> d.get('bar', -1, type=int)
-1

ImmutableTypeConversionDict

ImmutableTypeConversionDict,便是 TypeConversionDict 的不可变版。

>>> from werkzeug.datastructures import ImmutableTypeConversionDict
>>> dic = ImmutableTypeConversionDict(key="value")
>>> dic.get("key")
'value'
>>> dic["key"] = "v2"
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    dic["k2"] = "v2"
  File "/Library/Python/2.7/site-packages/werkzeug/datastructures.py", line 182, in __setitem__
    is_immutable(self)
  File "/Library/Python/2.7/site-packages/werkzeug/datastructures.py", line 28, in is_immutable
    raise TypeError('%r objects are immutable' % self.__class__.__name__)
TypeError: 'ImmutableTypeConversionDict' objects are immutable

MultiDict

MultiDict 这个东西在我第一次看到时候并没有理解这是做什么的,这是一个允许一个 key 有多个 value 的字典,但是如果当做普通字典用时,只会返回第一个值。

>>> d = MultiDict([('a', 'b'), ('a', 'c')])
>>> d
MultiDict([('a', 'b'), ('a', 'c')])
>>> d['a']
'b'
>>> d.getlist('a')
['b', 'c']
>>> 'a' in d
True

ImmutableMultiDict

ImmutableMultiDict 就是 MultiDict 的不可变版,在 flask 中的 request.args 便是一个 ImmutableMultiDict。

OrderedMultiDict

如字面意,OrderedMultiDict 是 MultiDict 中的 key 按字典序的版。

ImmutableOrderedMultiDict

同上,ImmutableOrderedMultiDict 为 OrderedMultiDict 不可变版。

功能函数

werkzeug.utils 下有一堆好玩的工具,可以在使用 flask 的时候直接拿来用。

cached_property

werkzeug.utils 中有个 cached_property 可以作为装饰器的类,这个装饰器和 @property 有相同的效果,不过被装饰的函数只会第一次运行,然后后面只会返回被缓存的结果。

In [17]: class Foo(object):
    ...:
    ...:     @cached_property
    ...:     def foo(self):
    ...:         print("hello word")
    ...:         return "hello"
    ...:

In [18]: obj = Foo()

In [19]: obj.foo
hello word
Out[19]: 'hello'

In [20]: obj.foo
Out[20]: 'hello'

import_string

import_string 可以通过字符串导出需要导入的模块:

In [21]: from werkzeug.utils import import_string

In [22]: import_string("flask")
Out[22]: <module 'flask' from '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/flask/__init__.py'>

In [23]: import_string("flask.Flask")
Out[23]: flask.app.Flask

secure_filename

可以用来生成一个合法的文件名:

>>> secure_filename("My cool movie.mov")
'My_cool_movie.mov'
>>> secure_filename("../../../etc/passwd")
'etc_passwd'
>>> secure_filename(u'i contain cool \xfcml\xe4uts.txt')
'i_contain_cool_umlauts.txt'

密码加密

之前在学校写东西,每次遇到密码加密,都是自己加各种盐,然后找个 md5 的库,加上几遍,对于密码的处理一直有 java 的风范。

Werkzeug 里面是有一套加密和密码验证的工具的。在 werkzeug.security 中,有 generate_password_hash, check_password_hash, 一对好用的工具。

generate_password_hash 接受三个参数:明文密码,加密方法(method, 默认为’pbkdf2:sha256’),盐的长度(salt_length,默认为8)。

In [25]: generate_password_hash("password")
Out[25]: 'pbkdf2:sha256:50000$F6gTN2Eh$52e209ed4431f9268d1bf16295439c46b25ab306acff72615837bdf268fee361'

In [26]: generate_password_hash("password")
Out[26]: 'pbkdf2:sha256:50000$GWo5sU55$729f84eb83e02549312fc4fc51db0614cbc783710902006dc510ccdac0a2b937'

返回的密文是:method$salt$hash 格式。并且,因为有盐的存在,所以同一个密码,并不会有相同的结果。

对于密码的验证:

In [27]: p = generate_password_hash("password")

In [29]: check_password_hash(p, "password")
Out[29]: True

In [30]: check_password_hash(p, "password1")
Out[30]: False
2017/7/12 posted in  Python 黑魔法

Python 生成验证码

在 InfoSub 接入验证码的时候,第一次尝试使用验证码,设计的比较简单,利用的是 captchaitsdangerous 模块。

在创建验证码的时候,后台随机生成一个字符串作为验证码(captcha_code),然后利用 itsdangerous.URLSafeTimedSerializer 对这个字符串进行编码,生成一个安全的(不能被随意反向编码,反向编码需要秘钥),方便的(可以直接扔到 url 里)的字符串。把这个字符串作为验证码的 id(captcha_id)。

这样使得校验验证码非常简单,利用秘钥对 captcha_id 反向编码便可以得到 captcha_code,将 captcha_code 与用户的输入进行比对便可以知道用户的验证码是否正确。

配置

首先要配置用于编码的 serializer:

serializer = URLSafeTimedSerializer(SECRET_KEY)

这样便可以使用 serializerloadsdumps 用于编码和反向编码:

captcha_id = serializer.dumps(captcha_code)
captcha_code = serializer.loads(captcha_id, max_age=max_age)

serializer.loads 方法接受一个 max_age 参数表示过期时间,如果时间超时,将返回空。

生成图片

生成图片用的是 captcha 库,首先是配置:

image = ImageCaptcha(width=160, height=60, fonts=[os.path.join(FONTS_PATH, 'CourierNew-Bold.ttf'),
                                                  os.path.join(FONTS_PATH, 'LiberationMono-Bold.ttf')])

可以指定图片大小,字体,字体大小。

返回图片有两种方式,一个是可以直接拿到图片的 IO 流(一个字节数组),或者直接持久化到硬盘上:

# 获得字节数组
data = image.generate(self.captcha_code)

# 将图片存储到 file_path 这个位置
file_path = tempfile.mkstemp(prefix="INFO-SUB-CAPTCHA-", suffix=".png")
image.write(self.captcha_code, file_path)

考虑到验证码的时效性,便没有采用持久化硬盘上这个操作,而是直接把图片流返回了。

封装的 Captcha 类:

class Captcha(object):
    def __init__(self, captcha_code=None):
        if captcha_code:
            self.captcha_code = captcha_code
        else:
            self.captcha_code = ''.join(random.sample(string.digits + string.lowercase + string.uppercase, 6))
        self.captcha_id = serializer.dumps(self.captcha_code)

    def image(self):
        data = image.generate(self.captcha_code)
        return data

    def save(self):
        file_path = tempfile.mkstemp(prefix="INFO-SUB-CAPTCHA-", suffix=".png")
        image.write(self.captcha_code, file_path)
        return file_path

    def validate(self, code):
        code = code.strip()
        if self.captcha_code.lower() == code.lower():
            return True
        return False

    @classmethod
    def get_by_captcha_id(cls, captcha_id, max_age=60 * 30):
        try:
            captcha_code = serializer.loads(captcha_id, max_age=max_age)
        except:
            captcha_code = None
        if not captcha_code:
            return None
        return cls(captcha_code)

获得验证码图片

因为使用的 flask,而 flask 有 send_file 函数将图片等文件返回:

@view_blueprint.route("/captcha/<captcha_id>")
def get_captcha(captcha_id):
    captcha = Captcha.get_by_captcha_id(captcha_id)
    if not captcha:
        abort(404)
    return send_file(captcha.image(), mimetype='image/png')

这样,直接访问带 captcha_id 的 url 将直接返回验证码图片。

<img src="{{ url_for('view.get_captcha', captcha_id=captcha_id) }}" width="100%" class="img-rounded">

验证操作

为了简单起见,我将验证操作写在了 WTF 表单中,比如登录表单:

class LoginForm(FlaskForm):
    username_or_email = StringField(u"用户名或注册邮箱", validators=[DataRequired(u"登录名不能为空")])
    password = PasswordField(u"密码", validators=[DataRequired(u"密码不能为空")])
    remember = BooleanField(u"记住我", default=False)
    captcha_code = StringField(u"验证码")
    captcha_id = HiddenField()

    def validate_password(self, field):
        user = get_user_by_username_or_email(self.username_or_email.data)
        if user and user.is_active and user.check_password(field.data):
            return
        raise ValidationError(u"用户名或密码错误")

    def validate_captcha_code(self, field):
        if current_app.config.get("DEBUG"):
            return
        if self.captcha_id.data:
            captcha = Captcha.get_by_captcha_id(self.captcha_id.data)
            if captcha.validate(field.data):
                return
        raise ValidationError(u"验证码错误")

在 view 层创建了 form 表单,在返回之前将 captcha_id 填充进去,在提交的时候,便会对验证码进行验证。

2017/7/10 posted in  Python 黑魔法