├── LICENSE ├── README.md ├── app.py ├── hosts.py ├── templates └── index.html ├── test.yaml └── test_playbook.py /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Flask + Celery + Ansible 4 | 5 | 6 |

Flask + Celery + Ansible

7 |

8 |

 9 |     
10 |     
11 |     
48 |   
49 | 
50 | 


--------------------------------------------------------------------------------
/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 | 


--------------------------------------------------------------------------------
/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 | 


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