SEBA-405 Convert synchronizer framework to library
Change-Id: If8562f23dc15c7d18d7a8b040b33756708b3c5ec
diff --git a/lib/xos-synchronizer/xossynchronizer/ansible_helper.py b/lib/xos-synchronizer/xossynchronizer/ansible_helper.py
new file mode 100644
index 0000000..c607607
--- /dev/null
+++ b/lib/xos-synchronizer/xossynchronizer/ansible_helper.py
@@ -0,0 +1,325 @@
+#!/usr/bin/env python
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import print_function
+import jinja2
+import tempfile
+import os
+import json
+import pickle
+import pdb
+import string
+import random
+import re
+import traceback
+import subprocess
+import threading
+
+from multiprocessing import Process, Queue
+from xosconfig import Config
+
+from multistructlog import create_logger
+
+log = create_logger(Config().get("logging"))
+
+
+step_dir = Config.get("steps_dir")
+sys_dir = Config.get("sys_dir")
+
+os_template_loader = jinja2.FileSystemLoader(
+ searchpath=[step_dir, "/opt/xos/synchronizers/shared_templates"]
+)
+os_template_env = jinja2.Environment(loader=os_template_loader)
+
+
+def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
+ return "".join(random.choice(chars) for _ in range(size))
+
+
+def shellquote(s):
+ return "'" + s.replace("'", "'\\''") + "'"
+
+
+def get_playbook_fn(opts, path):
+ if not opts.get("ansible_tag", None):
+ # if no ansible_tag is in the options, then generate a unique one
+ objname = id_generator()
+ opts = opts.copy()
+ opts["ansible_tag"] = objname
+
+ objname = opts["ansible_tag"]
+
+ pathed_sys_dir = os.path.join(sys_dir, path)
+ if not os.path.isdir(pathed_sys_dir):
+ os.makedirs(pathed_sys_dir)
+
+ # symlink steps/roles into sys/roles so that playbooks can access roles
+ roledir = os.path.join(step_dir, "roles")
+ rolelink = os.path.join(pathed_sys_dir, "roles")
+ if os.path.isdir(roledir) and not os.path.islink(rolelink):
+ os.symlink(roledir, rolelink)
+
+ return (opts, os.path.join(pathed_sys_dir, objname))
+
+
+def run_playbook(ansible_hosts, ansible_config, fqp, opts):
+ args = {
+ "ansible_hosts": ansible_hosts,
+ "ansible_config": ansible_config,
+ "fqp": fqp,
+ "opts": opts,
+ "config_file": Config.get_config_file(),
+ }
+
+ keep_temp_files = Config.get("keep_temp_files")
+
+ dir = tempfile.mkdtemp()
+ args_fn = None
+ result_fn = None
+ try:
+ log.info("creating args file", dir=dir)
+
+ args_fn = os.path.join(dir, "args")
+ result_fn = os.path.join(dir, "result")
+
+ open(args_fn, "w").write(pickle.dumps(args))
+
+ ansible_main_fn = os.path.join(os.path.dirname(__file__), "ansible_main.py")
+
+ os.system("python %s %s %s" % (ansible_main_fn, args_fn, result_fn))
+
+ result = pickle.loads(open(result_fn).read())
+
+ if hasattr(result, "exception"):
+ log.error("Exception in playbook", exception=result["exception"])
+
+ stats = result.get("stats", None)
+ aresults = result.get("aresults", None)
+ except Exception as e:
+ log.exception("Exception running ansible_main")
+ stats = None
+ aresults = None
+ finally:
+ if not keep_temp_files:
+ if args_fn and os.path.exists(args_fn):
+ os.remove(args_fn)
+ if result_fn and os.path.exists(result_fn):
+ os.remove(result_fn)
+ os.rmdir(dir)
+
+ return (stats, aresults)
+
+
+def run_template(
+ name,
+ opts,
+ path="",
+ expected_num=None,
+ ansible_config=None,
+ ansible_hosts=None,
+ run_ansible_script=None,
+ object=None,
+):
+ template = os_template_env.get_template(name)
+ buffer = template.render(opts)
+
+ (opts, fqp) = get_playbook_fn(opts, path)
+
+ f = open(fqp, "w")
+ f.write(buffer)
+ f.flush()
+
+ """
+ q = Queue()
+ p = Process(target=run_playbook, args=(ansible_hosts, ansible_config, fqp, opts, q,))
+ p.start()
+ stats,aresults = q.get()
+ p.join()
+ """
+ stats, aresults = run_playbook(ansible_hosts, ansible_config, fqp, opts)
+
+ error_msg = []
+
+ output_file = fqp + ".out"
+ try:
+ if aresults is None:
+ raise ValueError("Error executing playbook %s" % fqp)
+
+ ok_results = []
+ total_unreachable = 0
+ failed = 0
+
+ ofile = open(output_file, "w")
+
+ for x in aresults:
+ if not x.is_failed() and not x.is_unreachable() and not x.is_skipped():
+ ok_results.append(x)
+ elif x.is_unreachable():
+ failed += 1
+ total_unreachable += 1
+ try:
+ error_msg.append(x._result["msg"])
+ except BaseException:
+ pass
+ elif x.is_failed():
+ failed += 1
+ try:
+ error_msg.append(x._result["msg"])
+ except BaseException:
+ pass
+
+ # FIXME (zdw, 2017-02-19) - may not be needed with new callback logging
+
+ ofile.write("%s: %s\n" % (x._task, str(x._result)))
+
+ if object:
+ oprops = object.tologdict()
+ ansible = x._result
+ oprops["xos_type"] = "ansible"
+ oprops["ansible_result"] = json.dumps(ansible)
+
+ if failed == 0:
+ oprops["ansible_status"] = "OK"
+ else:
+ oprops["ansible_status"] = "FAILED"
+
+ log.info("Ran Ansible task", task=x._task, **oprops)
+
+ ofile.close()
+
+ if (expected_num is not None) and (len(ok_results) != expected_num):
+ raise ValueError(
+ "Unexpected num %s!=%d" % (str(expected_num), len(ok_results))
+ )
+
+ if failed:
+ raise ValueError("Ansible playbook failed.")
+
+ # NOTE(smbaker): Playbook errors are slipping through where `aresults` does not show any failed tasks, but
+ # `stats` does show them. See CORD-3169.
+ hosts = sorted(stats.processed.keys())
+ for h in hosts:
+ t = stats.summarize(h)
+ if t["unreachable"] > 0:
+ raise ValueError(
+ "Ansible playbook reported unreachable for host %s" % h
+ )
+ if t["failures"] > 0:
+ raise ValueError("Ansible playbook reported failures for host %s" % h)
+
+ except ValueError as e:
+ if error_msg:
+ try:
+ error = " // ".join(error_msg)
+ except BaseException:
+ error = "failed to join error_msg"
+ raise Exception(error)
+ else:
+ raise
+
+ processed_results = map(lambda x: x._result, ok_results)
+ return processed_results[1:] # 0 is setup
+
+
+def run_template_ssh(name, opts, path="", expected_num=None, object=None):
+ instance_name = opts["instance_name"]
+ hostname = opts["hostname"]
+ private_key = opts["private_key"]
+ baremetal_ssh = opts.get("baremetal_ssh", False)
+ if baremetal_ssh:
+ # no instance_id or ssh_ip for baremetal
+ # we never proxy to baremetal
+ proxy_ssh = False
+ else:
+ instance_id = opts["instance_id"]
+ ssh_ip = opts["ssh_ip"]
+ proxy_ssh = Config.get("proxy_ssh.enabled")
+
+ if not ssh_ip:
+ raise Exception("IP of ssh proxy not available. Synchronization deferred")
+
+ (opts, fqp) = get_playbook_fn(opts, path)
+ private_key_pathname = fqp + ".key"
+ config_pathname = fqp + ".cfg"
+ hosts_pathname = fqp + ".hosts"
+
+ f = open(private_key_pathname, "w")
+ f.write(private_key)
+ f.close()
+
+ f = open(config_pathname, "w")
+ f.write("[ssh_connection]\n")
+ if proxy_ssh:
+ proxy_ssh_key = Config.get("proxy_ssh.key")
+ proxy_ssh_user = Config.get("proxy_ssh.user")
+ if proxy_ssh_key:
+ # If proxy_ssh_key is known, then we can proxy into the compute
+ # node without needing to have the OpenCloud sshd machinery in
+ # place.
+ proxy_command = (
+ "ProxyCommand ssh -q -i %s -o StrictHostKeyChecking=no %s@%s nc %s 22"
+ % (proxy_ssh_key, proxy_ssh_user, hostname, ssh_ip)
+ )
+ else:
+ proxy_command = (
+ "ProxyCommand ssh -q -i %s -o StrictHostKeyChecking=no %s@%s"
+ % (private_key_pathname, instance_id, hostname)
+ )
+ f.write('ssh_args = -o "%s"\n' % proxy_command)
+ f.write("scp_if_ssh = True\n")
+ f.write("pipelining = True\n")
+ f.write("\n[defaults]\n")
+ f.write("host_key_checking = False\n")
+ f.write("timeout = 30\n")
+ f.close()
+
+ f = open(hosts_pathname, "w")
+ f.write("[%s]\n" % instance_name)
+ f.write("%s ansible_ssh_private_key_file=%s\n" % (ssh_ip, private_key_pathname))
+ f.close()
+
+ # SSH will complain if private key is world or group readable
+ os.chmod(private_key_pathname, 0o600)
+
+ print("ANSIBLE_CONFIG=%s" % config_pathname)
+ print("ANSIBLE_HOSTS=%s" % hosts_pathname)
+
+ return run_template(
+ name,
+ opts,
+ path,
+ ansible_config=config_pathname,
+ ansible_hosts=hosts_pathname,
+ run_ansible_script="/opt/xos/synchronizers/base/run_ansible_verbose",
+ object=object,
+ )
+
+
+def main():
+ run_template(
+ "ansible/sync_user_deployments.yaml",
+ {
+ "endpoint": "http://172.31.38.128:5000/v2.0/",
+ "name": "Sapan Bhatia",
+ "email": "gwsapan@gmail.com",
+ "password": "foobar",
+ "admin_user": "admin",
+ "admin_password": "6a789bf69dd647e2",
+ "admin_tenant": "admin",
+ "tenant": "demo",
+ "roles": ["user", "admin"],
+ },
+ )