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"],
+        },
+    )