From 25275685e9a735e51fae8b1a936ba5733f6fb770 Mon Sep 17 00:00:00 2001 From: Parker Berberian Date: Wed, 10 Oct 2018 16:06:47 -0400 Subject: Lab as a Service 2.0 See changes here: https://wiki.opnfv.org/display/INF/Pharos+Laas Change-Id: I59ada5f98e70a28d7f8c14eab3239597e236ca26 Signed-off-by: Sawyer Bergeron Signed-off-by: Parker Berberian --- dashboard/src/workflow/resource_bundle_workflow.py | 427 +++++++++++++++++++++ 1 file changed, 427 insertions(+) create mode 100644 dashboard/src/workflow/resource_bundle_workflow.py (limited to 'dashboard/src/workflow/resource_bundle_workflow.py') diff --git a/dashboard/src/workflow/resource_bundle_workflow.py b/dashboard/src/workflow/resource_bundle_workflow.py new file mode 100644 index 0000000..63ce3bd --- /dev/null +++ b/dashboard/src/workflow/resource_bundle_workflow.py @@ -0,0 +1,427 @@ +############################################################################## +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from django.shortcuts import render +from django.forms import formset_factory + +import json +import re +from xml.dom import minidom + +from workflow.models import WorkflowStep +from workflow.forms import * +from resource_inventory.models import * +from dashboard.exceptions import * + +import logging +logger = logging.getLogger(__name__) + + +class Define_Hardware(WorkflowStep): + + template = 'resource/steps/define_hardware.html' + title = "Define Hardware" + description = "Choose the type and amount of machines you want" + short_title = "hosts" + def get_context(self): + context = super(Define_Hardware, self).get_context() + selection_data = {"hosts": {}, "labs": {}} + models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) + hosts = models.get("hosts", []) + for host in hosts: + profile_id = "host_" + str(host.profile.id) + if profile_id not in selection_data['hosts']: + selection_data['hosts'][profile_id] = [] + selection_data['hosts'][profile_id].append({"host_name": host.resource.name, "class": profile_id}) + + if models.get("bundle", GenericResourceBundle()).lab: + selection_data['labs'] = {"lab_" + str(models.get("bundle").lab.lab_user.id): "true"} + + form = HardwareDefinitionForm( + selection_data=selection_data + ) + context['form'] = form + return context + + def render(self, request): + self.context = self.get_context() + return render(request, self.template, self.context) + + + def update_models(self, data): + data = json.loads(data['filter_field']) + models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) + models['hosts'] = [] # This will always clear existing data when this step changes + models['interfaces'] = {} + if "bundle" not in models: + models['bundle'] = GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER)) + host_data = data['hosts'] + names = {} + for host_dict in host_data: + id = host_dict['class'] + # bit of formatting + id = int(id.split("_")[-1]) + profile = HostProfile.objects.get(id=id) + # instantiate genericHost and store in repo + name = host_dict['host_name'] + if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})", name): + raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point") + if name in names: + raise NonUniqueHostnameException("All hosts must have unique names") + names[name] = True + genericResource = GenericResource(bundle=models['bundle'], name=name) + genericHost = GenericHost(profile=profile, resource=genericResource) + models['hosts'].append(genericHost) + for interface_profile in profile.interfaceprofile.all(): + genericInterface = GenericInterface(profile=interface_profile, host=genericHost) + if genericHost.resource.name not in models['interfaces']: + models['interfaces'][genericHost.resource.name] = [] + models['interfaces'][genericHost.resource.name].append(genericInterface) + + # add selected lab to models + for lab_dict in data['labs']: + if list(lab_dict.values())[0]: # True for lab the user selected + lab_user_id = int(list(lab_dict.keys())[0].split("_")[-1]) + models['bundle'].lab = Lab.objects.get(lab_user__id=lab_user_id) + break # if somehow we get two 'true' labs, we only use one + + # return to repo + self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models) + + + def update_confirmation(self): + confirm = self.repo_get(self.repo.CONFIRMATION, {}) + if "resource" not in confirm: + confirm['resource'] = {} + confirm['resource']['hosts'] = [] + models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {"hosts": []}) + for host in models['hosts']: + host_dict = {"name": host.resource.name, "profile": host.profile.name} + confirm['resource']['hosts'].append(host_dict) + if "lab" in models: + confirm['resource']['lab'] = models['lab'].lab_user.username + self.repo_put(self.repo.CONFIRMATION, confirm) + + + def post_render(self, request): + try: + self.form = HardwareDefinitionForm(request.POST) + if self.form.is_valid(): + self.update_models(self.form.cleaned_data) + self.update_confirmation() + self.metastep.set_valid("Step Completed") + else: + self.metastep.set_invalid("Please complete the fields highlighted in red to continue") + pass + except Exception as e: + self.metastep.set_invalid(str(e)) + self.context = self.get_context() + return render(request, self.template, self.context) + +class Define_Nets(WorkflowStep): + template = 'resource/steps/pod_definition.html' + title = "Define Networks" + description = "Use the tool below to draw the network topology of your POD" + short_title = "networking" + form = NetworkDefinitionForm + + def get_vlans(self): + vlans = self.repo_get(self.repo.VLANS) + if vlans: + return vlans + # try to grab some vlans from lab + models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) + if "bundle" not in models: + return None + lab = models['bundle'].lab + if lab is None or lab.vlan_manager is None: + return None + try: + vlans = lab.vlan_manager.get_vlan(count=lab.vlan_manager.block_size) + self.repo_put(self.repo.VLANS, vlans) + return vlans + except Exception as e: + return None + + def get_context(self): + # TODO: render *primarily* on hosts in repo models + context = super(Define_Nets, self).get_context() + context['form'] = NetworkDefinitionForm() + try: + context['hosts'] = [] + models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) + vlans = self.get_vlans() + if vlans: + context['vlans'] = vlans + hosts = models.get("hosts", []) + hostlist = self.repo_get(self.repo.GRB_LAST_HOSTLIST, None) + added_list = [] + added_dict = {} + context['added_hosts'] = [] + if not hostlist is None: + new_hostlist = [] + for host in models['hosts']: + intcount = host.profile.interfaceprofile.count() + new_hostlist.append(str(host.resource.name) + "*" + str(host.profile) + "&" + str(intcount)) + context['removed_hosts'] = list(set(hostlist) - set(new_hostlist)) + added_list = list(set(new_hostlist) - set(hostlist)) + for hoststr in added_list: + key = hoststr.split("*")[0] + added_dict[key] = hoststr + for generic_host in hosts: + host_profile = generic_host.profile + host = {} + host['id'] = generic_host.resource.name + host['interfaces'] = [] + for iface in host_profile.interfaceprofile.all(): + host['interfaces'].append({ + "name": iface.name, + "description": "speed: " + str(iface.speed) + "M\ntype: " + iface.nic_type}) + host['value'] = {"name": generic_host.resource.name} + host['value']['description'] = generic_host.profile.description + context['hosts'].append(json.dumps(host)) + if host['id'] in added_dict: + context['added_hosts'].append(json.dumps(host)) + bundle = models.get("bundle", False) + if bundle and bundle.xml: + context['xml'] = bundle.xml + else: + context['xml'] = False + + except Exception as e: + pass + return context + + def post_render(self, request): + models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) + if 'hosts' in models: + hostlist = [] + for host in models['hosts']: + intcount = host.profile.interfaceprofile.count() + hostlist.append(str(host.resource.name) + "*" + str(host.profile) + "&" + str(intcount)) + self.repo_put(self.repo.GRB_LAST_HOSTLIST, hostlist) + try: + xmlData = request.POST.get("xml") + self.updateModels(xmlData) + # update model with xml + self.metastep.set_valid("Networks applied successfully") + except Exception as e: + self.metastep.set_invalid("An error occurred when applying networks") + return self.render(request) + + def updateModels(self, xmlData): + models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) + models["vlans"] = {} + given_hosts, interfaces = self.parseXml(xmlData) + vlan_manager = models['bundle'].lab.vlan_manager + existing_host_list = models.get("hosts", []) + existing_hosts = {} # maps id to host + for host in existing_host_list: + existing_hosts[host.resource.name] = host + + bundle = models.get("bundle", GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER))) + + for hostid, given_host in given_hosts.items(): + existing_host = existing_hosts[hostid[5:]] + + for ifaceId in given_host['interfaces']: + iface = interfaces[ifaceId] + iface_profile = existing_host.profile.interfaceprofile.get(name=iface['profile_name']) + if existing_host.resource.name not in models['vlans']: + models['vlans'][existing_host.resource.name] = {} + models['vlans'][existing_host.resource.name][iface['profile_name']] = [] + for network in iface['networks']: + vlan_id = network['network']['vlan'] + is_public = network['network']['public'] + if is_public: + vlan_id = vlan_manager.get_public_vlan().vlan + vlan = Vlan(vlan_id=vlan_id, tagged=network['tagged'], public=is_public) + models['vlans'][existing_host.resource.name][iface['profile_name']].append(vlan) + bundle.xml = xmlData + self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models) + + # serialize and deserialize xml from mxGraph + def parseXml(self, xmlString): + parent_nets = {} # map network ports to networks + networks = {} # maps net id to network object + hosts = {} # cotains id -> hosts, each containing interfaces, referencing networks + interfaces = {} # maps id -> interface + xmlDom = minidom.parseString(xmlString) + root = xmlDom.documentElement.firstChild + connections = [] + netids = {} + untagged_ints = {} + for cell in root.childNodes: + cellId = cell.getAttribute('id') + + if cell.getAttribute("edge"): + #cell is a network connection + escaped_json_str = cell.getAttribute("value") + json_str = escaped_json_str.replace('"', '"') + attributes = json.loads(json_str) + tagged = attributes['tagged'] + interface = None + network = None + src = cell.getAttribute("source") + tgt = cell.getAttribute("target") + if src in parent_nets: + #src is a network port + network = networks[parent_nets[src]] + if tgt in untagged_ints and tagged==False: + raise InvalidVlanConfigurationException("More than one untagged vlan on an interface") + interface = interfaces[tgt] + untagged_ints[tgt] = True + else: + network = networks[parent_nets[tgt]] + if src in untagged_ints and tagged==False: + raise InvalidVlanConfigurationException("More than one untagged vlan on an interface") + interface = interfaces[src] + untagged_ints[src] = True + interface['networks'].append({"network": network, "tagged": tagged}) + + elif "network" in cellId: # cell is a network + escaped_json_str = cell.getAttribute("value") + json_str = escaped_json_str.replace('"', '"') + net_info = json.loads(json_str) + nid = net_info['vlan_id'] + public = net_info['public'] + try: + int_netid = int(nid) + assert public or int_netid > 1, "Net id is 1 or lower" + assert int_netid < 4095, "Net id is 4095 or greater" + except Exception as e: + raise InvalidVlanConfigurationException("VLAN ID is not an integer more than 1 and less than 4095") + if nid in netids: + raise NetworkExistsException("Non unique network id found") + else: + pass + network = {"name": net_info['name'], "vlan": net_info['vlan_id'], "public": public} + netids[net_info['vlan_id']] = True + networks[cellId] = network + + elif "host" in cellId: # cell is a host/machine + #TODO gather host info + cell_json_str = cell.getAttribute("value") + cell_json = json.loads(cell_json_str) + host = {"interfaces": [], "name": cellId, "profile_name": cell_json['name']} + hosts[cellId] = host + + elif cell.hasAttribute("parent"): + parentId = cell.getAttribute('parent') + if "network" in parentId: + parent_nets[cellId] = parentId + elif "host" in parentId: + #TODO gather iface info + cell_json_str = cell.getAttribute("value") + cell_json = json.loads(cell_json_str) + iface = {"name": cellId, "networks": [], "profile_name": cell_json['name']} + hosts[parentId]['interfaces'].append(cellId) + interfaces[cellId] = iface + return hosts, interfaces + + +class Resource_Meta_Info(WorkflowStep): + template = 'resource/steps/meta_info.html' + title = "Extra Info" + description = "Please fill out the rest of the information about your resource" + short_title = "pod info" + + def get_context(self): + context = super(Resource_Meta_Info, self).get_context() + name = "" + desc = "" + bundle = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}).get("bundle", False) + if bundle and bundle.name: + name = bundle.name + desc = bundle.description + context['form'] = ResourceMetaForm(initial={"bundle_name": name, "bundle_description": desc}) + return context + + def post_render(self, request): + form = ResourceMetaForm(request.POST) + if form.is_valid(): + models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) + name = form.cleaned_data['bundle_name'] + desc = form.cleaned_data['bundle_description'] + bundle = models.get("bundle", GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER))) + bundle.name = name + bundle.description = desc + models['bundle'] = bundle + self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models) + confirm = self.repo_get(self.repo.CONFIRMATION) + if "resource" not in confirm: + confirm['resource'] = {} + confirm_info = confirm['resource'] + confirm_info["name"] = name + tmp = desc + if len(tmp) > 60: + tmp = tmp[:60] + "..." + confirm_info["description"] = tmp + self.repo_put(self.repo.CONFIRMATION, confirm) + self.metastep.set_valid("Step Completed") + + else: + self.metastep.set_invalid("Please correct the fields highlighted in red to continue") + pass + return self.render(request) + + +class Host_Meta_Info(WorkflowStep): + template = "resource/steps/host_info.html" + title = "Host Info" + description = "We need a little bit of information about your chosen machines" + short_title = "host info" + + def __init__(self, *args, **kwargs): + super(Host_Meta_Info, self).__init__(*args, **kwargs) + self.formset = formset_factory(GenericHostMetaForm, extra=0) + + def get_context(self): + context = super(Host_Meta_Info, self).get_context() + GenericHostFormset = self.formset + models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) + initial_data = [] + if "hosts" not in models: + context['error'] = "Please go back and select your hosts" + else: + for host in models['hosts']: + profile = host.profile.name + name = host.resource.name + if not name: + name = "" + initial_data.append({"host_profile": profile, "host_name": name}) + context['formset'] = GenericHostFormset(initial=initial_data) + return context + + def post_render(self, request): + models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}) + if 'hosts' not in models: + models['hosts'] = [] + hosts = models['hosts'] + i = 0 + confirm_hosts = [] + GenericHostFormset = self.formset + formset = GenericHostFormset(request.POST) + if formset.is_valid(): + for form in formset: + host = hosts[i] + host.resource.name = form.cleaned_data['host_name'] + i += 1 + confirm_hosts.append({"name": host.resource.name, "profile": host.profile.name}) + models['hosts'] = hosts + self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models) + confirm = self.repo_get(self.repo.CONFIRMATION, {}) + if "resource" not in confirm: + confirm['resource'] = {} + confirm['resource']['hosts'] = confirm_hosts + self.repo_put(self.repo.CONFIRMATION, confirm) + else: + pass + return self.render(request) -- cgit 1.2.3-korg