Lab as a Service 2.0
[pharos-tools.git] / dashboard / src / workflow / resource_bundle_workflow.py
1 ##############################################################################
2 # Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
3 #
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 ##############################################################################
9
10
11 from django.shortcuts import render
12 from django.forms import formset_factory
13
14 import json
15 import re
16 from xml.dom import minidom
17
18 from workflow.models import WorkflowStep
19 from workflow.forms import *
20 from resource_inventory.models import *
21 from dashboard.exceptions import *
22
23 import logging
24 logger = logging.getLogger(__name__)
25
26
27 class Define_Hardware(WorkflowStep):
28
29     template = 'resource/steps/define_hardware.html'
30     title = "Define Hardware"
31     description = "Choose the type and amount of machines you want"
32     short_title = "hosts"
33     def get_context(self):
34         context = super(Define_Hardware, self).get_context()
35         selection_data = {"hosts": {}, "labs": {}}
36         models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
37         hosts = models.get("hosts", [])
38         for host in hosts:
39             profile_id = "host_" + str(host.profile.id)
40             if profile_id not in selection_data['hosts']:
41                 selection_data['hosts'][profile_id] = []
42             selection_data['hosts'][profile_id].append({"host_name": host.resource.name, "class": profile_id})
43
44         if models.get("bundle", GenericResourceBundle()).lab:
45             selection_data['labs'] = {"lab_" + str(models.get("bundle").lab.lab_user.id): "true"}
46
47         form = HardwareDefinitionForm(
48                 selection_data=selection_data
49                 )
50         context['form'] = form
51         return context
52
53     def render(self, request):
54         self.context = self.get_context()
55         return render(request, self.template, self.context)
56
57
58     def update_models(self, data):
59         data = json.loads(data['filter_field'])
60         models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
61         models['hosts'] = []  # This will always clear existing data when this step changes
62         models['interfaces'] = {}
63         if "bundle" not in models:
64             models['bundle'] = GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER))
65         host_data = data['hosts']
66         names = {}
67         for host_dict in host_data:
68             id = host_dict['class']
69             # bit of formatting
70             id = int(id.split("_")[-1])
71             profile = HostProfile.objects.get(id=id)
72             # instantiate genericHost and store in repo
73             name = host_dict['host_name']
74             if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})", name):
75                 raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
76             if name in names:
77                 raise NonUniqueHostnameException("All hosts must have unique names")
78             names[name] = True
79             genericResource = GenericResource(bundle=models['bundle'], name=name)
80             genericHost = GenericHost(profile=profile, resource=genericResource)
81             models['hosts'].append(genericHost)
82             for interface_profile in profile.interfaceprofile.all():
83                 genericInterface = GenericInterface(profile=interface_profile, host=genericHost)
84                 if genericHost.resource.name not in models['interfaces']:
85                     models['interfaces'][genericHost.resource.name] = []
86                 models['interfaces'][genericHost.resource.name].append(genericInterface)
87
88         # add selected lab to models
89         for lab_dict in data['labs']:
90             if list(lab_dict.values())[0]:  # True for lab the user selected
91                 lab_user_id = int(list(lab_dict.keys())[0].split("_")[-1])
92                 models['bundle'].lab = Lab.objects.get(lab_user__id=lab_user_id)
93                 break # if somehow we get two 'true' labs, we only use one
94
95         # return to repo
96         self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models)
97
98
99     def update_confirmation(self):
100         confirm = self.repo_get(self.repo.CONFIRMATION, {})
101         if "resource" not in confirm:
102             confirm['resource'] = {}
103         confirm['resource']['hosts'] = []
104         models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {"hosts": []})
105         for host in models['hosts']:
106             host_dict = {"name": host.resource.name, "profile": host.profile.name}
107             confirm['resource']['hosts'].append(host_dict)
108         if "lab" in models:
109             confirm['resource']['lab'] = models['lab'].lab_user.username
110         self.repo_put(self.repo.CONFIRMATION, confirm)
111
112
113     def post_render(self, request):
114         try:
115             self.form = HardwareDefinitionForm(request.POST)
116             if self.form.is_valid():
117                 self.update_models(self.form.cleaned_data)
118                 self.update_confirmation()
119                 self.metastep.set_valid("Step Completed")
120             else:
121                 self.metastep.set_invalid("Please complete the fields highlighted in red to continue")
122                 pass
123         except Exception as e:
124             self.metastep.set_invalid(str(e))
125         self.context = self.get_context()
126         return render(request, self.template, self.context)
127
128 class Define_Nets(WorkflowStep):
129     template = 'resource/steps/pod_definition.html'
130     title = "Define Networks"
131     description = "Use the tool below to draw the network topology of your POD"
132     short_title = "networking"
133     form = NetworkDefinitionForm
134
135     def get_vlans(self):
136         vlans = self.repo_get(self.repo.VLANS)
137         if vlans:
138             return vlans
139         # try to grab some vlans from lab
140         models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
141         if "bundle" not in models:
142             return None
143         lab = models['bundle'].lab
144         if lab is None or lab.vlan_manager is None:
145             return None
146         try:
147             vlans = lab.vlan_manager.get_vlan(count=lab.vlan_manager.block_size)
148             self.repo_put(self.repo.VLANS, vlans)
149             return vlans
150         except Exception as e:
151             return None
152
153     def get_context(self):
154         # TODO: render *primarily* on hosts in repo models
155         context = super(Define_Nets, self).get_context()
156         context['form'] = NetworkDefinitionForm()
157         try:
158             context['hosts'] = []
159             models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
160             vlans = self.get_vlans()
161             if vlans:
162                 context['vlans'] = vlans
163             hosts = models.get("hosts", [])
164             hostlist = self.repo_get(self.repo.GRB_LAST_HOSTLIST, None)
165             added_list = []
166             added_dict = {}
167             context['added_hosts'] = []
168             if not hostlist is None:
169                 new_hostlist = []
170                 for host in models['hosts']:
171                     intcount = host.profile.interfaceprofile.count()
172                     new_hostlist.append(str(host.resource.name) + "*" + str(host.profile) + "&" + str(intcount))
173                 context['removed_hosts'] = list(set(hostlist) - set(new_hostlist))
174                 added_list = list(set(new_hostlist) - set(hostlist))
175                 for hoststr in added_list:
176                     key = hoststr.split("*")[0]
177                     added_dict[key] = hoststr
178             for generic_host in hosts:
179                 host_profile = generic_host.profile
180                 host = {}
181                 host['id'] = generic_host.resource.name
182                 host['interfaces'] = []
183                 for iface in host_profile.interfaceprofile.all():
184                     host['interfaces'].append({
185                             "name": iface.name,
186                             "description": "speed: " + str(iface.speed) + "M\ntype: " + iface.nic_type})
187                 host['value'] = {"name": generic_host.resource.name}
188                 host['value']['description'] = generic_host.profile.description
189                 context['hosts'].append(json.dumps(host))
190                 if host['id'] in added_dict:
191                     context['added_hosts'].append(json.dumps(host))
192             bundle = models.get("bundle", False)
193             if bundle and bundle.xml:
194                 context['xml'] = bundle.xml
195             else:
196                 context['xml'] = False
197
198         except Exception as e:
199             pass
200         return context
201
202     def post_render(self, request):
203         models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
204         if 'hosts' in models:
205             hostlist = []
206             for host in models['hosts']:
207                 intcount = host.profile.interfaceprofile.count()
208                 hostlist.append(str(host.resource.name) + "*" + str(host.profile) + "&" + str(intcount))
209             self.repo_put(self.repo.GRB_LAST_HOSTLIST, hostlist)
210         try:
211             xmlData = request.POST.get("xml")
212             self.updateModels(xmlData)
213             # update model with xml
214             self.metastep.set_valid("Networks applied successfully")
215         except Exception as e:
216             self.metastep.set_invalid("An error occurred when applying networks")
217         return self.render(request)
218
219     def updateModels(self, xmlData):
220         models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
221         models["vlans"] = {}
222         given_hosts, interfaces = self.parseXml(xmlData)
223         vlan_manager = models['bundle'].lab.vlan_manager
224         existing_host_list = models.get("hosts", [])
225         existing_hosts = {} # maps id to host
226         for host in existing_host_list:
227             existing_hosts[host.resource.name] = host
228
229         bundle = models.get("bundle", GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER)))
230
231         for hostid, given_host in given_hosts.items():
232             existing_host = existing_hosts[hostid[5:]]
233
234             for ifaceId in given_host['interfaces']:
235                 iface = interfaces[ifaceId]
236                 iface_profile = existing_host.profile.interfaceprofile.get(name=iface['profile_name'])
237                 if existing_host.resource.name not in models['vlans']:
238                     models['vlans'][existing_host.resource.name] = {}
239                 models['vlans'][existing_host.resource.name][iface['profile_name']] = []
240                 for network in iface['networks']:
241                     vlan_id = network['network']['vlan']
242                     is_public = network['network']['public']
243                     if is_public:
244                         vlan_id = vlan_manager.get_public_vlan().vlan
245                     vlan = Vlan(vlan_id=vlan_id, tagged=network['tagged'], public=is_public)
246                     models['vlans'][existing_host.resource.name][iface['profile_name']].append(vlan)
247         bundle.xml = xmlData
248         self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models)
249
250     # serialize and deserialize xml from mxGraph
251     def parseXml(self, xmlString):
252         parent_nets = {}  # map network ports to networks
253         networks = {}  # maps net id to network object
254         hosts = {}  # cotains id -> hosts, each containing interfaces, referencing networks
255         interfaces = {}  # maps id -> interface
256         xmlDom = minidom.parseString(xmlString)
257         root = xmlDom.documentElement.firstChild
258         connections = []
259         netids = {}
260         untagged_ints = {}
261         for cell in root.childNodes:
262             cellId = cell.getAttribute('id')
263
264             if cell.getAttribute("edge"):
265                 #cell is a network connection
266                 escaped_json_str = cell.getAttribute("value")
267                 json_str = escaped_json_str.replace('"', '"')
268                 attributes = json.loads(json_str)
269                 tagged = attributes['tagged']
270                 interface = None
271                 network = None
272                 src = cell.getAttribute("source")
273                 tgt = cell.getAttribute("target")
274                 if src in parent_nets:
275                     #src is a network port
276                     network = networks[parent_nets[src]]
277                     if tgt in untagged_ints and tagged==False:
278                         raise InvalidVlanConfigurationException("More than one untagged vlan on an interface")
279                     interface = interfaces[tgt]
280                     untagged_ints[tgt] = True
281                 else:
282                     network = networks[parent_nets[tgt]]
283                     if src in untagged_ints and tagged==False:
284                         raise InvalidVlanConfigurationException("More than one untagged vlan on an interface")
285                     interface = interfaces[src]
286                     untagged_ints[src] = True
287                 interface['networks'].append({"network": network, "tagged": tagged})
288
289             elif "network" in cellId:  # cell is a network
290                 escaped_json_str = cell.getAttribute("value")
291                 json_str = escaped_json_str.replace('"', '"')
292                 net_info = json.loads(json_str)
293                 nid = net_info['vlan_id']
294                 public = net_info['public']
295                 try:
296                     int_netid = int(nid)
297                     assert public or int_netid > 1, "Net id is 1 or lower"
298                     assert int_netid < 4095, "Net id is 4095 or greater"
299                 except Exception as e:
300                     raise InvalidVlanConfigurationException("VLAN ID is not an integer more than 1 and less than 4095")
301                 if nid in netids:
302                     raise NetworkExistsException("Non unique network id found")
303                 else:
304                     pass
305                 network = {"name": net_info['name'], "vlan": net_info['vlan_id'], "public": public}
306                 netids[net_info['vlan_id']] = True
307                 networks[cellId] = network
308
309             elif "host" in cellId:  # cell is a host/machine
310                 #TODO gather host info
311                 cell_json_str = cell.getAttribute("value")
312                 cell_json = json.loads(cell_json_str)
313                 host = {"interfaces": [], "name": cellId, "profile_name": cell_json['name']}
314                 hosts[cellId] = host
315
316             elif cell.hasAttribute("parent"):
317                 parentId = cell.getAttribute('parent')
318                 if "network" in parentId:
319                     parent_nets[cellId] = parentId
320                 elif "host" in parentId:
321                     #TODO gather iface info
322                     cell_json_str = cell.getAttribute("value")
323                     cell_json = json.loads(cell_json_str)
324                     iface = {"name": cellId, "networks": [], "profile_name": cell_json['name']}
325                     hosts[parentId]['interfaces'].append(cellId)
326                     interfaces[cellId] = iface
327         return hosts, interfaces
328
329
330 class Resource_Meta_Info(WorkflowStep):
331     template = 'resource/steps/meta_info.html'
332     title = "Extra Info"
333     description = "Please fill out the rest of the information about your resource"
334     short_title = "pod info"
335
336     def get_context(self):
337         context = super(Resource_Meta_Info, self).get_context()
338         name = ""
339         desc = ""
340         bundle = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}).get("bundle", False)
341         if bundle and bundle.name:
342             name = bundle.name
343             desc = bundle.description
344         context['form'] = ResourceMetaForm(initial={"bundle_name": name, "bundle_description": desc})
345         return context
346
347     def post_render(self, request):
348         form = ResourceMetaForm(request.POST)
349         if form.is_valid():
350             models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
351             name = form.cleaned_data['bundle_name']
352             desc = form.cleaned_data['bundle_description']
353             bundle = models.get("bundle", GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER)))
354             bundle.name = name
355             bundle.description = desc
356             models['bundle'] = bundle
357             self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models)
358             confirm = self.repo_get(self.repo.CONFIRMATION)
359             if "resource" not in confirm:
360                 confirm['resource'] = {}
361             confirm_info = confirm['resource']
362             confirm_info["name"] = name
363             tmp = desc
364             if len(tmp) > 60:
365                 tmp = tmp[:60] + "..."
366             confirm_info["description"] = tmp
367             self.repo_put(self.repo.CONFIRMATION, confirm)
368             self.metastep.set_valid("Step Completed")
369
370         else:
371             self.metastep.set_invalid("Please correct the fields highlighted in red to continue")
372             pass
373         return self.render(request)
374
375
376 class Host_Meta_Info(WorkflowStep):
377     template = "resource/steps/host_info.html"
378     title = "Host Info"
379     description = "We need a little bit of information about your chosen machines"
380     short_title = "host info"
381
382     def __init__(self, *args, **kwargs):
383         super(Host_Meta_Info, self).__init__(*args, **kwargs)
384         self.formset = formset_factory(GenericHostMetaForm, extra=0)
385
386     def get_context(self):
387         context = super(Host_Meta_Info, self).get_context()
388         GenericHostFormset = self.formset
389         models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
390         initial_data = []
391         if "hosts" not in models:
392             context['error'] = "Please go back and select your hosts"
393         else:
394             for host in models['hosts']:
395                 profile = host.profile.name
396                 name = host.resource.name
397                 if not name:
398                     name = ""
399                 initial_data.append({"host_profile": profile, "host_name": name})
400         context['formset'] = GenericHostFormset(initial=initial_data)
401         return context
402
403     def post_render(self, request):
404         models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
405         if 'hosts' not in models:
406             models['hosts'] = []
407         hosts = models['hosts']
408         i = 0
409         confirm_hosts = []
410         GenericHostFormset = self.formset
411         formset = GenericHostFormset(request.POST)
412         if formset.is_valid():
413             for form in formset:
414                 host = hosts[i]
415                 host.resource.name = form.cleaned_data['host_name']
416                 i += 1
417                 confirm_hosts.append({"name": host.resource.name, "profile": host.profile.name})
418             models['hosts'] = hosts
419             self.repo_put(self.repo.GRESOURCE_BUNDLE_MODELS, models)
420             confirm = self.repo_get(self.repo.CONFIRMATION, {})
421             if "resource" not in confirm:
422                 confirm['resource'] = {}
423             confirm['resource']['hosts'] = confirm_hosts
424             self.repo_put(self.repo.CONFIRMATION, confirm)
425         else:
426             pass
427         return self.render(request)