├── hosts.py
├── test.yaml
├── LICENSE
├── templates
└── index.html
├── app.py
├── README.md
└── test_playbook.py
/hosts.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | '''
4 | '''
5 |
6 | import json
7 | import os
8 | group = os.environ.get('ANSBILE_HOST_GROUP')
9 |
10 | hosts = {
11 | 'group1': ['localhost', '127.0.0.1'],
12 | 'group2': ['127.0.0.1'],
13 | }
14 |
15 | ret = hosts.get(group, [])
16 | ret = {"default": ret}
17 |
18 | print json.dumps(ret, indent=4)
19 |
--------------------------------------------------------------------------------
/test.yaml:
--------------------------------------------------------------------------------
1 | - name: web service
2 | remote_user: phpa
3 | hosts: localhost
4 | vars:
5 | packages: nginx
6 | tasks:
7 | - name: install nginx
8 | yum: name={{ packages }} state=present
9 | tags: install
10 | - name: service nginx start
11 | service: name=nginx enabled=yes state=started
12 | sudo: yes
13 | tags: start
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Miguel Grinberg
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Flask + Celery + Ansible
4 |
5 |
6 | Flask + Celery + Ansible
7 | 执行Playbook部署任务
8 |
9 |
10 |
11 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import random
3 | import time
4 | from flask import Flask, request, render_template, session, flash, redirect, \
5 | url_for, jsonify
6 | from celery import Celery
7 | from test_playbook import get_pb
8 |
9 | app = Flask(__name__)
10 | app.config['SECRET_KEY'] = 'top-secret!'
11 |
12 | # Celery configuration
13 | app.config['CELERY_BROKER_URL'] = 'redis://localhost:6379/0'
14 | app.config['CELERY_RESULT_BACKEND'] = 'redis://localhost:6379/0'
15 |
16 |
17 | # Initialize Celery
18 | celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'])
19 | celery.conf.update(app.config)
20 |
21 |
22 | @celery.task(bind=True)
23 | def long_task(self):
24 | self.logs = []
25 | r = get_pb(self).run()
26 | self.logs.append("finish playbook")
27 | self.logs.append(str(r))
28 | return self.logs
29 |
30 |
31 | @app.route('/play', methods=['GET', 'POST'])
32 | def index():
33 | return render_template('index.html', email=session.get('email', ''))
34 |
35 |
36 | @app.route('/play/longtask', methods=['POST'])
37 | def longtask():
38 | task = long_task.apply_async()
39 | return jsonify({}), 202, {'Location': url_for('taskstatus', task_id=task.id)}
40 |
41 |
42 | @app.route('/play/status/')
43 | def taskstatus(task_id):
44 | task = long_task.AsyncResult(task_id)
45 | if task.state == 'PENDING':
46 | response = {
47 | 'state': task.state,
48 | 'status': 'Pending...'
49 | }
50 | elif task.state != 'FAILURE':
51 | response = {
52 | 'state': task.state,
53 | 'status': task.info
54 | }
55 | if 'result' in task.info:
56 | response['result'] = task.info['result']
57 | else:
58 | # something went wrong in the background job
59 | response = {
60 | 'state': task.state,
61 | 'status': task.info, # this is the exception raised
62 | }
63 | return jsonify(response)
64 |
65 |
66 | if __name__ == '__main__':
67 | app.run(debug=True)
68 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 介绍
2 |
3 | ansible是一个基于SSH协议的自动化部署工具,但官网提供的例子大多都是在命令行下执行的,
4 | 对于python api只提供了很简单的信息,对于批量任务(playbook)的api,官方甚至没有提供
5 | 文档。
6 |
7 | 但是要用ansible实现一套自动化部署系统,是必须要有个界面提供给使用者的, 使用者可以请求
8 | 执行一个批量部署任务,并获取任务执行进度和最终状态,在这期间,不能长时间占用web worker
9 | 的时间。
10 |
11 | 所以查了一些资料,最后用flask,celery,ansible来写了个demo
12 |
13 | ## 执行
14 |
15 | 启动redis,作为celery的broker和result backend
16 | $ service redis start
17 | 启动celery来接受任务处理
18 | $ celery worker -A app.celery --loglevel=info
19 | 启动flask web app
20 | $ python app.py
21 | 浏览器里打开http://localhost/play,按界面执行就可以了
22 |
23 |
24 | ## 文件介绍
25 |
26 | ### hosts.py
27 |
28 | 使用ansible进行自动化部署,需要先定义Inventory,一般来说Inventory是一个静态文本文件,
29 | 里面定义了ansible可管理的主机列表,但是有时候每个用户可管理的机器列表是不一样的,
30 | 或者要管理的机器列表经常变化,比如要和公司的CMDB去集成,这就需要用到 Dynamic Inventory
31 |
32 | 这个脚本是根据环境变量来获取指定的机器分组, 具体实现可从数据里读取可管理的机器列表。
33 |
34 | 参考链接:
35 |
36 | - [Dynamic Inventory](http://docs.ansible.com/intro_dynamic_inventory.html)
37 | - [Developing Dynamic Inventory Sources](http://docs.ansible.com/developing_inventory.html)
38 |
39 | ### test.yaml
40 |
41 | 这里定义了一个ansible playbook,就是一个部署任务,安装nginx,并启动nginx。
42 | 可以用如下代码执行该playbook
43 |
44 | $ ansible-playbook -i hosts.py test.yaml
45 |
46 | 参考链接:
47 |
48 | - [Playbooks](http://docs.ansible.com/playbooks.html)
49 |
50 | ### test_playbook.py
51 |
52 | 该文件是利用了ansible的python api,去以编程的方式,而不是命令行的方式去执行一个playbook。
53 | 可以直接用如下命令去执行
54 |
55 | $ python test_playbook.py
56 |
57 | 注意这里的`PlaybookRunnerCallbacks`和`PlaybookCallbacks`两个类,是我自己定义过的,主要目的
58 | 是在每个task执行开始,执行成功或失败时,调用`update_state`去更新celery任务状态, 每个类的
59 | `celery_taski`成员是一个celery的task,在构造函数里传入的。
60 |
61 | 参考链接
62 |
63 | - [celery custom states](http://docs.celeryproject.org/en/latest/userguide/tasks.html#custom-states)
64 | - [ansible python api](http://docs.ansible.com/developing_api.html)
65 | - [Running ansible-playbook using Python API](http://stackoverflow.com/questions/27590039/running-ansible-playbook-using-python-api)
66 |
67 | ### app.py
68 |
69 | 这是一个flask web app,其中`/play/longtask`是发起一个异步任务,也就是执行playbook,该请求会返回
70 | 轮询检测任务执行状态的url,包含了该异步任务的ID, 如`/play/status/111` 。
71 |
72 | 参考链接
73 |
74 | - [Using Celery with Flask](http://blog.miguelgrinberg.com/post/using-celery-with-flask)
75 |
76 | ### templates/index.html
77 |
78 | 在界面上点击按钮发起执行playbook的请求,然后自动轮询获取任务执行状态并显示。在整个任务执行过程中
79 | web应用不会被hang住,真正的任务执行是celery执行的。
80 |
81 | ## 参考链接:
82 |
83 | - [自动化运维工具之ansible](http://guoting.blog.51cto.com/8886857/1553446)
84 | - [配置管理工具之Ansible视频教程(共10课时)_](http://edu.51cto.com/index.php?do=lession&id=38985)
85 |
--------------------------------------------------------------------------------
/test_playbook.py:
--------------------------------------------------------------------------------
1 | from ansible.playbook import PlayBook
2 | from ansible.inventory import Inventory
3 | from ansible import callbacks
4 | from ansible import utils
5 |
6 |
7 | class PlaybookRunnerCallbacks(callbacks.PlaybookRunnerCallbacks):
8 | def __init__(self, task, stats, verbose=None):
9 | super(PlaybookRunnerCallbacks, self).__init__(stats, verbose)
10 | self.celery_task = task
11 |
12 | def on_ok(self, host, host_result):
13 | super(PlaybookRunnerCallbacks, self).on_ok(host, host_result)
14 | self.celery_task.logs.append("ok:[%s]" % host)
15 | self.celery_task.update_state(state='PROGRESS', meta={'msg': self.celery_task.logs})
16 |
17 | def on_unreachable(self, host, results):
18 | super(PlaybookRunnerCallbacks, self).on_unreachable(host, results)
19 | self.celery_task.logs.append("unreachable:[%s] %s" % (host, results))
20 | self.celery_task.update_state(state='FAILURE', meta={'msg': self.celery_task.logs})
21 |
22 | def on_failed(self, host, results, ignore_errors=False):
23 | super(PlaybookRunnerCallbacks, self).on_failed(host, results, ignore_errors)
24 | self.celery_task.logs.append("failed:[%s] %s" % (host, results))
25 | self.celery_task.update_state(state='FAILURE', meta={'msg': self.celery_task.logs})
26 |
27 |
28 | class PlaybookCallbacks(callbacks.PlaybookCallbacks):
29 | def __init__(self, task, verbose=False):
30 | super(PlaybookCallbacks, self).__init__(verbose);
31 | self.celery_task = task
32 |
33 | def on_setup(self):
34 | super(PlaybookCallbacks, self).on_setup()
35 | self.celery_task.logs.append("GATHERING FACTS")
36 | self.celery_task.update_state(state='PROGRESS', meta={'msg': self.celery_task.logs})
37 |
38 | def on_task_start(self, name, is_conditional):
39 | super(PlaybookCallbacks, self).on_task_start(name, is_conditional)
40 | self.celery_task.logs.append("TASK: [%s]" % name)
41 | self.celery_task.update_state(state='PROGRESS', meta={'msg': self.celery_task.logs})
42 |
43 |
44 | hostfile = './hosts.py'
45 | inventory = Inventory(hostfile)
46 | stats = callbacks.AggregateStats()
47 | vars = {"hosts":['127.0.0.1']}
48 |
49 |
50 | def get_pb(task):
51 | if task:
52 | runner_cb = PlaybookRunnerCallbacks(task, stats, verbose=utils.VERBOSITY)
53 | playbook_cb = PlaybookCallbacks(task, verbose=utils.VERBOSITY)
54 | else:
55 | runner_cb = callbacks.PlaybookRunnerCallbacks(stats, verbose=utils.VERBOSITY)
56 | playbook_cb = callbacks.PlaybookCallbacks(verbose=utils.VERBOSITY)
57 |
58 | pb = PlayBook(playbook='./test.yaml',
59 | callbacks=playbook_cb,
60 | runner_callbacks=runner_cb,
61 | stats=stats,
62 | inventory=inventory,
63 | extra_vars=vars,
64 | )
65 | return pb
66 |
67 | if __name__ == '__main__':
68 | get_pb(None).run()
69 |
--------------------------------------------------------------------------------