Skip to content

Commit 1cdfe2a

Browse files
author
mfallone
committed
New branch Ansible. Added psi_ansible which can be run at the command line specifying a playbook.
Email stats can be configured and sent --HG-- branch : ansible
1 parent 381ab44 commit 1cdfe2a

File tree

3 files changed

+370
-0
lines changed

3 files changed

+370
-0
lines changed

Automation/ansible/.empty

Whitespace-only changes.

Automation/psi_ansible.py

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
#!/usr/bin/python
2+
3+
import optparse
4+
import ansible.runner
5+
import ansible.playbook
6+
import os
7+
import sys
8+
import datetime
9+
import psi_ops
10+
import pynliner
11+
12+
import psi_ops_config
13+
14+
PSI_OPS_DB_FILENAME = os.path.join(os.path.abspath('.'), 'psi_ops.dat')
15+
16+
from mako.template import Template
17+
from mako.lookup import TemplateLookup
18+
from mako import exceptions
19+
20+
# Using the FeedbackDecryptor's mail capabilities
21+
sys.path.append(os.path.abspath(os.path.join('..', 'EmailResponder')))
22+
sys.path.append(os.path.abspath(os.path.join('..', 'EmailResponder', 'FeedbackDecryptor')))
23+
import sender
24+
from config import config
25+
26+
def prepare_linode_base_host(host):
27+
ansible_base_linode = create_host(host_name='linode_base_image',
28+
host_vars={'ansible_ssh_host': host.base_ip_address,
29+
'ansible_ssh_user': 'root',
30+
'ansible_ssh_pass': host.base_root_password,
31+
'ansible_ssh_port': host.base_ssh_port,
32+
})
33+
return ansible_base_linode
34+
35+
def create_host(host_name=None, host_vars=dict()):
36+
"""
37+
Create a new host object and return it.
38+
host_name: String containing IP/name of server
39+
host_vars: Variables that are set against the host.
40+
"""
41+
try:
42+
# Create a new host entry and set variables
43+
if isinstance(host_name, basestring):
44+
host = ansible.inventory.host.Host(host_name)
45+
46+
for k,v in host_vars.iteritems():
47+
host.set_variable(k,v)
48+
49+
except Exception as e:
50+
print type(e), str(e)
51+
raise e
52+
53+
return host
54+
55+
def add_hosts_to_group(hosts, group):
56+
"""
57+
Add a single or list of Ansible host objects to an Ansible group
58+
hosts = ansible.inventory.Host
59+
group = ansible.inventory.group.Group
60+
"""
61+
try:
62+
if type(hosts) is ansible.inventory.Host:
63+
# probably means we only have one host
64+
group.add_host(hosts)
65+
elif isinstance(hosts, list):
66+
for host in hosts:
67+
group.add_host(host)
68+
69+
except Exception as e:
70+
print type(e), str(e)
71+
raise e
72+
73+
def run_against_inventory(inv=ansible.inventory.Inventory([]), mod_name='ping', mod_args='', pattern='*', forks=10):
74+
"""
75+
Run a single task against an Inventory.
76+
inv : Ansible Inventory object
77+
mod_name : module name
78+
mod_args : extra arguments for the module
79+
pattern : hosts or groups to match against
80+
forks : number of forks for the runner to create (default = 10)
81+
"""
82+
try:
83+
# create a runnable task and execute
84+
runner = ansible.runner.Runner(
85+
module_name=mod_name,
86+
module_args=mod_args,
87+
pattern=pattern,
88+
forks=forks,
89+
inventory=inv,
90+
)
91+
92+
return runner.run()
93+
94+
except Exception as e:
95+
raise e
96+
97+
def organize_hosts_by_provider(hosts_list):
98+
"""
99+
Takes a list of psinet hosts and organizes into provider dictionary objects.
100+
hosts_list : list of psinet hosts
101+
"""
102+
hosts_dict = dict()
103+
try:
104+
all_hosts = hosts_list
105+
for host in all_hosts:
106+
if host.provider not in hosts_dict.keys():
107+
hosts_dict[host.provider] = list()
108+
109+
hosts_dict[host.provider].append(host)
110+
111+
except Exception as e:
112+
raise e
113+
114+
return hosts_dict
115+
116+
def populate_ansible_hosts(hosts=list()):
117+
"""
118+
Maps a list of psinet hosts into Ansible Hosts
119+
hosts : list of psinet hosts
120+
"""
121+
ansible_hosts = list()
122+
try:
123+
for host in hosts:
124+
ansible_hosts.append(create_host(host_name=host.id,
125+
host_vars={'ansible_ssh_host': host.ip_address,
126+
'ansible_ssh_user': host.ssh_username,
127+
'ansible_ssh_pass': host.ssh_password,
128+
'ansible_ssh_port': host.ssh_port,
129+
}))
130+
131+
except Exception as e:
132+
raise e
133+
134+
return ansible_hosts
135+
136+
def run_playbook(playbook_file, inventory, verbose=psi_ops_config.ANSIBLE_VERBOSE_LEVEL, email_stats=True):
137+
"""
138+
Runs a playbook file and returns the result
139+
playbook_file : Playbook file to open and run (String)
140+
inventory : Ansible inventory to run playbook against
141+
verbose : Output verbosity
142+
"""
143+
try:
144+
start_time = datetime.datetime.now()
145+
playbook_callbacks = ansible.callbacks.PlaybookCallbacks(verbose=verbose)
146+
stats = ansible.callbacks.AggregateStats()
147+
runner_callbacks = ansible.callbacks.PlaybookRunnerCallbacks(stats, verbose=verbose)
148+
149+
playbook = ansible.playbook.PlayBook(playbook=playbook_file,
150+
callbacks=playbook_callbacks, runner_callbacks=runner_callbacks,
151+
stats=stats, inventory=inventory)
152+
153+
res = playbook.run()
154+
end_time = datetime.datetime.now()
155+
print "Run completed at: %s\nTotal run time: %s" % (str(end_time), str(end_time-start_time))
156+
157+
if email_stats == True:
158+
# stats.dark : (dict) number of hosts that could not be contacted
159+
# stats.failures : (dict) number of hosts that failed to complete the tasks
160+
record = (str(start_time), str(end_time), playbook_file, stats.processed, stats.dark, stats.failures, stats.changed, stats.skipped, res)
161+
send_mail(record)
162+
163+
except Exception as e:
164+
raise e
165+
166+
def send_mail(record, subject='PSI ANSIBLE'):
167+
template_filename = 'psi_mail_ansible_stats.mako'
168+
template_lookup = TemplateLookup(directories=[os.path.dirname(os.path.abspath('__file__'))])
169+
template = Template(filename=template_filename, default_filters=['unicode', 'h'], lookup=template_lookup)
170+
171+
try:
172+
rendered = template.render(data=record)
173+
except:
174+
raise Exception(exceptions.text_error_template().render())
175+
176+
# CSS in email HTML must be inline
177+
rendered = pynliner.fromString(rendered)
178+
179+
sender.send(config['emailRecipients'], config['emailUsername'], subject, repr(record), rendered)
180+
181+
def refresh_base_images():
182+
try:
183+
psinet = psi_ops.PsiphonNetwork.load_from_file(PSI_OPS_DB_FILENAME)
184+
185+
except Exception as e:
186+
raise
187+
188+
def main(infile=None, send_mail_stats=False):
189+
try:
190+
psinet = psi_ops.PsiphonNetwork.load_from_file(PSI_OPS_DB_FILENAME)
191+
psinet_hosts_list = psinet.get_hosts()
192+
193+
inv = ansible.inventory.Inventory([])
194+
195+
#Run against subset
196+
if psi_ops_config.RUN_AGAINST_SUBSET == True:
197+
print 'Running Playbook against subset'
198+
subset_hosts_list = psi_ops_config.ANSIBLE_TEST_DIGITALOCEAN + psi_ops_config.ANSIBLE_TEST_LINODES + psi_ops_config.ANSIBLE_TEST_FASTHOSTS
199+
psinet_hosts_list = [h for h in psinet_hosts_list if h.id in subset_hosts_list]
200+
201+
psinet_hosts_dict = organize_hosts_by_provider(psinet_hosts_list)
202+
203+
for provider in psinet_hosts_dict:
204+
group = ansible.inventory.Group(provider)
205+
ansible_hosts_list = populate_ansible_hosts(psinet_hosts_dict[provider])
206+
add_hosts_to_group(ansible_hosts_list, group)
207+
inv.add_group(group)
208+
209+
# Add test group if set
210+
if psi_ops_config.ANSIBLE_INCLUDE_TEST_GROUP == True:
211+
print "Creating Test Group"
212+
test_hosts_list = list()
213+
for h in psinet_hosts_list:
214+
if h.id in psi_ops_config.ANSIBLE_TEST_HOSTS:
215+
test_hosts_list.append(h)
216+
217+
ansible_hosts_list = populate_ansible_hosts(test_hosts_list)
218+
group = ansible.inventory.Group(psi_ops_config.ANSIBLE_TEST_GROUP)
219+
add_hosts_to_group(ansible_hosts_list, group)
220+
inv.add_group(group)
221+
222+
# Add linode base image group
223+
if psi_ops_config.ANSIBLE_INCLUDE_BASE_IMAGE == True:
224+
print "Creating Linode Base Image Group"
225+
linode_base_host = prepare_linode_base_host(psinet._PsiphonNetwork__linode_account)
226+
group = ansible.inventory.Group('linode_base_image')
227+
add_hosts_to_group(linode_base_host, group)
228+
inv.add_group(group)
229+
230+
if not infile:
231+
raise "Must specify input file"
232+
233+
playbook_file = infile
234+
res = run_playbook(playbook_file, inv, send_mail_stats)
235+
print res
236+
237+
except Exception as e:
238+
raise type(e), str(e)
239+
240+
241+
if __name__ == "__main__":
242+
parser = optparse.OptionParser('usage: %prog [options]')
243+
parser.add_option("-i", "--infile", help="Specify ansible playbook file")
244+
parser.add_option("-t", "--test_servers", action="store_true", help="Runs playbook against test systems")
245+
parser.add_option("-s", "--subset", action="store_true", help="Run against a subset of servers")
246+
parser.add_option("-b", "--base_image", action="store_true", help="Forces base image to be included")
247+
parser.add_option("-r", "--refresh_base_images", action="store_true", help="Updates base images for linode and digitalocean")
248+
parser.add_option("-m", "--send_mail", action="store_true", help="Send email after playbook is run")
249+
250+
251+
infile=None
252+
send_mail_stats = False
253+
254+
(options, _) = parser.parse_args()
255+
if options.infile:
256+
infile = options.infile
257+
print infile
258+
if options.send_mail:
259+
send_mail_stats=True
260+
if options.test_servers:
261+
psi_ops_config.ANSIBLE_INCLUDE_TEST_GROUP = True
262+
if options.subset:
263+
psi_ops_config.RUN_AGAINST_SUBSET = True
264+
if options.base_image:
265+
psi_ops_config.ANSIBLE_INCLUDE_BASE_IMAGE = True
266+
if options.refresh_base_images:
267+
refresh_base_images()
268+
269+
main(infile=infile, send_mail_stats=send_mail_stats)
270+
271+
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
## Copyright (c) 2014, Psiphon Inc.
2+
## All rights reserved.
3+
##
4+
## This program is free software: you can redistribute it and/or modify
5+
## it under the terms of the GNU General Public License as published by
6+
## the Free Software Foundation, either version 3 of the License, or
7+
## (at your option) any later version.
8+
##
9+
## This program is distributed in the hope that it will be useful,
10+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
## GNU General Public License for more details.
13+
##
14+
## You should have received a copy of the GNU General Public License
15+
## along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
<h1>Psiphon 3 Ansible Stats</h1>
18+
<%
19+
start_time, end_time, playbook_file, hosts_processed, hosts_dark, hosts_failed, hosts_changed, hosts_skipped, hosts_summary = data
20+
21+
import datetime
22+
import operator
23+
24+
elapsed_time = datetime.datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S.%f") - datetime.datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S.%f")
25+
26+
count_processed = len(hosts_processed)
27+
count_unreachable = len(hosts_dark)
28+
count_failed = len(hosts_failed)
29+
count_changed = len(hosts_changed)
30+
count_skipped = len(hosts_skipped)
31+
32+
%>
33+
34+
<h2>Playbook: ${playbook_file}</h2>
35+
36+
<h3>Host Stats</h3>
37+
<ul>
38+
<li>Unreachable: ${count_unreachable}</li>
39+
<li>Processed: ${count_processed} </li>
40+
<li>Failed: ${count_failed}</li>
41+
<li>Changed: ${count_changed}</li>
42+
<li>Skipped: ${count_skipped}</li>
43+
</ul>
44+
45+
<hr>
46+
47+
% if count_unreachable > 0:
48+
<h3>Unreachable Hosts</h3>
49+
<tbody>
50+
% for c in hosts_dark:
51+
<tr>${c}</tr>
52+
% endfor
53+
</tbody>
54+
% endif
55+
56+
% if count_failed > 0:
57+
<h3>Failed Hosts</h3>
58+
<tbody>
59+
% for c in hosts_failed:
60+
<tr>${c}</tr>
61+
% endfor
62+
</tbody>
63+
% endif
64+
65+
% if count_processed > 0:
66+
<h3>Procssed Hosts</h3>
67+
<tbody>
68+
% for c in hosts_processed:
69+
<tr>${c}</tr>
70+
% endfor
71+
</tbody>
72+
% endif
73+
74+
% if count_skipped > 0:
75+
<h3>Skipped Hosts</h3>
76+
<tbody>
77+
% for c in hosts_skipped:
78+
<tr>${c}</tr>
79+
% endfor
80+
</tbody>
81+
% endif
82+
83+
% if count_changed > 0:
84+
<h3>Changed Hosts</h3>
85+
<tbody>
86+
% for c in hosts_changed:
87+
<tr>${c}</tr>
88+
% endfor
89+
</tbody>
90+
% endif
91+
92+
<hr>
93+
<p>
94+
<sub>
95+
Start Time: ${start_time}
96+
End Time: ${end_time}
97+
Elapsed: ${elapsed_time}
98+
</sub>
99+
</p>

0 commit comments

Comments
 (0)