Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | import jinja2 |
| 3 | import tempfile |
| 4 | import os |
| 5 | import json |
| 6 | import pdb |
| 7 | import string |
| 8 | import random |
| 9 | import re |
| 10 | import traceback |
| 11 | import subprocess |
| 12 | from xos.config import Config, XOS_DIR |
Scott Baker | 3586c55 | 2016-01-14 15:30:20 -0800 | [diff] [blame] | 13 | from xos.logger import observer_logger |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 14 | |
| 15 | try: |
| 16 | step_dir = Config().observer_steps_dir |
| 17 | sys_dir = Config().observer_sys_dir |
| 18 | except: |
Sapan Bhatia | f444188 | 2016-01-18 09:19:42 -0500 | [diff] [blame] | 19 | step_dir = XOS_DIR + '/synchronizers/openstack/steps' |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 20 | sys_dir = '/opt/opencloud' |
| 21 | |
| 22 | os_template_loader = jinja2.FileSystemLoader( searchpath=step_dir) |
| 23 | os_template_env = jinja2.Environment(loader=os_template_loader) |
| 24 | |
| 25 | def parse_output(msg): |
| 26 | lines = msg.splitlines() |
| 27 | results = [] |
| 28 | |
| 29 | observer_logger.info(msg) |
| 30 | |
| 31 | for l in lines: |
| 32 | magic_str = 'ok: [127.0.0.1] => ' |
| 33 | magic_str2 = 'changed: [127.0.0.1] => ' |
| 34 | if (l.startswith(magic_str)): |
| 35 | w = len(magic_str) |
| 36 | str = l[w:] |
Scott Baker | b014346 | 2016-02-10 14:31:24 -0800 | [diff] [blame] | 37 | |
| 38 | # handle ok: [127.0.0.1] => (item=org.onosproject.driver) => {... |
| 39 | if str.startswith("(") and (" => {" in str): |
| 40 | str = str.split("=> ",1)[1] |
| 41 | |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 42 | d = json.loads(str) |
| 43 | results.append(d) |
| 44 | elif (l.startswith(magic_str2)): |
| 45 | w = len(magic_str2) |
| 46 | str = l[w:] |
Scott Baker | b014346 | 2016-02-10 14:31:24 -0800 | [diff] [blame] | 47 | |
| 48 | if str.startswith("(") and (" => {" in str): |
| 49 | str = str.split("=> ",1)[1] |
| 50 | |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 51 | d = json.loads(str) |
| 52 | results.append(d) |
| 53 | |
| 54 | |
| 55 | return results |
| 56 | |
| 57 | def parse_unreachable(msg): |
| 58 | total_unreachable=0 |
| 59 | total_failed=0 |
| 60 | for l in msg.splitlines(): |
| 61 | x = re.findall('ok=([0-9]+).*changed=([0-9]+).*unreachable=([0-9]+).*failed=([0-9]+)', l) |
| 62 | if x: |
| 63 | (ok, changed, unreachable, failed) = x[0] |
| 64 | ok=int(ok) |
| 65 | changed=int(changed) |
| 66 | unreachable=int(unreachable) |
| 67 | failed=int(failed) |
| 68 | |
| 69 | total_unreachable += unreachable |
| 70 | total_failed += failed |
| 71 | return {'unreachable':total_unreachable,'failed':total_failed} |
Zack Williams | fcaa9b2 | 2016-03-07 20:30:14 -0700 | [diff] [blame] | 72 | |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 73 | |
| 74 | def id_generator(size=6, chars=string.ascii_uppercase + string.digits): |
| 75 | return ''.join(random.choice(chars) for _ in range(size)) |
| 76 | |
| 77 | def shellquote(s): |
| 78 | return "'" + s.replace("'", "'\\''") + "'" |
| 79 | |
| 80 | def get_playbook_fn(opts, path): |
| 81 | if not opts.get("ansible_tag", None): |
| 82 | # if no ansible_tag is in the options, then generate a unique one |
| 83 | objname= id_generator() |
| 84 | opts = opts.copy() |
| 85 | opts["ansible_tag"] = objname |
| 86 | |
| 87 | objname = opts["ansible_tag"] |
| 88 | |
Zack Williams | fcaa9b2 | 2016-03-07 20:30:14 -0700 | [diff] [blame] | 89 | pathed_sys_dir = os.path.join(sys_dir, path) |
| 90 | if not os.path.isdir(pathed_sys_dir): |
| 91 | os.makedirs(pathed_sys_dir) |
| 92 | |
| 93 | # symlink steps/roles into sys/roles so that playbooks can access roles |
| 94 | roledir = os.path.join(step_dir,"roles") |
| 95 | rolelink = os.path.join(pathed_sys_dir, "roles") |
| 96 | if os.path.isdir(roledir) and not os.path.islink(rolelink): |
| 97 | os.symlink(roledir,rolelink) |
| 98 | |
| 99 | return (opts, os.path.join(pathed_sys_dir,objname)) |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 100 | |
| 101 | def run_template(name, opts, path='', expected_num=None, ansible_config=None, ansible_hosts=None, run_ansible_script=None): |
| 102 | template = os_template_env.get_template(name) |
| 103 | buffer = template.render(opts) |
| 104 | |
| 105 | (opts, fqp) = get_playbook_fn(opts, path) |
| 106 | |
| 107 | f = open(fqp,'w') |
| 108 | f.write(buffer) |
| 109 | f.flush() |
| 110 | |
| 111 | # This is messy -- there's no way to specify ansible config file from |
| 112 | # the command line, but we can specify it using the environment. |
| 113 | env = os.environ.copy() |
| 114 | if ansible_config: |
| 115 | env["ANSIBLE_CONFIG"] = ansible_config |
| 116 | if ansible_hosts: |
| 117 | env["ANSIBLE_HOSTS"] = ansible_hosts |
| 118 | |
| 119 | if (not Config().observer_pretend): |
| 120 | if not run_ansible_script: |
Scott Baker | 698626c | 2016-01-20 15:57:30 -0800 | [diff] [blame] | 121 | run_ansible_script = os.path.join(XOS_DIR, "synchronizers/base/run_ansible") |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 122 | |
| 123 | process = subprocess.Popen("%s %s" % (run_ansible_script, shellquote(fqp)), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) |
| 124 | msg = process.stdout.read() |
| 125 | err_msg = process.stderr.read() |
| 126 | |
| 127 | if getattr(Config(), "observer_save_ansible_output", False): |
| 128 | try: |
| 129 | open(fqp+".out","w").write(msg) |
| 130 | open(fqp+".err","w").write(err_msg) |
| 131 | except: |
| 132 | # fail silently |
| 133 | pass |
| 134 | |
| 135 | else: |
| 136 | msg = open(fqp+'.out').read() |
| 137 | |
| 138 | try: |
| 139 | ok_results = parse_output(msg) |
| 140 | if (expected_num is not None) and (len(ok_results) != expected_num): |
| 141 | raise ValueError('Unexpected num %s!=%d' % (str(expected_num), len(ok_results)) ) |
| 142 | |
| 143 | parsed = parse_unreachable(msg) |
| 144 | total_unreachable = parsed['unreachable'] |
| 145 | failed = parsed['failed'] |
| 146 | if (failed): |
| 147 | raise ValueError('Ansible playbook failed.') |
| 148 | |
| 149 | if (total_unreachable > 0): |
| 150 | raise ValueError("Unreachable results in ansible recipe") |
| 151 | except ValueError,e: |
| 152 | all_fatal = [e.message] + re.findall(r'^msg: (.*)',msg,re.MULTILINE) |
| 153 | all_fatal2 = re.findall(r'^ERROR: (.*)',msg,re.MULTILINE) |
| 154 | |
| 155 | all_fatal.extend(all_fatal2) |
| 156 | try: |
| 157 | error = ' // '.join(all_fatal) |
| 158 | except: |
| 159 | pass |
| 160 | raise Exception(error) |
| 161 | |
| 162 | return ok_results |
| 163 | |
| 164 | def run_template_ssh(name, opts, path='', expected_num=None): |
| 165 | instance_name = opts["instance_name"] |
| 166 | hostname = opts["hostname"] |
| 167 | private_key = opts["private_key"] |
| 168 | baremetal_ssh = opts.get("baremetal_ssh",False) |
| 169 | if baremetal_ssh: |
Scott Baker | ac78543 | 2016-02-10 15:25:08 -0800 | [diff] [blame] | 170 | # no instance_id or ssh_ip for baremetal |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 171 | # we never proxy to baremetal |
| 172 | proxy_ssh = False |
| 173 | else: |
| 174 | instance_id = opts["instance_id"] |
Scott Baker | ac78543 | 2016-02-10 15:25:08 -0800 | [diff] [blame] | 175 | ssh_ip = opts["ssh_ip"] |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 176 | try: |
| 177 | proxy_ssh = Config().observer_proxy_ssh |
| 178 | except: |
| 179 | proxy_ssh = True |
| 180 | |
| 181 | (opts, fqp) = get_playbook_fn(opts, path) |
| 182 | private_key_pathname = fqp + ".key" |
| 183 | config_pathname = fqp + ".config" |
| 184 | hosts_pathname = fqp + ".hosts" |
| 185 | |
| 186 | f = open(private_key_pathname, "w") |
| 187 | f.write(private_key) |
| 188 | f.close() |
| 189 | |
| 190 | f = open(config_pathname, "w") |
| 191 | f.write("[ssh_connection]\n") |
| 192 | if proxy_ssh: |
Scott Baker | dce38de | 2016-02-10 17:25:07 -0800 | [diff] [blame] | 193 | proxy_ssh_key = getattr(Config(), "observer_proxy_ssh_key", None) |
| 194 | proxy_ssh_user = getattr(Config(), "observer_proxy_ssh_user", "root") |
| 195 | if proxy_ssh_key: |
| 196 | # If proxy_ssh_key is known, then we can proxy into the compute |
| 197 | # node without needing to have the OpenCloud sshd machinery in |
| 198 | # place. |
| 199 | proxy_command = "ProxyCommand ssh -q -i %s -o StrictHostKeyChecking=no %s@%s nc %s 22" % (proxy_ssh_key, proxy_ssh_user, hostname, ssh_ip) |
| 200 | else: |
| 201 | proxy_command = "ProxyCommand ssh -q -i %s -o StrictHostKeyChecking=no %s@%s" % (private_key_pathname, instance_id, hostname) |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 202 | f.write('ssh_args = -o "%s"\n' % proxy_command) |
| 203 | f.write('scp_if_ssh = True\n') |
| 204 | f.write('pipelining = True\n') |
| 205 | f.write('\n[defaults]\n') |
| 206 | f.write('host_key_checking = False\n') |
| 207 | f.close() |
| 208 | |
| 209 | f = open(hosts_pathname, "w") |
| 210 | f.write("[%s]\n" % instance_name) |
| 211 | if proxy_ssh or baremetal_ssh: |
| 212 | f.write("%s ansible_ssh_private_key_file=%s\n" % (hostname, private_key_pathname)) |
| 213 | else: |
| 214 | # acb: Login user is hardcoded, this is not great |
Scott Baker | ac78543 | 2016-02-10 15:25:08 -0800 | [diff] [blame] | 215 | f.write("%s ansible_ssh_private_key_file=%s ansible_ssh_user=ubuntu\n" % (ssh_ip, private_key_pathname)) |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 216 | f.close() |
| 217 | |
| 218 | # SSH will complain if private key is world or group readable |
| 219 | os.chmod(private_key_pathname, 0600) |
| 220 | |
| 221 | print "ANSIBLE_CONFIG=%s" % config_pathname |
| 222 | print "ANSIBLE_HOSTS=%s" % hosts_pathname |
| 223 | |
Srikanth Vavilapalli | 562ba49 | 2016-01-25 20:06:43 -0500 | [diff] [blame] | 224 | return run_template(name, opts, path, ansible_config = config_pathname, ansible_hosts = hosts_pathname, run_ansible_script="/opt/xos/synchronizers/base/run_ansible_verbose") |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 225 | |
| 226 | |
| 227 | |
| 228 | def main(): |
| 229 | run_template('ansible/sync_user_deployments.yaml',{ "endpoint" : "http://172.31.38.128:5000/v2.0/", |
| 230 | "name" : "Sapan Bhatia", |
| 231 | "email": "gwsapan@gmail.com", |
| 232 | "password": "foobar", |
| 233 | "admin_user":"admin", |
| 234 | "admin_password":"6a789bf69dd647e2", |
| 235 | "admin_tenant":"admin", |
| 236 | "tenant":"demo", |
| 237 | "roles":['user','admin'] }) |