Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 1 | import hashlib |
| 2 | import os |
| 3 | import socket |
| 4 | import sys |
| 5 | import base64 |
| 6 | import time |
| 7 | from django.db.models import F, Q |
| 8 | from xos.config import Config |
Sapan Bhatia | 77bb93a | 2016-01-14 17:05:48 -0500 | [diff] [blame] | 9 | from synchronizers.base.syncstep import SyncStep |
| 10 | from synchronizers.base.ansible import run_template_ssh |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 11 | from core.models import Service, Slice, ControllerSlice, ControllerUser |
Scott Baker | b1c0e72 | 2016-01-15 07:57:33 -0800 | [diff] [blame] | 12 | from xos.logger import Logger, logging |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 13 | |
| 14 | logger = Logger(level=logging.INFO) |
| 15 | |
| 16 | class SyncInstanceUsingAnsible(SyncStep): |
| 17 | # All of the following should be defined for classes derived from this |
Scott Baker | 4c052b2 | 2016-02-11 11:08:42 -0800 | [diff] [blame] | 18 | # base class. Examples below use VSGTenant. |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 19 | |
Scott Baker | 4c052b2 | 2016-02-11 11:08:42 -0800 | [diff] [blame] | 20 | # provides=[VSGTenant] |
| 21 | # observes=VSGTenant |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 22 | # requested_interval=0 |
| 23 | # template_name = "sync_vcpetenant.yaml" |
| 24 | # service_key_name = "/opt/xos/observers/vcpe/vcpe_private_key" |
| 25 | |
| 26 | def __init__(self, **args): |
| 27 | SyncStep.__init__(self, **args) |
| 28 | |
Scott Baker | df540bc | 2016-02-10 12:41:10 -0800 | [diff] [blame] | 29 | def skip_ansible_fields(self, o): |
| 30 | # Return True if the instance processing and get_ansible_fields stuff |
| 31 | # should be skipped. This hook is primarily for the OnosApp |
| 32 | # sync step, so it can do its external REST API sync thing. |
| 33 | return False |
| 34 | |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 35 | def defer_sync(self, o, reason): |
| 36 | logger.info("defer object %s due to %s" % (str(o), reason)) |
| 37 | raise Exception("defer object %s due to %s" % (str(o), reason)) |
| 38 | |
| 39 | def get_extra_attributes(self, o): |
| 40 | # This is a place to include extra attributes that aren't part of the |
| 41 | # object itself. |
| 42 | |
| 43 | return {} |
| 44 | |
| 45 | def get_instance(self, o): |
| 46 | # We need to know what instance is associated with the object. Let's |
| 47 | # assume 'o' has a field called 'instance'. If the field is called |
| 48 | # something else, or if custom logic is needed, then override this |
| 49 | # method. |
| 50 | |
| 51 | return o.instance |
| 52 | |
Scott Baker | 268e2aa | 2016-02-10 12:23:53 -0800 | [diff] [blame] | 53 | def get_external_sync(self, o): |
| 54 | hostname = getattr(o, "external_hostname", None) |
| 55 | container = getattr(o, "external_container", None) |
| 56 | if hostname and container: |
| 57 | return (hostname, container) |
| 58 | else: |
| 59 | return None |
| 60 | |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 61 | def run_playbook(self, o, fields, template_name=None): |
| 62 | if not template_name: |
| 63 | template_name = self.template_name |
| 64 | tStart = time.time() |
| 65 | run_template_ssh(template_name, fields) |
| 66 | logger.info("playbook execution time %d" % int(time.time()-tStart)) |
| 67 | |
| 68 | def pre_sync_hook(self, o, fields): |
| 69 | pass |
| 70 | |
| 71 | def post_sync_hook(self, o, fields): |
| 72 | pass |
| 73 | |
| 74 | def sync_fields(self, o, fields): |
| 75 | self.run_playbook(o, fields) |
| 76 | |
| 77 | def prepare_record(self, o): |
| 78 | pass |
| 79 | |
| 80 | def get_node(self,o): |
| 81 | return o.node |
| 82 | |
| 83 | def get_node_key(self, node): |
| 84 | return "/root/setup/node_key" |
| 85 | |
| 86 | def get_ansible_fields(self, instance): |
| 87 | # return all of the fields that tell Ansible how to talk to the context |
| 88 | # that's setting up the container. |
| 89 | |
| 90 | if (instance.isolation == "vm"): |
| 91 | # legacy where container was configured by sync_vcpetenant.py |
| 92 | |
| 93 | fields = { "instance_name": instance.name, |
| 94 | "hostname": instance.node.name, |
| 95 | "instance_id": instance.instance_id, |
| 96 | "username": "ubuntu", |
Scott Baker | ac78543 | 2016-02-10 15:25:08 -0800 | [diff] [blame] | 97 | "ssh_ip": instance.get_ssh_ip(), |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 98 | } |
| 99 | key_name = self.service_key_name |
| 100 | elif (instance.isolation == "container"): |
| 101 | # container on bare metal |
| 102 | node = self.get_node(instance) |
| 103 | hostname = node.name |
| 104 | fields = { "hostname": hostname, |
| 105 | "baremetal_ssh": True, |
| 106 | "instance_name": "rootcontext", |
| 107 | "username": "root", |
| 108 | "container_name": "%s-%s" % (instance.slice.name, str(instance.id)) |
Scott Baker | ac78543 | 2016-02-10 15:25:08 -0800 | [diff] [blame] | 109 | # ssh_ip is not used for container-on-metal |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 110 | } |
| 111 | key_name = self.get_node_key(node) |
| 112 | else: |
| 113 | # container in a VM |
| 114 | if not instance.parent: |
| 115 | raise Exception("Container-in-VM has no parent") |
| 116 | if not instance.parent.instance_id: |
| 117 | raise Exception("Container-in-VM parent is not yet instantiated") |
| 118 | if not instance.parent.slice.service: |
| 119 | raise Exception("Container-in-VM parent has no service") |
| 120 | if not instance.parent.slice.service.private_key_fn: |
| 121 | raise Exception("Container-in-VM parent service has no private_key_fn") |
| 122 | fields = { "hostname": instance.parent.node.name, |
| 123 | "instance_name": instance.parent.name, |
| 124 | "instance_id": instance.parent.instance_id, |
| 125 | "username": "ubuntu", |
Scott Baker | ac78543 | 2016-02-10 15:25:08 -0800 | [diff] [blame] | 126 | "ssh_ip": instance.parent.get_ssh_ip(), |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 127 | "container_name": "%s-%s" % (instance.slice.name, str(instance.id)) |
| 128 | } |
| 129 | key_name = instance.parent.slice.service.private_key_fn |
| 130 | |
| 131 | if not os.path.exists(key_name): |
| 132 | raise Exception("Node key %s does not exist" % key_name) |
| 133 | |
| 134 | key = file(key_name).read() |
| 135 | |
| 136 | fields["private_key"] = key |
| 137 | |
| 138 | # now the ceilometer stuff |
| 139 | |
| 140 | cslice = ControllerSlice.objects.get(slice=instance.slice) |
| 141 | if not cslice: |
| 142 | raise Exception("Controller slice object for %s does not exist" % instance.slice.name) |
| 143 | |
| 144 | cuser = ControllerUser.objects.get(user=instance.creator) |
| 145 | if not cuser: |
| 146 | raise Exception("Controller user object for %s does not exist" % instance.creator) |
| 147 | |
| 148 | fields.update({"keystone_tenant_id": cslice.tenant_id, |
| 149 | "keystone_user_id": cuser.kuser_id, |
| 150 | "rabbit_user": instance.controller.rabbit_user, |
| 151 | "rabbit_password": instance.controller.rabbit_password, |
| 152 | "rabbit_host": instance.controller.rabbit_host}) |
| 153 | |
| 154 | return fields |
| 155 | |
| 156 | def sync_record(self, o): |
| 157 | logger.info("sync'ing object %s" % str(o)) |
| 158 | |
| 159 | self.prepare_record(o) |
| 160 | |
Scott Baker | df540bc | 2016-02-10 12:41:10 -0800 | [diff] [blame] | 161 | if self.skip_ansible_fields(o): |
| 162 | fields = {} |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 163 | else: |
Scott Baker | df540bc | 2016-02-10 12:41:10 -0800 | [diff] [blame] | 164 | if self.get_external_sync(o): |
| 165 | # sync to some external host |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 166 | |
Scott Baker | df540bc | 2016-02-10 12:41:10 -0800 | [diff] [blame] | 167 | # UNTESTED |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 168 | |
Scott Baker | df540bc | 2016-02-10 12:41:10 -0800 | [diff] [blame] | 169 | (hostname, container_name) = self.get_external_sync(o) |
| 170 | fields = { "hostname": hostname, |
| 171 | "baremetal_ssh": True, |
| 172 | "instance_name": "rootcontext", |
| 173 | "username": "root", |
| 174 | "container_name": container_name |
| 175 | } |
| 176 | key_name = self.get_node_key(node) |
| 177 | if not os.path.exists(key_name): |
| 178 | raise Exception("Node key %s does not exist" % key_name) |
| 179 | |
| 180 | key = file(key_name).read() |
| 181 | |
| 182 | fields["private_key"] = key |
| 183 | # TO DO: Ceilometer stuff |
| 184 | else: |
| 185 | instance = self.get_instance(o) |
| 186 | # sync to an XOS instance |
| 187 | if not instance: |
| 188 | self.defer_sync(o, "waiting on instance") |
| 189 | return |
| 190 | |
| 191 | if not instance.instance_name: |
| 192 | self.defer_sync(o, "waiting on instance.instance_name") |
| 193 | return |
| 194 | |
| 195 | fields = self.get_ansible_fields(instance) |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 196 | |
Scott Baker | 268e2aa | 2016-02-10 12:23:53 -0800 | [diff] [blame] | 197 | fields["ansible_tag"] = o.__class__.__name__ + "_" + str(o.id) |
Sapan Bhatia | 037c947 | 2016-01-14 11:44:43 -0500 | [diff] [blame] | 198 | |
| 199 | # If 'o' defines a 'sync_attributes' list, then we'll copy those |
| 200 | # attributes into the Ansible recipe's field list automatically. |
| 201 | if hasattr(o, "sync_attributes"): |
| 202 | for attribute_name in o.sync_attributes: |
| 203 | fields[attribute_name] = getattr(o, attribute_name) |
| 204 | |
| 205 | fields.update(self.get_extra_attributes(o)) |
| 206 | |
| 207 | self.sync_fields(o, fields) |
| 208 | |
| 209 | o.save() |
| 210 | |
| 211 | def delete_record(self, o): |
| 212 | try: |
| 213 | controller = o.get_controller() |
| 214 | controller_register = json.loads(o.node.site_deployment.controller.backend_register) |
| 215 | |
| 216 | if (controller_register.get('disabled',False)): |
| 217 | raise InnocuousException('Controller %s is disabled'%o.node.site_deployment.controller.name) |
| 218 | except AttributeError: |
| 219 | pass |
| 220 | |
| 221 | instance = self.get_instance(o) |
| 222 | if isinstance(instance, basestring): |
| 223 | # sync to some external host |
| 224 | |
| 225 | # XXX - this probably needs more work... |
| 226 | |
| 227 | fields = { "hostname": instance, |
| 228 | "instance_id": "ubuntu", # this is the username to log into |
| 229 | "private_key": service.key, |
| 230 | } |
| 231 | else: |
| 232 | # sync to an XOS instance |
| 233 | fields = self.get_ansible_fields(instance) |
| 234 | |
| 235 | fields["ansible_tag"] = o.__class__.__name__ + "_" + str(o.id) |
| 236 | |
| 237 | # If 'o' defines a 'sync_attributes' list, then we'll copy those |
| 238 | # attributes into the Ansible recipe's field list automatically. |
| 239 | if hasattr(o, "sync_attributes"): |
| 240 | for attribute_name in o.sync_attributes: |
| 241 | fields[attribute_name] = getattr(o, attribute_name) |
| 242 | |
| 243 | fields.update(self.map_delete_inputs(o)) |
| 244 | |
| 245 | fields['delete']=True |
| 246 | res = self.run_playbook(o,fields) |
| 247 | try: |
| 248 | self.map_delete_outputs(o,res) |
| 249 | except AttributeError: |
| 250 | pass |