blob: d421aa15ea97dddebea4e738dc7ffa1ecb99f98b [file] [log] [blame]
Scott Baker7a327592016-06-20 17:34:06 -07001import hashlib
2import os
3import socket
4import sys
5import base64
6import time
7import re
8import json
9from collections import OrderedDict
10from django.db.models import F, Q
11from xos.config import Config
12from synchronizers.base.ansible import run_template
13from synchronizers.base.syncstep import SyncStep
14from synchronizers.base.ansible import run_template_ssh
15from synchronizers.base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
16from core.models import Service, Slice, Controller, ControllerSlice, ControllerUser, Node, TenantAttribute, Tag
17from services.onos.models import ONOSService, ONOSApp
18from xos.logger import Logger, logging
19from services.vrouter.models import VRouterService
20from services.vtn.models import VTNService
21from services.volt.models import VOLTService, VOLTDevice, AccessDevice
22
23# hpclibrary will be in steps/..
24parentdir = os.path.join(os.path.dirname(__file__),"..")
25sys.path.insert(0,parentdir)
26
27logger = Logger(level=logging.INFO)
28
29class SyncONOSApp(SyncInstanceUsingAnsible):
30 provides=[ONOSApp]
31 observes=ONOSApp
32 requested_interval=0
33 template_name = "sync_onosapp.yaml"
34 #service_key_name = "/opt/xos/synchronizers/onos/onos_key"
35
36 def __init__(self, *args, **kwargs):
37 super(SyncONOSApp, self).__init__(*args, **kwargs)
38
39 def fetch_pending(self, deleted):
40 if (not deleted):
41 objs = ONOSApp.get_tenant_objects().filter(Q(enacted__lt=F('updated')) | Q(enacted=None),Q(lazy_blocked=False))
42 else:
43 objs = ONOSApp.get_deleted_tenant_objects()
44
45 return objs
46
47 def get_instance(self, o):
48 # We assume the ONOS service owns a slice, so pick one of the instances
49 # inside that slice to sync to.
50
51 serv = self.get_onos_service(o)
52
53 if serv.no_container:
54 raise Exception("get_instance() was called on a service that was marked no_container")
55
56 if serv.slices.exists():
57 slice = serv.slices.all()[0]
58 if slice.instances.exists():
59 return slice.instances.all()[0]
60
61 return None
62
63 def get_onos_service(self, o):
64 if not o.provider_service:
65 return None
66
67 onoses = ONOSService.get_service_objects().filter(id=o.provider_service.id)
68 if not onoses:
69 return None
70
71 return onoses[0]
72
73 def is_no_container(self, o):
74 return self.get_onos_service(o).no_container
75
76 def skip_ansible_fields(self, o):
77 return self.is_no_container(o)
78
79 def get_files_dir(self, o):
80 if not hasattr(Config(), "observer_steps_dir"):
81 # make steps_dir mandatory; there's no valid reason for it to not
82 # be defined.
83 raise Exception("observer_steps_dir is not defined in config file")
84
85 step_dir = Config().observer_steps_dir
86
87 return os.path.join(step_dir, "..", "files", str(self.get_onos_service(o).id), o.name)
88
89 def get_cluster_configuration(self, o):
90 instance = self.get_instance(o)
91 if not instance:
92 raise Exception("No instance for ONOS App")
93 node_ips = [socket.gethostbyname(instance.node.name)]
94
95 ipPrefix = ".".join(node_ips[0].split(".")[:3]) + ".*"
96 result = '{ "nodes": ['
97 result = result + ",".join(['{ "ip": "%s"}' % ip for ip in node_ips])
98 result = result + '], "ipPrefix": "%s"}' % ipPrefix
99 return result
100
101 def get_dynamic_parameter_value(self, o, param):
102 instance = self.get_instance(o)
103 if not instance:
104 raise Exception("No instance for ONOS App")
105 if param == 'rabbit_host':
106 return instance.controller.rabbit_host
107 if param == 'rabbit_user':
108 return instance.controller.rabbit_user
109 if param == 'rabbit_password':
110 return instance.controller.rabbit_password
111 if param == 'keystone_tenant_id':
112 cslice = ControllerSlice.objects.get(slice=instance.slice)
113 if not cslice:
114 raise Exception("Controller slice object for %s does not exist" % instance.slice.name)
115 return cslice.tenant_id
116 if param == 'keystone_user_id':
117 cuser = ControllerUser.objects.get(user=instance.creator)
118 if not cuser:
119 raise Exception("Controller user object for %s does not exist" % instance.creator)
120 return cuser.kuser_id
121
122 def get_node_tag(self, o, node, tagname):
123 tags = Tag.select_by_content_object(node).filter(name=tagname)
124 return tags[0].value
125
126 # Scan attrs for attribute name
127 # If it's not present, save it as a TenantAttribute
128 def attribute_default(self, tenant, attrs, name, default):
129 if name in attrs:
130 value = attrs[name]
131 else:
132 value = default
133 logger.info("saving default value %s for attribute %s" % (value, name))
134 ta = TenantAttribute(tenant=tenant, name=name, value=value)
135 ta.save()
136 return value
137
138 # This function currently assumes a single Deployment and Site
139 def get_vtn_config(self, o, attrs):
140
141 privateGatewayMac = None
142 localManagementIp = None
143 ovsdbPort = None
144 sshPort = None
145 sshUser = None
146 sshKeyFile = None
147 mgmtSubnetBits = None
148 xosEndpoint = None
149 xosUser = None
150 xosPassword = None
151
152 # VTN-specific configuration from the VTN Service
153 vtns = VTNService.get_service_objects().all()
154 if vtns:
155 vtn = vtns[0]
156 privateGatewayMac = vtn.privateGatewayMac
157 localManagementIp = vtn.localManagementIp
158 ovsdbPort = vtn.ovsdbPort
159 sshPort = vtn.sshPort
160 sshUser = vtn.sshUser
161 sshKeyFile = vtn.sshKeyFile
162 mgmtSubnetBits = vtn.mgmtSubnetBits
163 xosEndpoint = vtn.xosEndpoint
164 xosUser = vtn.xosUser
165 xosPassword = vtn.xosPassword
166
167 # OpenStack endpoints and credentials
168 keystone_server = "http://keystone:5000/v2.0/"
169 user_name = "admin"
170 password = "ADMIN_PASS"
171 controllers = Controller.objects.all()
172 if controllers:
173 controller = controllers[0]
174 keystone_server = controller.auth_url
175 user_name = controller.admin_user
176 tenant_name = controller.admin_tenant
177 password = controller.admin_password
178
179 data = {
180 "apps" : {
Andy Bavier4a503b12016-06-21 11:41:29 -0400181 "org.opencord.vtn" : {
Scott Baker7a327592016-06-20 17:34:06 -0700182 "cordvtn" : {
183 "privateGatewayMac" : privateGatewayMac,
184 "localManagementIp": localManagementIp,
185 "ovsdbPort": ovsdbPort,
186 "ssh": {
187 "sshPort": sshPort,
188 "sshUser": sshUser,
189 "sshKeyFile": sshKeyFile
190 },
191 "openstack": {
192 "endpoint": keystone_server,
193 "tenant": tenant_name,
194 "user": user_name,
195 "password": password
196 },
197 "xos": {
198 "endpoint": xosEndpoint,
199 "user": xosUser,
200 "password": xosPassword
201 },
202 "publicGateways": [],
203 "nodes" : []
204 }
205 }
206 }
207 }
208
Andy Bavier4a503b12016-06-21 11:41:29 -0400209 # Generate apps->org.opencord.vtn->cordvtn->nodes
Scott Baker7a327592016-06-20 17:34:06 -0700210 nodes = Node.objects.all()
211 for node in nodes:
212 nodeip = socket.gethostbyname(node.name)
213
214 try:
215 bridgeId = self.get_node_tag(o, node, "bridgeId")
216 dataPlaneIntf = self.get_node_tag(o, node, "dataPlaneIntf")
217 dataPlaneIp = self.get_node_tag(o, node, "dataPlaneIp")
218 except:
219 logger.error("not adding node %s to the VTN configuration" % node.name)
220 continue
221
222 node_dict = {
223 "hostname": node.name,
224 "hostManagementIp": "%s/%s" % (nodeip, mgmtSubnetBits),
225 "bridgeId": bridgeId,
226 "dataPlaneIntf": dataPlaneIntf,
227 "dataPlaneIp": dataPlaneIp
228 }
Andy Bavier4a503b12016-06-21 11:41:29 -0400229 data["apps"]["org.opencord.vtn"]["cordvtn"]["nodes"].append(node_dict)
Scott Baker7a327592016-06-20 17:34:06 -0700230
231 # Generate apps->org.onosproject.cordvtn->cordvtn->publicGateways
232 # Pull the gateway information from vRouter
233 vrouters = VRouterService.get_service_objects().all()
234 if vrouters:
235 for gateway in vrouters[0].get_gateways():
236 gatewayIp = gateway['gateway_ip'].split('/',1)[0]
237 gatewayMac = gateway['gateway_mac']
238 gateway_dict = {
239 "gatewayIp": gatewayIp,
240 "gatewayMac": gatewayMac
241 }
Andy Bavier4a503b12016-06-21 11:41:29 -0400242 data["apps"]["org.opencord.vtn"]["cordvtn"]["publicGateways"].append(gateway_dict)
Scott Baker7a327592016-06-20 17:34:06 -0700243
244 return json.dumps(data, indent=4, sort_keys=True)
245
246 def get_volt_network_config(self, o, attrs):
247 try:
248 volt = VOLTService.get_service_objects().all()[0]
249 except:
250 return None
251
252 devices = []
253 for voltdev in volt.volt_devices.all():
254 access_devices = []
255 for access in voltdev.access_devices.all():
256 access_device = {
257 "uplink" : access.uplink,
258 "vlan" : access.vlan
259 }
260 access_devices.append(access_device)
261
262 if voltdev.access_agent:
263 agent = voltdev.access_agent
264 olts = {}
265 for port_mapping in agent.port_mappings.all():
266 olts[port_mapping.port] = port_mapping.mac
267 agent_config = {
268 "olts" : olts,
269 "mac" : agent.mac
270 }
271
272 device = {
273 voltdev.openflow_id : {
274 "accessDevice" : access_devices,
275 "accessAgent" : agent_config
276 },
277 "basic" : {
278 "driver" : voltdev.driver
279 }
280 }
281 devices.append(device)
282
283 data = {
284 "devices" : devices
285 }
286 return json.dumps(data, indent=4, sort_keys=True)
287
288 def get_volt_component_config(self, o, attrs):
289 data = {
290 "org.ciena.onos.ext_notifier.KafkaNotificationBridge":{
291 "rabbit.user": "<rabbit_user>",
292 "rabbit.password": "<rabbit_password>",
293 "rabbit.host": "<rabbit_host>",
294 "publish.kafka": "false",
295 "publish.rabbit": "true",
296 "volt.events.rabbit.topic": "notifications.info",
297 "volt.events.rabbit.exchange": "voltlistener",
298 "volt.events.opaque.info": "{project_id: <keystone_tenant_id>, user_id: <keystone_user_id>}",
299 "publish.volt.events": "true"
300 }
301 }
302 return json.dumps(data, indent=4, sort_keys=True)
303
304 def get_vrouter_network_config(self, o, attrs):
305 # From the onosproject wiki:
306 # https://wiki.onosproject.org/display/ONOS/vRouter
307 data = {
308 "devices" : {
309 "of:00000000000000b1" : {
310 "basic" : {
311 "driver" : "softrouter"
312 }
313 }
314 },
315 "ports" : {
316 "of:00000000000000b1/1" : {
317 "interfaces" : [
318 {
319 "name" : "b1-1",
320 "ips" : [ "10.0.1.2/24" ],
321 "mac" : "00:00:00:00:00:01"
322 }
323 ]
324 },
325 "of:00000000000000b1/2" : {
326 "interfaces" : [
327 {
328 "name" : "b1-2",
329 "ips" : [ "10.0.2.2/24" ],
330 "mac" : "00:00:00:00:00:01"
331 }
332 ]
333 },
334 "of:00000000000000b1/3" : {
335 "interfaces" : [
336 {
337 "name" : "b1-3",
338 "ips" : [ "10.0.3.2/24" ],
339 "mac" : "00:00:00:00:00:01"
340 }
341 ]
342 },
343 "of:00000000000000b1/4" : {
344 "interfaces" : [
345 {
346 "name" : "b1-4",
347 "ips" : [ "10.0.4.2/24" ],
348 "mac" : "00:00:00:00:00:02",
349 "vlan" : "100"
350 }
351 ]
352 }
353 },
354 "apps" : {
355 "org.onosproject.router" : {
356 "router" : {
357 "controlPlaneConnectPoint" : "of:00000000000000b1/5",
358 "ospfEnabled" : "true",
359 "interfaces" : [ "b1-1", "b1-2", "b1-2", "b1-4" ]
360 }
361 }
362 }
363 }
364 return json.dumps(data, indent=4, sort_keys=True)
365
366 def write_configs(self, o):
367 o.config_fns = []
368 o.rest_configs = []
369 o.component_configs = []
370 o.files_dir = self.get_files_dir(o)
371
372 if not os.path.exists(o.files_dir):
373 os.makedirs(o.files_dir)
374
375 # Combine the service attributes with the tenant attributes. Tenant
376 # attribute can override service attributes.
377 attrs = o.provider_service.serviceattribute_dict
378 attrs.update(o.tenantattribute_dict)
379
380 ordered_attrs = attrs.keys()
381
382 onos = self.get_onos_service(o)
383 if onos.node_key:
384 file(os.path.join(o.files_dir, "node_key"),"w").write(onos.node_key)
385 o.node_key_fn="node_key"
386 else:
387 o.node_key_fn=None
388
389 o.early_rest_configs=[]
390 if ("cordvtn" in o.dependencies) and (not self.is_no_container(o)):
391 # For VTN, since it's running in a docker host container, we need
392 # to make sure it configures the cluster using the right ip addresses.
393 # NOTE: rest_onos/v1/cluster/configuration/ will reboot the cluster and
394 # must go first.
395 name="rest_onos/v1/cluster/configuration/"
396 value= self.get_cluster_configuration(o)
397 fn = name[5:].replace("/","_")
398 endpoint = name[5:]
399 file(os.path.join(o.files_dir, fn),"w").write(" " +value)
400 o.early_rest_configs.append( {"endpoint": endpoint, "fn": fn} )
401
402 # Generate config files and save them to the appropriate tenant attributes
403 configs = []
404 for key, value in attrs.iteritems():
405 if key == "autogenerate" and value:
406 for config in value.split(','):
407 configs.append(config.strip())
408
409 for label in configs:
410 config = None
411 value = None
412 if label == "vtn-network-cfg":
413 # Generate the VTN config file... where should this live?
414 config = "rest_onos/v1/network/configuration/"
415 value = self.get_vtn_config(o, attrs)
416 elif label == "volt-network-cfg":
417 config = "rest_onos/v1/network/configuration/"
418 value = self.get_volt_network_config(o, attrs)
419 elif label == "volt-component-cfg":
420 config = "component_config"
421 value = self.get_volt_component_config(o, attrs)
422 elif label == "vrouter-network-cfg":
423 config = "rest_onos/v1/network/configuration/"
424 value = self.get_vrouter_network_config(o, attrs)
425
426 if config:
427 tas = TenantAttribute.objects.filter(tenant=o, name=config)
428 if tas:
429 ta = tas[0]
430 if ta.value != value:
431 logger.info("updating %s with autogenerated config" % config)
432 ta.value = value
433 ta.save()
434 attrs[config] = value
435 else:
436 logger.info("saving autogenerated config %s" % config)
437 ta = TenantAttribute(tenant=o, name=config, value=value)
438 ta.save()
439 attrs[config] = value
440
441 for name in attrs.keys():
442 value = attrs[name]
443 if name.startswith("config_"):
444 fn = name[7:] # .replace("_json",".json")
445 o.config_fns.append(fn)
446 file(os.path.join(o.files_dir, fn),"w").write(value)
447 if name.startswith("rest_"):
448 fn = name[5:].replace("/","_")
449 endpoint = name[5:]
450 # Ansible goes out of it's way to make our life difficult. If
451 # 'lookup' sees a file that it thinks contains json, then it'll
452 # insist on parsing and return a json object. We just want
453 # a string, so prepend a space and then strip the space off
454 # later.
455 file(os.path.join(o.files_dir, fn),"w").write(" " +value)
456 o.rest_configs.append( {"endpoint": endpoint, "fn": fn} )
457 if name.startswith("component_config"):
458 components = json.loads(value,object_pairs_hook=OrderedDict)
459 for component in components.keys():
460 config = components[component]
461 for key in config.keys():
462 config_val = config[key]
463 found = re.findall('<(.+?)>',config_val)
464 for x in found:
465 #Get value corresponding to that string
466 val = self.get_dynamic_parameter_value(o, x)
467 if val:
468 config_val = re.sub('<'+x+'>', val, config_val)
469 #TODO: else raise an exception?
470 o.component_configs.append( {"component": component, "config_params": "'{\""+key+"\":\""+config_val+"\"}'"} )
471
472 def prepare_record(self, o):
473 self.write_configs(o)
474
475 def get_extra_attributes_common(self, o):
476 fields = {}
477
478 # These are attributes that are not dependent on Instance. For example,
479 # REST API stuff.
480
481 onos = self.get_onos_service(o)
482
483 fields["files_dir"] = o.files_dir
484 fields["appname"] = o.name
485 fields["rest_configs"] = o.rest_configs
486 fields["rest_hostname"] = onos.rest_hostname
487 fields["rest_port"] = onos.rest_port
488
489 if o.dependencies:
490 fields["dependencies"] = [x.strip() for x in o.dependencies.split(",")]
491 else:
492 fields["dependencies"] = []
493
Andy Bavier4a503b12016-06-21 11:41:29 -0400494 if o.install_dependencies:
495 fields["install_dependencies"] = [x.strip() for x in o.install_dependencies.split(",")]
496 else:
497 fields["install_dependencies"] = []
498
Scott Baker7a327592016-06-20 17:34:06 -0700499 return fields
500
501 def get_extra_attributes_full(self, o):
502 instance = self.get_instance(o)
503
504 fields = self.get_extra_attributes_common(o)
505
506 fields["config_fns"] = o.config_fns
507 fields["early_rest_configs"] = o.early_rest_configs
508 fields["component_configs"] = o.component_configs
509 fields["node_key_fn"] = o.node_key_fn
510
Scott Baker7a327592016-06-20 17:34:06 -0700511 if (instance.isolation=="container"):
512 fields["ONOS_container"] = "%s-%s" % (instance.slice.name, str(instance.id))
513 else:
514 fields["ONOS_container"] = "ONOS"
515 return fields
516
517 def get_extra_attributes(self, o):
518 if self.is_no_container(o):
519 return self.get_extra_attributes_common(o)
520 else:
521 return self.get_extra_attributes_full(o)
522
523 def sync_fields(self, o, fields):
524 # the super causes the playbook to be run
525 super(SyncONOSApp, self).sync_fields(o, fields)
526
527 def run_playbook(self, o, fields):
528 if self.is_no_container(o):
529 # There is no machine to SSH to, so use the synchronizer's
530 # run_template method directly.
531 run_template("sync_onosapp_nocontainer.yaml", fields)
532 else:
533 super(SyncONOSApp, self).run_playbook(o, fields)
534
535 def delete_record(self, m):
536 pass