-
Notifications
You must be signed in to change notification settings - Fork 1
/
app.py
251 lines (228 loc) · 9.39 KB
/
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# -*- coding: utf8 -*-
#
# Copyright (C) 2015 Daniel Lombraña González
#
# Deployments is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Deployments is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Deployments. If not, see <http://www.gnu.org/licenses/>.
from flask import Flask, request, abort, Response, url_for
from subprocess import Popen, PIPE, CalledProcessError
import config
import hmac
import hashlib
import json
import requests
import ansible.playbook
from ansible import callbacks
from ansible import utils
from ansible.errors import AnsibleError
app = Flask(__name__)
@app.route("/getstatus")
def get_status():
"""Get status."""
url = request.args.get('url')
if url:
token = config.TOKEN
headers = {'Content-type': 'application/json'}
auth = (token, '')
output = requests.get(url, headers=headers, auth=auth)
if output.status_code == 200:
resp = Response(response=json.dumps(output.json()),
status=200,
mimetype="application/json")
return resp
else:
return abort(404)
else:
return abort(404)
@app.route("/", methods=['GET', 'POST'])
def event_handler():
"""Handle deployment webhooks from Github."""
if authorize(request, config):
if request.method == 'POST':
if request.headers.get('X-GitHub-Event') == 'pull_request':
if (request.json['action'] == 'closed' and
request.json['pull_request']['merged'] is True):
res = create_deployment(request.json['pull_request'],
config.TOKEN)
if res.status_code != 200 and res.status_code != 201:
print res.text
return abort(res.status_code)
else:
return "Pull Request merged!"
return "Pull Request created!"
elif request.headers.get('X-GitHub-Event') == 'deployment':
print "Process Deployment"
if process_deployment(request.json):
return "Deployment done!"
else:
return abort(500)
elif request.headers.get('X-GitHub-Event') == 'deployment_status':
print "Update Deployment Status"
communicate_deployment(request.json)
return "Update Deployment Status"
else:
return abort(501)
else:
return abort(501)
else:
return abort(403)
def run_ansible_playbook(ansible_hosts, playbook):
"""
Run Ansible like ansible-playbook command.
Similar to: ansible-playbook -i ansible_hosts playbook.yml
"""
stats = callbacks.AggregateStats()
playbook_cb = callbacks.PlaybookCallbacks(verbose=utils.VERBOSITY)
inventory = ansible.inventory.Inventory(ansible_hosts)
runner_cb = callbacks.PlaybookRunnerCallbacks(stats,
verbose=utils.VERBOSITY)
pb = ansible.playbook.PlayBook(playbook=playbook,
callbacks=playbook_cb,
runner_callbacks=runner_cb,
stats=stats, inventory=inventory)
pb.run()
hosts = sorted(pb.stats.processed.keys())
for h in hosts:
t = pb.stats.summarize(h)
if t['failures'] > 0:
msg = "[%s]:[%s] playbook failed!" % (h, playbook)
raise AnsibleError(msg)
def process_deployment(deployment):
"""Process deployment."""
try:
repo = config.REPOS.get(deployment['repository']['full_name'])
if repo:
update_deployment(deployment, status='pending')
# ansible_hosts and Playbook defined? Then run only Ansible.
if 'ansible_hosts' in repo and 'ansible_playbook' in repo:
run_ansible_playbook(repo['ansible_hosts'],
repo['ansible_playbook'])
update_deployment(deployment, status='success')
return True
else:
for command in repo['commands']:
p = Popen(command, cwd=repo['folder'], stderr=PIPE)
return_code = p.wait()
if return_code != 0:
raise CalledProcessError(return_code,
command,
output=p.communicate())
update_deployment(deployment, status='success')
return True
except KeyError as e:
message = "ansible playbook or host file is missing in config file."
update_deployment(deployment, status='error', message=message)
return False
except AnsibleError as e:
update_deployment(deployment, status='error', message=str(e))
return False
except CalledProcessError as e:
message = "command: %s ERROR: %s" % (e.cmd, e.output[1])
update_deployment(deployment, status='error', message=message)
return False
except OSError as e:
update_deployment(deployment, status='error', message=str(e))
return False
def create_deployment(pull_request, token):
"""Create a deployment."""
user = pull_request['user']['login']
repo = pull_request['head']['repo']['full_name']
payload = {'environment': 'production', 'deploy_user': user}
url = 'https://api.github.com/repos/%s/deployments' % (repo)
headers = {'Content-type': 'application/json'}
auth = (token, '')
if config.REPOS[repo].get('required_contexts'):
required_contexts = config.REPOS[repo].get('required_contexts')
else:
required_contexts = []
data = {'ref': pull_request['head']['sha'],
'payload': payload,
'auto_merge': False,
'required_contexts': required_contexts,
'description': 'mydesc'}
deployment = requests.post(url, data=json.dumps(data), headers=headers,
auth=auth)
return deployment
def update_deployment(deployment, status, message="ERROR"):
"""Update a deployment."""
token = config.TOKEN
repo = deployment['repository']['full_name']
url = 'https://api.github.com/repos/%s/deployments/%s/statuses' % (repo, deployment['deployment']['id'])
# print url
headers = {'Content-type': 'application/json'}
auth = (token, '')
if status == 'success':
msg = "The deployment has been successful."
else:
msg = message
data = {'state': status,
'target_url': 'http://example.com',
'description': msg}
r = requests.post(url, data=json.dumps(data), headers=headers, auth=auth)
if r.status_code == 200:
return True
else:
return False
# print r.text
def communicate_deployment(deployment):
"""Communicate deployment via Slack."""
repo = deployment['repository']['full_name']
repo_url = deployment['repository']['url']
status = deployment['deployment_status']['state']
status_url = url_for('.get_status', url=deployment['deployment']['url'],
_external=True)
user = deployment['deployment']['payload']['deploy_user']
msg = 'Repository <%s|%s> has been deployed by *%s* with <%s/statuses|%s>.' % (repo_url,
repo,
user,
status_url,
status)
text = {'text': msg}
headers = {'Content-type': 'application/json'}
try:
r = requests.post(config.SLACK_WEBHOOK,
data=json.dumps(text), headers=headers)
return r.text
except AttributeError:
return msg
# See http://stackoverflow.com/questions/18168819/how-to-securely-verify-an-hmac-in-python-2-7
def compare_digest(x, y): # pragma: no cover
"""Compare to hmac digests."""
if not (isinstance(x, bytes) and isinstance(y, bytes)):
raise TypeError("both inputs should be instances of bytes")
if len(x) != len(y):
return False
result = 0
result = sum(a != b for a, b in zip(x, y))
return result == 0
def authorize(request, config):
"""Authorize Github webhook."""
x_hub_signature = request.headers.get('X-Hub-Signature')
if x_hub_signature is None:
return False
try:
sha_name, signature = x_hub_signature.split('=')
except ValueError:
return False
if signature is None or signature == "":
return False
if sha_name != 'sha1':
return False
mac = hmac.new(config.SECRET, msg=request.data, digestmod=hashlib.sha1)
if compare_digest(mac.hexdigest(), bytes(signature)):
return True
else:
return False
if __name__ == "__main__": # pragma: no cover
app.debug = config.DEBUG
app.run()