blob: 846aeb92e839c86b4d9f15fb3d0ae094757a4d85 [file] [log] [blame]
Scott Bakerbba67b62019-01-28 17:38:21 -08001#!/usr/bin/env python
2
3# Copyright 2017-present Open Networking Foundation
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
Zack Williams5c2ea232019-01-30 15:23:01 -070017from __future__ import absolute_import, print_function
Scott Bakerbba67b62019-01-28 17:38:21 -080018
Zack Williams5c2ea232019-01-30 15:23:01 -070019import json
20import os
21import pickle
22import random
23import string
24import tempfile
25
26import jinja2
Scott Bakerbba67b62019-01-28 17:38:21 -080027
28from multistructlog import create_logger
Zack Williams5c2ea232019-01-30 15:23:01 -070029from xosconfig import Config
30from six.moves import range
Scott Bakerbba67b62019-01-28 17:38:21 -080031
32log = create_logger(Config().get("logging"))
33
34
35step_dir = Config.get("steps_dir")
36sys_dir = Config.get("sys_dir")
37
38os_template_loader = jinja2.FileSystemLoader(
39 searchpath=[step_dir, "/opt/xos/synchronizers/shared_templates"]
40)
41os_template_env = jinja2.Environment(loader=os_template_loader)
42
43
44def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
45 return "".join(random.choice(chars) for _ in range(size))
46
47
48def shellquote(s):
49 return "'" + s.replace("'", "'\\''") + "'"
50
51
52def get_playbook_fn(opts, path):
53 if not opts.get("ansible_tag", None):
54 # if no ansible_tag is in the options, then generate a unique one
55 objname = id_generator()
56 opts = opts.copy()
57 opts["ansible_tag"] = objname
58
59 objname = opts["ansible_tag"]
60
61 pathed_sys_dir = os.path.join(sys_dir, path)
62 if not os.path.isdir(pathed_sys_dir):
63 os.makedirs(pathed_sys_dir)
64
65 # symlink steps/roles into sys/roles so that playbooks can access roles
66 roledir = os.path.join(step_dir, "roles")
67 rolelink = os.path.join(pathed_sys_dir, "roles")
68 if os.path.isdir(roledir) and not os.path.islink(rolelink):
69 os.symlink(roledir, rolelink)
70
71 return (opts, os.path.join(pathed_sys_dir, objname))
72
73
74def run_playbook(ansible_hosts, ansible_config, fqp, opts):
75 args = {
76 "ansible_hosts": ansible_hosts,
77 "ansible_config": ansible_config,
78 "fqp": fqp,
79 "opts": opts,
80 "config_file": Config.get_config_file(),
81 }
82
83 keep_temp_files = Config.get("keep_temp_files")
84
85 dir = tempfile.mkdtemp()
86 args_fn = None
87 result_fn = None
88 try:
89 log.info("creating args file", dir=dir)
90
91 args_fn = os.path.join(dir, "args")
92 result_fn = os.path.join(dir, "result")
93
94 open(args_fn, "w").write(pickle.dumps(args))
95
96 ansible_main_fn = os.path.join(os.path.dirname(__file__), "ansible_main.py")
97
98 os.system("python %s %s %s" % (ansible_main_fn, args_fn, result_fn))
99
100 result = pickle.loads(open(result_fn).read())
101
102 if hasattr(result, "exception"):
103 log.error("Exception in playbook", exception=result["exception"])
104
105 stats = result.get("stats", None)
106 aresults = result.get("aresults", None)
Zack Williams5c2ea232019-01-30 15:23:01 -0700107 except BaseException:
Scott Bakerbba67b62019-01-28 17:38:21 -0800108 log.exception("Exception running ansible_main")
109 stats = None
110 aresults = None
111 finally:
112 if not keep_temp_files:
113 if args_fn and os.path.exists(args_fn):
114 os.remove(args_fn)
115 if result_fn and os.path.exists(result_fn):
116 os.remove(result_fn)
117 os.rmdir(dir)
118
119 return (stats, aresults)
120
121
122def run_template(
123 name,
124 opts,
125 path="",
126 expected_num=None,
127 ansible_config=None,
128 ansible_hosts=None,
129 run_ansible_script=None,
130 object=None,
131):
132 template = os_template_env.get_template(name)
133 buffer = template.render(opts)
134
135 (opts, fqp) = get_playbook_fn(opts, path)
136
137 f = open(fqp, "w")
138 f.write(buffer)
139 f.flush()
140
141 """
142 q = Queue()
143 p = Process(target=run_playbook, args=(ansible_hosts, ansible_config, fqp, opts, q,))
144 p.start()
145 stats,aresults = q.get()
146 p.join()
147 """
148 stats, aresults = run_playbook(ansible_hosts, ansible_config, fqp, opts)
149
150 error_msg = []
151
152 output_file = fqp + ".out"
153 try:
154 if aresults is None:
155 raise ValueError("Error executing playbook %s" % fqp)
156
157 ok_results = []
158 total_unreachable = 0
159 failed = 0
160
161 ofile = open(output_file, "w")
162
163 for x in aresults:
164 if not x.is_failed() and not x.is_unreachable() and not x.is_skipped():
165 ok_results.append(x)
166 elif x.is_unreachable():
167 failed += 1
168 total_unreachable += 1
169 try:
170 error_msg.append(x._result["msg"])
171 except BaseException:
172 pass
173 elif x.is_failed():
174 failed += 1
175 try:
176 error_msg.append(x._result["msg"])
177 except BaseException:
178 pass
179
180 # FIXME (zdw, 2017-02-19) - may not be needed with new callback logging
181
182 ofile.write("%s: %s\n" % (x._task, str(x._result)))
183
184 if object:
185 oprops = object.tologdict()
186 ansible = x._result
187 oprops["xos_type"] = "ansible"
188 oprops["ansible_result"] = json.dumps(ansible)
189
190 if failed == 0:
191 oprops["ansible_status"] = "OK"
192 else:
193 oprops["ansible_status"] = "FAILED"
194
195 log.info("Ran Ansible task", task=x._task, **oprops)
196
197 ofile.close()
198
199 if (expected_num is not None) and (len(ok_results) != expected_num):
200 raise ValueError(
201 "Unexpected num %s!=%d" % (str(expected_num), len(ok_results))
202 )
203
204 if failed:
205 raise ValueError("Ansible playbook failed.")
206
207 # NOTE(smbaker): Playbook errors are slipping through where `aresults` does not show any failed tasks, but
208 # `stats` does show them. See CORD-3169.
209 hosts = sorted(stats.processed.keys())
210 for h in hosts:
211 t = stats.summarize(h)
212 if t["unreachable"] > 0:
213 raise ValueError(
214 "Ansible playbook reported unreachable for host %s" % h
215 )
216 if t["failures"] > 0:
217 raise ValueError("Ansible playbook reported failures for host %s" % h)
218
Zack Williams5c2ea232019-01-30 15:23:01 -0700219 except ValueError:
Scott Bakerbba67b62019-01-28 17:38:21 -0800220 if error_msg:
221 try:
222 error = " // ".join(error_msg)
223 except BaseException:
224 error = "failed to join error_msg"
225 raise Exception(error)
226 else:
227 raise
228
Zack Williams5c2ea232019-01-30 15:23:01 -0700229 processed_results = [x._result for x in ok_results]
Scott Bakerbba67b62019-01-28 17:38:21 -0800230 return processed_results[1:] # 0 is setup
231
232
233def run_template_ssh(name, opts, path="", expected_num=None, object=None):
234 instance_name = opts["instance_name"]
235 hostname = opts["hostname"]
236 private_key = opts["private_key"]
237 baremetal_ssh = opts.get("baremetal_ssh", False)
238 if baremetal_ssh:
239 # no instance_id or ssh_ip for baremetal
240 # we never proxy to baremetal
241 proxy_ssh = False
242 else:
243 instance_id = opts["instance_id"]
244 ssh_ip = opts["ssh_ip"]
245 proxy_ssh = Config.get("proxy_ssh.enabled")
246
247 if not ssh_ip:
248 raise Exception("IP of ssh proxy not available. Synchronization deferred")
249
250 (opts, fqp) = get_playbook_fn(opts, path)
251 private_key_pathname = fqp + ".key"
252 config_pathname = fqp + ".cfg"
253 hosts_pathname = fqp + ".hosts"
254
255 f = open(private_key_pathname, "w")
256 f.write(private_key)
257 f.close()
258
259 f = open(config_pathname, "w")
260 f.write("[ssh_connection]\n")
261 if proxy_ssh:
262 proxy_ssh_key = Config.get("proxy_ssh.key")
263 proxy_ssh_user = Config.get("proxy_ssh.user")
264 if proxy_ssh_key:
265 # If proxy_ssh_key is known, then we can proxy into the compute
266 # node without needing to have the OpenCloud sshd machinery in
267 # place.
268 proxy_command = (
269 "ProxyCommand ssh -q -i %s -o StrictHostKeyChecking=no %s@%s nc %s 22"
270 % (proxy_ssh_key, proxy_ssh_user, hostname, ssh_ip)
271 )
272 else:
273 proxy_command = (
274 "ProxyCommand ssh -q -i %s -o StrictHostKeyChecking=no %s@%s"
275 % (private_key_pathname, instance_id, hostname)
276 )
277 f.write('ssh_args = -o "%s"\n' % proxy_command)
278 f.write("scp_if_ssh = True\n")
279 f.write("pipelining = True\n")
280 f.write("\n[defaults]\n")
281 f.write("host_key_checking = False\n")
282 f.write("timeout = 30\n")
283 f.close()
284
285 f = open(hosts_pathname, "w")
286 f.write("[%s]\n" % instance_name)
287 f.write("%s ansible_ssh_private_key_file=%s\n" % (ssh_ip, private_key_pathname))
288 f.close()
289
290 # SSH will complain if private key is world or group readable
291 os.chmod(private_key_pathname, 0o600)
292
293 print("ANSIBLE_CONFIG=%s" % config_pathname)
294 print("ANSIBLE_HOSTS=%s" % hosts_pathname)
295
296 return run_template(
297 name,
298 opts,
299 path,
300 ansible_config=config_pathname,
301 ansible_hosts=hosts_pathname,
302 run_ansible_script="/opt/xos/synchronizers/base/run_ansible_verbose",
303 object=object,
304 )
305
306
307def main():
308 run_template(
309 "ansible/sync_user_deployments.yaml",
310 {
311 "endpoint": "http://172.31.38.128:5000/v2.0/",
312 "name": "Sapan Bhatia",
313 "email": "gwsapan@gmail.com",
314 "password": "foobar",
315 "admin_user": "admin",
316 "admin_password": "6a789bf69dd647e2",
317 "admin_tenant": "admin",
318 "tenant": "demo",
319 "roles": ["user", "admin"],
320 },
321 )