1 ##############################################################################
2 # Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 ##############################################################################
11 from django.conf import settings
12 from django.forms import formset_factory
13 from django.core.exceptions import ValidationError
15 from typing import List
18 from xml.dom import minidom
21 from workflow.models import WorkflowStep
22 from account.models import Lab
23 from workflow.forms import (
24 HardwareDefinitionForm,
25 NetworkDefinitionForm,
27 HostSoftwareDefinitionForm,
29 from resource_inventory.models import (
31 ResourceConfiguration,
32 InterfaceConfiguration,
37 from dashboard.exceptions import (
38 InvalidVlanConfigurationException,
39 NetworkExistsException,
40 ResourceAvailabilityException
44 logger = logging.getLogger(__name__)
47 class Define_Hardware(WorkflowStep):
49 template = 'resource/steps/define_hardware.html'
50 title = "Define Hardware"
51 description = "Choose the type and amount of machines you want"
54 def __init__(self, *args, **kwargs):
56 super().__init__(*args, **kwargs)
58 def get_context(self):
59 context = super(Define_Hardware, self).get_context()
60 user = self.repo_get(self.repo.SESSION_USER)
61 context['form'] = self.form or HardwareDefinitionForm(user)
64 def update_models(self, data):
65 data = data['filter_field']
66 models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {})
67 models['resources'] = [] # This will always clear existing data when this step changes
68 models['connections'] = []
69 models['interfaces'] = {}
70 if "template" not in models:
71 template = ResourceTemplate.objects.create(temporary=True)
72 models['template'] = template
74 resource_data = data['resource']
76 new_template = models['template']
78 public_network = Network.objects.create(name="public", bundle=new_template, is_public=True)
80 all_networks = {public_network.id: public_network}
82 for resource_template_dict in resource_data.values():
83 id = resource_template_dict['id']
84 old_template = ResourceTemplate.objects.get(id=id)
86 # instantiate genericHost and store in repo
87 for _ in range(0, resource_template_dict['count']):
88 resource_configs = old_template.resourceConfigurations.all()
89 for config in resource_configs:
90 # need to save now for connections to refer to it later
91 new_config = ResourceConfiguration.objects.create(
92 profile=config.profile,
95 template=new_template)
97 for interface_config in config.interface_configs.all():
98 new_interface_config = InterfaceConfiguration.objects.create(
99 profile=interface_config.profile,
100 resource_config=new_config)
102 for connection in interface_config.connections.all():
104 if connection.network.is_public:
105 network = public_network
107 # check if network is known
108 if connection.network.id not in all_networks:
109 # create matching one
110 new_network = Network(
111 name=connection.network.name + "_" + str(new_config.id),
116 all_networks[connection.network.id] = new_network
118 network = all_networks[connection.network.id]
120 new_connection = NetworkConnection(
122 vlan_is_tagged=connection.vlan_is_tagged)
124 new_interface_config.save() # can't do later because M2M on next line
125 new_connection.save()
127 new_interface_config.connections.add(new_connection)
129 unique_resource_ref = new_config.name + "_" + str(new_config.id)
130 if unique_resource_ref not in models['interfaces']:
131 models['interfaces'][unique_resource_ref] = []
132 models['interfaces'][unique_resource_ref].append(interface_config)
134 models['resources'].append(new_config)
136 models['networks'] = all_networks
138 # add selected lab to models
139 for lab_dict in data['lab'].values():
140 if lab_dict['selected']:
141 models['template'].lab = Lab.objects.get(lab_user__id=lab_dict['id'])
142 models['template'].save()
143 break # if somehow we get two 'true' labs, we only use one
146 self.repo_put(self.repo.RESOURCE_TEMPLATE_MODELS, models)
148 def update_confirmation(self):
149 confirm = self.repo_get(self.repo.CONFIRMATION, {})
150 if "template" not in confirm:
151 confirm['template'] = {}
152 confirm['template']['resources'] = []
153 models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {})
154 if 'template' in models:
155 for resource in models['template'].getConfigs():
156 host_dict = {"name": resource.name, "profile": resource.profile.name}
157 confirm['template']['resources'].append(host_dict)
158 if "template" in models:
159 confirm['template']['lab'] = models['template'].lab.lab_user.username
160 self.repo_put(self.repo.CONFIRMATION, confirm)
162 def post(self, post_data, user):
164 user = self.repo_get(self.repo.SESSION_USER)
165 self.form = HardwareDefinitionForm(user, post_data)
166 if self.form.is_valid():
167 self.update_models(self.form.cleaned_data)
168 self.update_confirmation()
169 self.set_valid("Step Completed")
171 self.set_invalid("Please complete the fields highlighted in red to continue")
172 except Exception as e:
173 print("Caught exception: " + str(e))
174 traceback.print_exc()
175 self.set_invalid(str(e))
178 class Define_Software(WorkflowStep):
179 template = 'config_bundle/steps/define_software.html'
180 title = "Pick Software"
181 description = "Choose the opnfv and image of your machines"
182 short_title = "host config"
184 def build_filter_data(self, hosts_data):
186 Build list of Images to filter out.
188 returns a 2D array of images to exclude
189 based on the ordering of the passed
194 user = self.repo_get(self.repo.SESSION_USER)
195 lab = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS)['template'].lab
196 for i, host_data in enumerate(hosts_data):
197 host = ResourceConfiguration.objects.get(pk=host_data['host_id'])
198 wrong_owner = Image.objects.exclude(owner=user).exclude(public=True)
199 wrong_host = Image.objects.exclude(architecture=host.profile.architecture)
200 wrong_lab = Image.objects.exclude(from_lab=lab)
201 excluded_images = wrong_owner | wrong_host | wrong_lab
202 filter_data.append([])
203 for image in excluded_images:
204 filter_data[i].append(image.pk)
207 def create_hostformset(self, hostlist, data=None):
209 configs = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}).get("resources")
211 for config in configs:
212 hosts_initial.append({
213 'host_id': config.id,
214 'host_name': config.name,
215 'headnode': config.is_head_node,
216 'image': config.image
219 for host in hostlist:
220 hosts_initial.append({
222 'host_name': host.name
225 HostFormset = formset_factory(HostSoftwareDefinitionForm, extra=0)
226 filter_data = self.build_filter_data(hosts_initial)
228 class SpecialHostFormset(HostFormset):
229 def get_form_kwargs(self, index):
230 kwargs = super(SpecialHostFormset, self).get_form_kwargs(index)
231 if index is not None:
232 kwargs['imageQS'] = Image.objects.exclude(pk__in=filter_data[index])
236 return SpecialHostFormset(data, initial=hosts_initial)
237 return SpecialHostFormset(initial=hosts_initial)
239 def get_host_list(self, grb=None):
240 return self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS).get("resources")
242 def get_context(self):
243 context = super(Define_Software, self).get_context()
245 context["formset"] = self.create_hostformset(self.get_host_list())
249 def post(self, post_data, user):
250 hosts = self.get_host_list()
252 # TODO: fix headnode in form, currently doesn't return a selected one
253 # models['headnode_index'] = post_data.get("headnode", 1)
254 formset = self.create_hostformset(hosts, data=post_data)
256 if formset.is_valid():
257 for i, form in enumerate(formset):
259 image = form.cleaned_data['image']
260 hostname = form.cleaned_data['host_name']
261 headnode = form.cleaned_data['headnode']
264 host.is_head_node = headnode
269 if not has_headnode and len(hosts) > 0:
270 self.set_invalid("No headnode. Please set a headnode.")
273 self.set_valid("Completed")
275 self.set_invalid("Please complete all fields")
278 class Define_Nets(WorkflowStep):
279 template = 'resource/steps/pod_definition.html'
280 title = "Define Networks"
281 description = "Use the tool below to draw the network topology of your POD"
282 short_title = "networking"
283 form = NetworkDefinitionForm
286 vlans = self.repo_get(self.repo.VLANS)
289 # try to grab some vlans from lab
290 models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {})
291 if "bundle" not in models:
293 lab = models['bundle'].lab
294 if lab is None or lab.vlan_manager is None:
297 vlans = lab.vlan_manager.get_vlans(count=lab.vlan_manager.block_size)
298 self.repo_put(self.repo.VLANS, vlans)
303 def make_mx_network_dict(self, network):
306 'name': network.name,
307 'public': network.is_public
310 def make_mx_resource_dict(self, resource_config):
312 'id': resource_config.id,
315 'name': resource_config.name,
316 'id': resource_config.id,
317 'description': resource_config.profile.description
321 for interface_config in resource_config.interface_configs.all():
323 for connection in interface_config.connections.all():
324 connections.append({'tagged': connection.vlan_is_tagged, 'network': connection.network.id})
327 "id": interface_config.id,
328 "name": interface_config.profile.name,
329 "description": "speed: " + str(interface_config.profile.speed) + "M\ntype: " + interface_config.profile.nic_type,
330 "connections": connections
333 resource_dict['interfaces'].append(interface_dict)
337 def make_mx_host_dict(self, generic_host):
339 'id': generic_host.profile.name,
342 "name": generic_host.profile.name,
343 "description": generic_host.profile.description
346 for iface in generic_host.profile.interfaceprofile.all():
347 host['interfaces'].append({
349 "description": "speed: " + str(iface.speed) + "M\ntype: " + iface.nic_type
353 # first step guards this one, so can't get here without at least empty
354 # models being populated by step one
355 def get_context(self):
356 context = super(Define_Nets, self).get_context()
358 'form': NetworkDefinitionForm(),
359 'debug': settings.DEBUG,
369 models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS) # infallible, guarded by prior step
370 for resource in models['resources']:
371 d = self.make_mx_resource_dict(resource)
372 context['resources'][d['id']] = d
374 for network in models['networks'].values():
375 d = self.make_mx_network_dict(network)
376 context['networks'][d['id']] = d
380 def post(self, post_data, user):
382 xmlData = post_data.get("xml")
383 self.updateModels(xmlData)
384 # update model with xml
385 self.set_valid("Networks applied successfully")
386 except ResourceAvailabilityException:
387 self.set_invalid("Public network not availble")
388 except Exception as e:
389 traceback.print_exc()
390 self.set_invalid("An error occurred when applying networks: " + str(e))
392 def resetNetworks(self, networks: List[Network]): # potentially just pass template here?
393 for network in networks:
396 def updateModels(self, xmlData):
397 models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {})
402 given_hosts, interfaces, networks = self.parseXml(xmlData)
403 except Exception as e:
404 print("tried to parse Xml, got exception instead:")
407 existing_rconfig_list = models.get("resources", [])
408 existing_rconfigs = {} # maps id to host
409 for rconfig in existing_rconfig_list:
410 existing_rconfigs["host_" + str(rconfig.id)] = rconfig
412 bundle = models.get("template") # hard fail if not in repo
414 self.resetNetworks(models['networks'].values())
415 models['networks'] = {}
417 for net_id, net in networks.items():
418 network = Network.objects.create(
421 is_public=net['public'])
423 models['networks'][net_id] = network
426 for hostid, given_host in given_hosts.items():
427 for ifaceId in given_host['interfaces']:
428 iface = interfaces[ifaceId]
430 iface_config = InterfaceConfiguration.objects.get(id=iface['config_id'])
431 if iface_config.resource_config.template.id != bundle.id:
432 raise ValidationError("User does not own the template they are editing")
434 for connection in iface['connections']:
435 network_id = connection['network']
436 net = models['networks'][network_id]
437 connection = NetworkConnection(vlan_is_tagged=connection['tagged'], network=net)
439 iface_config.connections.add(connection)
441 self.repo_put(self.repo.RESOURCE_TEMPLATE_MODELS, models)
443 def decomposeXml(self, xmlString):
445 Translate XML into useable data.
447 This function takes in an xml doc from our front end
448 and returns dictionaries that map cellIds to the xml
449 nodes themselves. There is no unpacking of the
450 xml objects, just grouping and organizing
458 xmlDom = minidom.parseString(xmlString)
459 root = xmlDom.documentElement.firstChild
460 for cell in root.childNodes:
461 cellId = cell.getAttribute('id')
462 group = cellId.split("_")[0]
463 parentGroup = cell.getAttribute("parent").split("_")[0]
464 # place cell into correct group
466 if cell.getAttribute("edge"):
467 connections[cellId] = cell
469 elif "network" in group:
470 networks[cellId] = cell
472 elif "host" in group:
475 elif "host" in parentGroup:
476 interfaces[cellId] = cell
478 # make network ports also map to thier network
479 elif "network" in parentGroup:
480 network_ports[cellId] = cell.getAttribute("parent") # maps port ID to net ID
482 return connections, networks, hosts, interfaces, network_ports
484 # serialize and deserialize xml from mxGraph
485 def parseXml(self, xmlString):
486 networks = {} # maps net name to network object
487 hosts = {} # cotains id -> hosts, each containing interfaces, referencing networks
488 interfaces = {} # maps id -> interface
489 untagged_ifaces = set() # used to check vlan config
490 network_names = set() # used to check network names
491 xml_connections, xml_nets, xml_hosts, xml_ifaces, xml_ports = self.decomposeXml(xmlString)
494 for cellId, cell in xml_hosts.items():
495 cell_json_str = cell.getAttribute("value")
496 cell_json = json.loads(cell_json_str)
497 host = {"interfaces": [], "name": cellId, "hostname": cell_json['name']}
501 for cellId, cell in xml_nets.items():
502 escaped_json_str = cell.getAttribute("value")
503 json_str = escaped_json_str.replace('"', '"')
504 net_info = json.loads(json_str)
505 net_name = net_info['name']
506 public = net_info['public']
507 if net_name in network_names:
508 raise NetworkExistsException("Non unique network name found")
509 network = {"name": net_name, "public": public, "id": cellId}
510 networks[cellId] = network
511 network_names.add(net_name)
514 for cellId, cell in xml_ifaces.items():
515 parentId = cell.getAttribute('parent')
516 cell_json_str = cell.getAttribute("value")
517 cell_json = json.loads(cell_json_str)
518 iface = {"graph_id": cellId, "connections": [], "config_id": cell_json['id'], "profile_name": cell_json['name']}
519 hosts[parentId]['interfaces'].append(cellId)
520 interfaces[cellId] = iface
523 for cellId, cell in xml_connections.items():
524 escaped_json_str = cell.getAttribute("value")
525 json_str = escaped_json_str.replace('"', '"')
526 attributes = json.loads(json_str)
527 tagged = attributes['tagged']
530 src = cell.getAttribute("source")
531 tgt = cell.getAttribute("target")
532 if src in interfaces:
533 interface = interfaces[src]
534 network = networks[xml_ports[tgt]]
536 interface = interfaces[tgt]
537 network = networks[xml_ports[src]]
540 if interface['config_id'] in untagged_ifaces:
541 raise InvalidVlanConfigurationException("More than one untagged vlan on an interface")
542 untagged_ifaces.add(interface['config_id'])
544 # add connection to interface
545 interface['connections'].append({"tagged": tagged, "network": network['id']})
547 return hosts, interfaces, networks
550 class Resource_Meta_Info(WorkflowStep):
551 template = 'resource/steps/meta_info.html'
553 description = "Please fill out the rest of the information about your resource"
554 short_title = "pod info"
556 def update_confirmation(self):
557 confirm = self.repo_get(self.repo.CONFIRMATION, {})
558 if "template" not in confirm:
559 confirm['template'] = {}
560 models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {})
561 if "template" in models:
562 confirm['template']['description'] = models['template'].description
563 confirm['template']['name'] = models['template'].name
564 self.repo_put(self.repo.CONFIRMATION, confirm)
566 def get_context(self):
567 context = super(Resource_Meta_Info, self).get_context()
570 models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, None)
571 bundle = models['template']
574 desc = bundle.description
575 context['form'] = ResourceMetaForm(initial={"bundle_name": name, "bundle_description": desc})
578 def post(self, post_data, user):
579 form = ResourceMetaForm(post_data)
581 models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {})
582 name = form.cleaned_data['bundle_name']
583 desc = form.cleaned_data['bundle_description']
584 bundle = models['template'] # infallible
586 bundle.description = desc
588 self.repo_put(self.repo.RESOURCE_TEMPLATE_MODELS, models)
589 confirm = self.repo_get(self.repo.CONFIRMATION)
590 if "resource" not in confirm:
591 confirm['resource'] = {}
592 confirm_info = confirm['resource']
593 confirm_info["name"] = name
596 tmp = tmp[:60] + "..."
597 confirm_info["description"] = tmp
598 self.repo_put(self.repo.CONFIRMATION, confirm)
599 self.set_valid("Step Completed")
601 self.set_invalid("Please correct the fields highlighted in red to continue")