├── 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 |

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 | 


--------------------------------------------------------------------------------