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/resource_inventory/__init__.py | 8 + dashboard/src/resource_inventory/admin.py | 29 ++ dashboard/src/resource_inventory/apps.py | 14 + .../resource_inventory/migrations/0001_initial.py | 328 +++++++++++++++++++++ .../migrations/0002_auto_20180919_1459.py | 18 ++ .../migrations/0003_vlan_public.py | 18 ++ .../src/resource_inventory/migrations/__init__.py | 0 dashboard/src/resource_inventory/models.py | 299 +++++++++++++++++++ .../src/resource_inventory/resource_manager.py | 197 +++++++++++++ .../src/resource_inventory/tests/test_managers.py | 236 +++++++++++++++ .../src/resource_inventory/tests/test_models.py | 162 ++++++++++ dashboard/src/resource_inventory/urls.py | 34 +++ dashboard/src/resource_inventory/views.py | 24 ++ 13 files changed, 1367 insertions(+) create mode 100644 dashboard/src/resource_inventory/__init__.py create mode 100644 dashboard/src/resource_inventory/admin.py create mode 100644 dashboard/src/resource_inventory/apps.py create mode 100644 dashboard/src/resource_inventory/migrations/0001_initial.py create mode 100644 dashboard/src/resource_inventory/migrations/0002_auto_20180919_1459.py create mode 100644 dashboard/src/resource_inventory/migrations/0003_vlan_public.py create mode 100644 dashboard/src/resource_inventory/migrations/__init__.py create mode 100644 dashboard/src/resource_inventory/models.py create mode 100644 dashboard/src/resource_inventory/resource_manager.py create mode 100644 dashboard/src/resource_inventory/tests/test_managers.py create mode 100644 dashboard/src/resource_inventory/tests/test_models.py create mode 100644 dashboard/src/resource_inventory/urls.py create mode 100644 dashboard/src/resource_inventory/views.py (limited to 'dashboard/src/resource_inventory') diff --git a/dashboard/src/resource_inventory/__init__.py b/dashboard/src/resource_inventory/__init__.py new file mode 100644 index 0000000..f903394 --- /dev/null +++ b/dashboard/src/resource_inventory/__init__.py @@ -0,0 +1,8 @@ +############################################################################## +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, 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 +############################################################################## diff --git a/dashboard/src/resource_inventory/admin.py b/dashboard/src/resource_inventory/admin.py new file mode 100644 index 0000000..222877a --- /dev/null +++ b/dashboard/src/resource_inventory/admin.py @@ -0,0 +1,29 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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.contrib import admin + +from resource_inventory.models import * + +profiles = [HostProfile, InterfaceProfile, DiskProfile, CpuProfile, RamProfile] + +admin.site.register(profiles) + +generics = [GenericResourceBundle, GenericResource, GenericHost, GenericPod, GenericInterface] + +admin.site.register(generics) + +physical = [Host, Interface, Network, Vlan, ResourceBundle] + +admin.site.register(physical) + +config = [Scenario, Installer, Opsys, ConfigBundle, OPNFVConfig, OPNFVRole, Image, HostConfiguration] + +admin.site.register(config) diff --git a/dashboard/src/resource_inventory/apps.py b/dashboard/src/resource_inventory/apps.py new file mode 100644 index 0000000..79768a7 --- /dev/null +++ b/dashboard/src/resource_inventory/apps.py @@ -0,0 +1,14 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt 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.apps import AppConfig + + +class ResourcesConfig(AppConfig): + name = 'hwresource' diff --git a/dashboard/src/resource_inventory/migrations/0001_initial.py b/dashboard/src/resource_inventory/migrations/0001_initial.py new file mode 100644 index 0000000..d01e8e7 --- /dev/null +++ b/dashboard/src/resource_inventory/migrations/0001_initial.py @@ -0,0 +1,328 @@ +# Generated by Django 2.1 on 2018-09-14 14:48 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('account', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='ConfigBundle', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200, unique=True)), + ('description', models.CharField(default='', max_length=1000)), + ], + ), + migrations.CreateModel( + name='CpuProfile', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('cores', models.IntegerField()), + ('architecture', models.CharField(choices=[('x86_64', 'x86_64'), ('aarch64', 'aarch64')], max_length=50)), + ('cpus', models.IntegerField()), + ('cflags', models.TextField(null=True)), + ], + ), + migrations.CreateModel( + name='DiskProfile', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('size', models.IntegerField()), + ('media_type', models.CharField(choices=[('SSD', 'SSD'), ('HDD', 'HDD')], max_length=50)), + ('name', models.CharField(max_length=50)), + ('rotation', models.IntegerField(default=0)), + ('interface', models.CharField(choices=[('sata', 'sata'), ('sas', 'sas'), ('ssd', 'ssd'), ('nvme', 'nvme'), ('scsi', 'scsi'), ('iscsi', 'iscsi')], default='sata', max_length=50)), + ], + ), + migrations.CreateModel( + name='GenericHost', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ], + ), + migrations.CreateModel( + name='GenericInterface', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='generic_interfaces', to='resource_inventory.GenericHost')), + ], + ), + migrations.CreateModel( + name='GenericResource', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, validators=[django.core.validators.RegexValidator(message='Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)', regex='(?=^.{1,253}$)(?=(^([A-Za-z0-9\\-\\_]{1,62}\\.)*[A-Za-z0-9\\-\\_]{1,63}$))')])), + ], + ), + migrations.CreateModel( + name='GenericResourceBundle', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=300, unique=True)), + ('xml', models.TextField()), + ('description', models.CharField(default='', max_length=1000)), + ('lab', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='account.Lab')), + ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Host', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('booked', models.BooleanField(default=False)), + ('name', models.CharField(max_length=200, unique=True)), + ('labid', models.CharField(default='default_id', max_length=200)), + ('working', models.BooleanField(default=True)), + ('vendor', models.CharField(default='unknown', max_length=100)), + ('model', models.CharField(default='unknown', max_length=150)), + ], + ), + migrations.CreateModel( + name='HostConfiguration', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('bundle', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='hostConfigurations', to='resource_inventory.ConfigBundle')), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='configuration', to='resource_inventory.GenericHost')), + ], + ), + migrations.CreateModel( + name='HostProfile', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('host_type', models.PositiveSmallIntegerField()), + ('name', models.CharField(max_length=200, unique=True)), + ('description', models.TextField()), + ('labs', models.ManyToManyField(related_name='hostprofiles', to='account.Lab')), + ], + ), + migrations.CreateModel( + name='Image', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('lab_id', models.IntegerField()), + ('name', models.CharField(max_length=200)), + ('public', models.BooleanField(default=True)), + ('description', models.TextField()), + ('from_lab', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.Lab')), + ('host_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.HostProfile')), + ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Installer', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200)), + ], + ), + migrations.CreateModel( + name='Interface', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('mac_address', models.CharField(max_length=17)), + ('bus_address', models.CharField(max_length=50)), + ('name', models.CharField(default='eth0', max_length=100)), + ], + ), + migrations.CreateModel( + name='InterfaceProfile', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('speed', models.IntegerField()), + ('name', models.CharField(max_length=100)), + ('nic_type', models.CharField(choices=[('onboard', 'onboard'), ('pcie', 'pcie')], default='onboard', max_length=50)), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='interfaceprofile', to='resource_inventory.HostProfile')), + ], + ), + migrations.CreateModel( + name='Network', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('vlan_id', models.IntegerField()), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='OPNFVConfig', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('bundle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opnfv_config', to='resource_inventory.ConfigBundle')), + ('installer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Installer')), + ], + ), + migrations.CreateModel( + name='OPNFVRole', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200)), + ('description', models.TextField()), + ], + ), + migrations.CreateModel( + name='Opsys', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100)), + ('sup_installers', models.ManyToManyField(blank=True, to='resource_inventory.Installer')), + ], + ), + migrations.CreateModel( + name='RamProfile', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('amount', models.IntegerField()), + ('channels', models.IntegerField()), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='ramprofile', to='resource_inventory.HostProfile')), + ], + ), + migrations.CreateModel( + name='ResourceBundle', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('template', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='resource_inventory.GenericResourceBundle')), + ], + ), + migrations.CreateModel( + name='Scenario', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=300)), + ], + ), + migrations.CreateModel( + name='Vlan', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('vlan_id', models.IntegerField()), + ('tagged', models.BooleanField()), + ], + ), + migrations.CreateModel( + name='GenericPod', + fields=[ + ('genericresource_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='resource_inventory.GenericResource')), + ], + bases=('resource_inventory.genericresource',), + ), + migrations.AddField( + model_name='opnfvconfig', + name='scenario', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Scenario'), + ), + migrations.AddField( + model_name='interface', + name='config', + field=models.ManyToManyField(to='resource_inventory.Vlan'), + ), + migrations.AddField( + model_name='interface', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='resource_inventory.Host'), + ), + migrations.AddField( + model_name='installer', + name='sup_scenarios', + field=models.ManyToManyField(blank=True, to='resource_inventory.Scenario'), + ), + migrations.AddField( + model_name='hostconfiguration', + name='image', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='resource_inventory.Image'), + ), + migrations.AddField( + model_name='hostconfiguration', + name='opnfvRole', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='resource_inventory.OPNFVRole'), + ), + migrations.AddField( + model_name='host', + name='bundle', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hosts', to='resource_inventory.ResourceBundle'), + ), + migrations.AddField( + model_name='host', + name='config', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='configuration', to='resource_inventory.HostConfiguration'), + ), + migrations.AddField( + model_name='host', + name='lab', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.Lab'), + ), + migrations.AddField( + model_name='host', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.HostProfile'), + ), + migrations.AddField( + model_name='host', + name='template', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.GenericHost'), + ), + migrations.AddField( + model_name='genericresource', + name='bundle', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='generic_resources', to='resource_inventory.GenericResourceBundle'), + ), + migrations.AddField( + model_name='genericinterface', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='resource_inventory.InterfaceProfile'), + ), + migrations.AddField( + model_name='genericinterface', + name='vlans', + field=models.ManyToManyField(to='resource_inventory.Vlan'), + ), + migrations.AddField( + model_name='generichost', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='resource_inventory.HostProfile'), + ), + migrations.AddField( + model_name='generichost', + name='resource', + field=models.OneToOneField(on_delete=django.db.models.deletion.DO_NOTHING, related_name='generic_host', to='resource_inventory.GenericResource'), + ), + migrations.AddField( + model_name='diskprofile', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='storageprofile', to='resource_inventory.HostProfile'), + ), + migrations.AddField( + model_name='cpuprofile', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cpuprofile', to='resource_inventory.HostProfile'), + ), + migrations.AddField( + model_name='configbundle', + name='bundle', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.GenericResourceBundle'), + ), + migrations.AddField( + model_name='configbundle', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='genericpod', + name='hosts', + field=models.ManyToManyField(to='resource_inventory.GenericHost'), + ), + migrations.AddField( + model_name='genericpod', + name='networks', + field=models.ManyToManyField(to='resource_inventory.Network'), + ), + ] diff --git a/dashboard/src/resource_inventory/migrations/0002_auto_20180919_1459.py b/dashboard/src/resource_inventory/migrations/0002_auto_20180919_1459.py new file mode 100644 index 0000000..80c9e6f --- /dev/null +++ b/dashboard/src/resource_inventory/migrations/0002_auto_20180919_1459.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1 on 2018-09-19 14:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('resource_inventory', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='hostprofile', + name='host_type', + field=models.PositiveSmallIntegerField(default=0), + ), + ] diff --git a/dashboard/src/resource_inventory/migrations/0003_vlan_public.py b/dashboard/src/resource_inventory/migrations/0003_vlan_public.py new file mode 100644 index 0000000..07dc647 --- /dev/null +++ b/dashboard/src/resource_inventory/migrations/0003_vlan_public.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1 on 2018-09-26 14:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('resource_inventory', '0002_auto_20180919_1459'), + ] + + operations = [ + migrations.AddField( + model_name='vlan', + name='public', + field=models.BooleanField(default=False), + ), + ] diff --git a/dashboard/src/resource_inventory/migrations/__init__.py b/dashboard/src/resource_inventory/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/src/resource_inventory/models.py b/dashboard/src/resource_inventory/models.py new file mode 100644 index 0000000..b71748e --- /dev/null +++ b/dashboard/src/resource_inventory/models.py @@ -0,0 +1,299 @@ +############################################################################## +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, 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.contrib.auth.models import User +from django.db import models +from django.core.validators import RegexValidator + +import re + +from account.models import Lab + + +# profile of resources hosted by labs +class HostProfile(models.Model): + id = models.AutoField(primary_key=True) + host_type = models.PositiveSmallIntegerField(default=0) + name = models.CharField(max_length=200, unique=True) + description = models.TextField() + labs = models.ManyToManyField(Lab, related_name="hostprofiles") + + def validate(self): + validname = re.compile("^[A-Za-z0-9\-\_\.\/\, ]+$") + if not validname.match(self.name): + return "Invalid host profile name given. Name must only use A-Z, a-z, 0-9, hyphens, underscores, dots, commas, or spaces." + else: + return None + + def __str__(self): + return self.name + + +class InterfaceProfile(models.Model): + id = models.AutoField(primary_key=True) + speed = models.IntegerField() + name = models.CharField(max_length=100) + host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='interfaceprofile') + nic_type = models.CharField(max_length=50, choices=[ + ("onboard", "onboard"), + ("pcie", "pcie") + ], default="onboard") + + def __str__(self): + return self.name + " for " + str(self.host) + + +class DiskProfile(models.Model): + id = models.AutoField(primary_key=True) + size = models.IntegerField() + media_type = models.CharField(max_length=50, choices=[ + ("SSD", "SSD"), + ("HDD", "HDD") + ]) + name = models.CharField(max_length=50) + host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='storageprofile') + rotation = models.IntegerField(default=0) + interface = models.CharField(max_length=50, choices=[ + ("sata", "sata"), + ("sas", "sas"), + ("ssd", "ssd"), + ("nvme", "nvme"), + ("scsi", "scsi"), + ("iscsi", "iscsi"), + ], default="sata") + + def __str__(self): + return self.name + " for " + str(self.host) + + +class CpuProfile(models.Model): + id = models.AutoField(primary_key=True) + cores = models.IntegerField() + architecture = models.CharField(max_length=50, choices=[ + ("x86_64", "x86_64"), + ("aarch64", "aarch64") + ]) + cpus = models.IntegerField() + host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='cpuprofile') + cflags = models.TextField(null=True) + + def __str__(self): + return str(self.architecture) + " " + str(self.cpus) + "S" + str(self.cores) + " C for " + str(self.host) + + +class RamProfile(models.Model): + id = models.AutoField(primary_key=True) + amount = models.IntegerField() + channels = models.IntegerField() + host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='ramprofile') + + def __str__(self): + return str(self.amount) + "G for " + str(self.host) + + +##Networking -- located here due to import order requirements +class Network(models.Model): + id = models.AutoField(primary_key=True) + vlan_id = models.IntegerField() + name = models.CharField(max_length=100) + + def __str__(self): + return self.name + +class Vlan(models.Model): + id = models.AutoField(primary_key=True) + vlan_id = models.IntegerField() + tagged = models.BooleanField() + public = models.BooleanField(default=False) + + def __str__(self): + return str(self.vlan_id) + ("_T" if self.tagged else "") + + +# Generic resource templates +class GenericResourceBundle(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=300, unique=True) + xml = models.TextField() + owner = models.ForeignKey(User, null=True, on_delete=models.DO_NOTHING) + lab = models.ForeignKey(Lab, null=True, on_delete=models.DO_NOTHING) + description = models.CharField(max_length=1000, default="") + + def getHosts(self): + return_hosts = [] + for genericResource in self.generic_resources.all(): + return_hosts.append(genericResource.getHost()) + + return return_hosts + + def __str__(self): + return self.name + + +class GenericResource(models.Model): + bundle = models.ForeignKey(GenericResourceBundle, related_name='generic_resources', on_delete=models.DO_NOTHING) + hostname_validchars = RegexValidator(regex='(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))', message="Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)") + name = models.CharField(max_length=200, validators=[hostname_validchars]) + + def getHost(self): + return self.generic_host + + def __str__(self): + return self.name + + def validate(self): + validname = re.compile('(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))') + if not validname.match(self.name): + return "Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)" + else: + return None + + +# Host template +class GenericHost(models.Model): + id = models.AutoField(primary_key=True) + profile = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING) + resource = models.OneToOneField(GenericResource, related_name='generic_host', on_delete=models.DO_NOTHING) + + def __str__(self): + return self.resource.name + + +# Physical, actual resources +class ResourceBundle(models.Model): + id = models.AutoField(primary_key=True) + template = models.ForeignKey(GenericResourceBundle, on_delete=models.DO_NOTHING) + + def __str__(self): + return "instance of " + str(self.template) + + +# Networking + + +class GenericInterface(models.Model): + id = models.AutoField(primary_key=True) + vlans = models.ManyToManyField(Vlan) + profile = models.ForeignKey(InterfaceProfile, on_delete=models.DO_NOTHING) + host = models.ForeignKey(GenericHost, on_delete=models.DO_NOTHING, related_name='generic_interfaces') + + def __str__(self): + return "type " + str(self.profile) + " on host " + str(self.host) + + +class Scenario(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=300) + + def __str__(self): + return self.name + +class Installer(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=200) + sup_scenarios = models.ManyToManyField(Scenario, blank=True) + + def __str__(self): + return self.name + +class Opsys(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + sup_installers = models.ManyToManyField(Installer, blank=True) + + def __str__(self): + return self.name + +class ConfigBundle(models.Model): + id = models.AutoField(primary_key=True) + owner = models.ForeignKey(User, on_delete=models.CASCADE) #consider setting to root user? + name = models.CharField(max_length=200, unique=True) + description = models.CharField(max_length=1000, default="") + bundle = models.ForeignKey(GenericResourceBundle, null=True, on_delete=models.CASCADE) + + def __str__(self): + return self.name + +class OPNFVConfig(models.Model): + id = models.AutoField(primary_key=True) + installer = models.ForeignKey(Installer, on_delete=models.CASCADE) + scenario = models.ForeignKey(Scenario, on_delete=models.CASCADE) + bundle = models.ForeignKey(ConfigBundle, related_name="opnfv_config", on_delete=models.CASCADE) + + def __str__(self): + return "OPNFV job with " + str(self.installer) + " and " + str(self.scenario) + +class OPNFVRole(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=200) + description = models.TextField() + + def __str__(self): + return self.name + +class Image(models.Model): + """ + model for representing OS images / snapshots of hosts + """ + id = models.AutoField(primary_key=True) + lab_id = models.IntegerField() # ID the lab who holds this image knows + from_lab = models.ForeignKey(Lab, on_delete=models.CASCADE) + name = models.CharField(max_length=200) + owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) + public = models.BooleanField(default=True) + host_type = models.ForeignKey(HostProfile, on_delete=models.CASCADE) #may need to change to models.SET() once images are transferrable between compatible host types + description = models.TextField() + + def __str__(self): + return self.name + +class HostConfiguration(models.Model): + """ + model to represent a complete configuration for a single + physical host + """ + id = models.AutoField(primary_key=True) + host = models.ForeignKey(GenericHost, related_name="configuration", on_delete=models.CASCADE) + image = models.ForeignKey(Image, on_delete=models.PROTECT) + bundle = models.ForeignKey(ConfigBundle, related_name="hostConfigurations", null=True, on_delete=models.CASCADE) + opnfvRole = models.ForeignKey(OPNFVRole, on_delete=models.PROTECT) #need protocol for phasing out a role if we are going to allow that to happen + + def __str__(self): + return "config with " + str(self.host) + " and image " + str(self.image) + + +# Concrete host, actual machine in a lab +class Host(models.Model): + id = models.AutoField(primary_key=True) + template = models.ForeignKey(GenericHost, on_delete=models.SET_NULL, null=True) + booked = models.BooleanField(default=False) + name = models.CharField(max_length=200, unique=True) + bundle = models.ForeignKey(ResourceBundle, related_name='hosts', on_delete=models.SET_NULL, null=True) + config = models.ForeignKey(HostConfiguration, null=True, related_name="configuration", on_delete=models.SET_NULL) + labid = models.CharField(max_length=200, default="default_id") + profile = models.ForeignKey(HostProfile, on_delete=models.CASCADE) + lab = models.ForeignKey(Lab, on_delete=models.CASCADE) + working = models.BooleanField(default=True) + vendor = models.CharField(max_length=100, default="unknown") + model = models.CharField(max_length=150, default="unknown") + + def __str__(self): + return self.name + + +class Interface(models.Model): + id = models.AutoField(primary_key=True) + mac_address = models.CharField(max_length=17) + bus_address = models.CharField(max_length=50) + name = models.CharField(max_length=100, default="eth0") + config = models.ManyToManyField(Vlan) + host = models.ForeignKey(Host, on_delete=models.CASCADE, related_name='interfaces') + + def __str__(self): + return self.mac_address + " on host " + str(self.host) diff --git a/dashboard/src/resource_inventory/resource_manager.py b/dashboard/src/resource_inventory/resource_manager.py new file mode 100644 index 0000000..cd70867 --- /dev/null +++ b/dashboard/src/resource_inventory/resource_manager.py @@ -0,0 +1,197 @@ +############################################################################## +# 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.core.exceptions import * +from django.template.loader import render_to_string + +import booking +from dashboard.exceptions import * +from resource_inventory.models import * + +class ResourceManager: + + instance = None + + def __init__(self): + pass + + @staticmethod + def getInstance(): + if ResourceManager.instance is None: + ResourceManager.instance = ResourceManager() + return ResourceManager.instance + + #public interface + def deleteResourceBundle(self, resourceBundle): + for host in Host.objects.filter(bundle=resourceBundle): + self.releaseHost(host) + resourceBundle.delete() + + def convertResourceBundle(self, genericResourceBundle, lab=None, config=None): + """ + Takes in a GenericResourceBundle and 'converts' it into a ResourceBundle + """ + resource_bundle = ResourceBundle() + resource_bundle.template = genericResourceBundle + resource_bundle.save() + + hosts = genericResourceBundle.getHosts() + + #current supported case: user creating new booking + #currently unsupported: editing existing booking + + physical_hosts = [] + + for host in hosts: + host_config=None + if config: + host_config = HostConfiguration.objects.get(bundle=config, host=host) + try: + physical_host = self.acquireHost(host, genericResourceBundle.lab.name) + except ResourceAvailabilityException: + self.fail_acquire(physical_hosts) + raise ResourceAvailabilityException("Could not provision hosts, not enough available") + try: + physical_host.bundle = resource_bundle + physical_host.template = host + physical_host.config = host_config + physical_hosts.append(physical_host) + + self.configureNetworking(physical_host) + except: + self.fail_acquire(physical_hosts) + raise ResourceProvisioningException("Network configuration failed.") + try: + physical_host.save() + except: + self.fail_acquire(physical_hosts) + raise ModelValidationException("Saving hosts failed") + + return resource_bundle + + def configureNetworking(self, host): + generic_interfaces = list(host.template.generic_interfaces.all()) + for int_num, physical_interface in enumerate(host.interfaces.all()): + generic_interface = generic_interfaces[int_num] + physical_interface.config.clear() + for vlan in generic_interface.vlans.all(): + physical_interface.config.add(vlan) + + #private interface + def acquireHost(self, genericHost, labName): + host_full_set = Host.objects.filter(lab__name__exact=labName, profile=genericHost.profile) + if not host_full_set.first(): + raise ResourceExistenceException("No matching servers found") + host_set = host_full_set.filter(booked=False) + if not host_set.first(): + raise ResourceAvailabilityException("No unbooked hosts match requested hosts") + host = host_set.first() + host.booked = True + host.template = genericHost + host.save() + return host + + def releaseHost(self, host): + host.template = None + host.bundle = None + host.booked = False + host.save() + + def fail_acquire(self, hosts): + for host in hosts: + self.releaseHost(host) + + def makePDF(self, resource): + """ + fills the pod descriptor file template with info about the resource + """ + template = "dashboard/pdf.yaml" + info = {} + info['details'] = self.get_pdf_details(resource) + info['jumphost'] = self.get_pdf_jumphost(resource) + info['nodes'] = self.get_pdf_nodes(resource) + + return render_to_string(template, context=info) + + def get_pdf_details(self, resource): + details = {} + owner = "Anon" + email = "email@mail.com" + resource_lab = resource.template.lab + lab = resource_lab.name + location = resource_lab.location + pod_type = "development" + link = "https://wiki.opnfv.org/display/INF/Pharos+Laas" + + try: + # try to get more specific info that may fail, we dont care if it does + booking_owner = booking.models.Booking.objects.get(resource=resource).owner + owner = booking_owner.username + email = booking_owner.userprofile.email_addr + except Exception as e: + pass + + details['owner'] = owner + details['email'] = email + details['lab'] = lab + details['location'] = location + details['type'] = pod_type + details['link'] = link + + return details + + def get_pdf_jumphost(self, resource): + jumphost = Host.objects.get(bundle=resource, config__opnfvRole__name__iexact="jumphost") + return self.get_pdf_host(jumphost) + + def get_pdf_nodes(self, resource): + pdf_nodes = [] + nodes = Host.objects.filter(bundle=resource).exclude(config__opnfvRole__name__iexact="jumphost") + for node in nodes: + pdf_nodes.append(self.get_pdf_host(node)) + + return pdf_nodes + + + def get_pdf_host(self, host): + host_info = {} + host_info['name'] = host.template.resource.name + host_info['node'] = {} + host_info['node']['type'] = "baremetal" + host_info['node']['vendor'] = host.vendor + host_info['node']['model'] = host.model + host_info['node']['arch'] = host.profile.cpuprofile.first().architecture + host_info['node']['cpus'] = host.profile.cpuprofile.first().cpus + host_info['node']['cores'] = host.profile.cpuprofile.first().cores + cflags = host.profile.cpuprofile.first().cflags + if cflags and cflags.strip(): + host_info['node']['cpu_cflags'] = cflags + host_info['node']['memory'] = str(host.profile.ramprofile.first().amount) + "G" + host_info['disks'] = [] + for disk in host.profile.storageprofile.all(): + disk_info = {} + disk_info['name'] = disk.name + disk_info['capacity'] = str(disk.size) + "G" + disk_info['type'] = disk.media_type + disk_info['interface'] = disk.interface + disk_info['rotation'] = disk.rotation + host_info['disks'].append(disk_info) + + host_info['interfaces'] = [] + for interface in host.interfaces.all(): + iface_info = {} + iface_info['name'] = interface.name + iface_info['address'] = "unknown" + iface_info['mac_address'] = interface.mac_address + vlans = "|".join([str(vlan.vlan_id) for vlan in interface.config.all()]) + iface_info['vlans'] = vlans + host_info['interfaces'].append(iface_info) + + return host_info diff --git a/dashboard/src/resource_inventory/tests/test_managers.py b/dashboard/src/resource_inventory/tests/test_managers.py new file mode 100644 index 0000000..5a13b2e --- /dev/null +++ b/dashboard/src/resource_inventory/tests/test_managers.py @@ -0,0 +1,236 @@ +############################################################################## +# 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.test import TestCase +from django.contrib.auth.models import User + +from resource.inventory_manager import InventoryManager +from resource.resource_manager import ResourceManager +from resource.models import * + + +class InventoryManagerTestCase(TestCase): + + def test_singleton(self): + instance = InventoryManager.getInstance() + self.assertTrue(isinstance(instance, InventoryManager)) + self.assertTrue(instance is InventoryManager.getInstance()) + + def setUp(self): + # setup + # create lab and give it resources + user = User.objects.create(username="username") + self.lab = Lab.objects.create( + lab_user=user, + name='test lab', + contact_email='someone@email.com', + contact_phone='dont call me' + ) + + # create hostProfile + hostProfile = HostProfile.objects.create( + host_type=0, + name='Test profile', + description='a test profile' + ) + interfaceProfile = InterfaceProfile.objects.create( + speed=1000, + name='eno3', + host=hostProfile + ) + diskProfile = DiskProfile.objects.create( + size=1000, + media_type="SSD", + name='/dev/sda', + host=hostProfile + ) + cpuProfile = CpuProfile.objects.create( + cores=96, + architecture="x86_64", + cpus=2, + host=hostProfile + ) + ramProfile = RamProfile.objects.create( + amount=256, + channels=4, + host=hostProfile + ) + + #create GenericResourceBundle + genericBundle = GenericResourceBundle.objects.create() + + self.gHost1 = GenericHost.objects.create( + bundle=genericBundle, + name='generic host 1', + profile=hostProfile + ) + self.gHost2 = GenericHost.objects.create( + bundle=genericBundle, + name='generic host 2', + profile=hostProfile + ) + + #actual resource bundle + bundle = ResourceBundle.objects.create(template=genericBundle) + + self.host1 = Host.objects.create( + template=self.gHost1, + booked=True, + name='host1', + bundle=bundle, + profile=hostProfile, + lab=self.lab + ) + + self.host2 = Host.objects.create( + template=self.gHost2, + booked=True, + name='host2', + bundle=bundle, + profile=hostProfile, + lab=self.lab + ) + + vlan1 = Vlan.objects.create(vlan_id=300, tagged=False) + vlan2 = Vlan.objects.create(vlan_id=300, tagged=False) + + iface1 = Interface.objects.create( + mac_address='00:11:22:33:44:55', + bus_address='some bus address', + switch_name='switch1', + port_name='port10', + config=vlan1, + host=self.host1 + ) + iface2 = Interface.objects.create( + mac_address='00:11:22:33:44:56', + bus_address='some bus address', + switch_name='switch1', + port_name='port12', + config=vlan2, + host=self.host2 + ) + + def test_acquire_host(self): + host = InventoryManager.getInstance().acquireHost(self.gHost1, self.lab.name) + self.assertNotEquals(host, None) + self.assertTrue(host.booked) + self.assertEqual(host.template, self.gHost1) + + def test_release_host(self): + host = InventoryManager.getInstance().acquireHost(self.gHost1, self.lab.name) + self.assertTrue(host.booked) + InventoryManager.getInstance().releaseHost(host) + self.assertFalse(host.booked) + + +class ResourceManagerTestCase(TestCase): + def test_singleton(self): + instance = ResourceManager.getInstance() + self.assertTrue(isinstance(instance, ResourceManager)) + self.assertTrue(instance is ResourceManager.getInstance()) + + def setUp(self): + # setup + # create lab and give it resources + user = User.objects.create(username="username") + self.lab = Lab.objects.create( + lab_user=user, + name='test lab', + contact_email='someone@email.com', + contact_phone='dont call me' + ) + + # create hostProfile + hostProfile = HostProfile.objects.create( + host_type=0, + name='Test profile', + description='a test profile' + ) + interfaceProfile = InterfaceProfile.objects.create( + speed=1000, + name='eno3', + host=hostProfile + ) + diskProfile = DiskProfile.objects.create( + size=1000, + media_type="SSD", + name='/dev/sda', + host=hostProfile + ) + cpuProfile = CpuProfile.objects.create( + cores=96, + architecture="x86_64", + cpus=2, + host=hostProfile + ) + ramProfile = RamProfile.objects.create( + amount=256, + channels=4, + host=hostProfile + ) + + #create GenericResourceBundle + genericBundle = GenericResourceBundle.objects.create() + + self.gHost1 = GenericHost.objects.create( + bundle=genericBundle, + name='generic host 1', + profile=hostProfile + ) + self.gHost2 = GenericHost.objects.create( + bundle=genericBundle, + name='generic host 2', + profile=hostProfile + ) + + #actual resource bundle + bundle = ResourceBundle.objects.create(template=genericBundle) + + self.host1 = Host.objects.create( + template=self.gHost1, + booked=True, + name='host1', + bundle=bundle, + profile=hostProfile, + lab=self.lab + ) + + self.host2 = Host.objects.create( + template=self.gHost2, + booked=True, + name='host2', + bundle=bundle, + profile=hostProfile, + lab=self.lab + ) + + vlan1 = Vlan.objects.create(vlan_id=300, tagged=False) + vlan2 = Vlan.objects.create(vlan_id=300, tagged=False) + + iface1 = Interface.objects.create( + mac_address='00:11:22:33:44:55', + bus_address='some bus address', + switch_name='switch1', + port_name='port10', + config=vlan1, + host=self.host1 + ) + iface2 = Interface.objects.create( + mac_address='00:11:22:33:44:56', + bus_address='some bus address', + switch_name='switch1', + port_name='port12', + config=vlan2, + host=self.host2 + ) + + def test_convert_bundle(self): + bundle = ResourceManager.getInstance().convertResoureBundle(self.genericBundle, self.lab.name) + # verify bundle configuration diff --git a/dashboard/src/resource_inventory/tests/test_models.py b/dashboard/src/resource_inventory/tests/test_models.py new file mode 100644 index 0000000..4ddedf2 --- /dev/null +++ b/dashboard/src/resource_inventory/tests/test_models.py @@ -0,0 +1,162 @@ +############################################################################## +# 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.test import TestCase +from django.contrib.auth.models import User +from account.models import Lab +from resource_inventory.models import * + + +class ConfigUtil(): + count=0 + + @staticmethod + def makeScenario(): + return Scenario.objects.create(name="testScenario") + + @staticmethod + def makeInstaller(): + inst = Installer.objects.create( + name = "testInstaller" + ) + inst.sup_scenarios = [ConfigUtil.makeScenario()] + return inst + + @staticmethod + def makeOpsys(): + os = Opsys.objects.create( + name = "test Operating System" + ) + os.sup_installers = [ConfigUtil.makeInstaller()] + return os + + @staticmethod + def makeConfigBundle(): + user = User.objects.create(username="test_user" + str(ConfigUtil.count)) + ConfigUtil.count += 1 + return ConfigBundle.objects.create( + owner = user + ) + + @staticmethod + def makeOPNFVConfig(): + installer = ConfigUtil.makeInstaller() + scenario = ConfigUtil.makeScenario() + bundle = ConfigUtil.makeConfigBundle() + return OPNFVConfig.objects.create( + installer=installer, + scenario=scenario, + bundle=bundle + ) + + @staticmethod + def makeOPNFVRole(): + return OPNFVRole.objects.create( + name="Test role", + description="This is a test role" + ) + + @staticmethod + def makeImage(): + owner = User.objects.create(username="another test user") + lab_user = User.objects.create(username="labUserForTests") + lab = Lab.objects.create( + lab_user=lab_user, + name="this is lab for testing", + contact_email="email@mail.com", + contact_phone="123-4567" + ) + + return Image.objects.create( + lab_id=0, + from_lab=lab, + name="an image for testing", + owner=owner + ) + + + @staticmethod + def makeGenericHost(): + profile = HostProfile.objects.create( + host_type=0, + name="test lab for config bundle", + description="this is a test profile" + ) + user = User.objects.create(username="test sample user 12") + bundle = GenericResourceBundle.objects.create( + name="Generic bundle for config tests", + xml="", + owner=user, + description="" + ) + + resource = GenericResource.objects.create( + bundle=bundle, + name="a test generic resource" + ) + + return GenericHost.objects.create( + profile=profile, + resource=resource + ) + + @staticmethod + def makeHostConfiguration(): + host = ConfigUtil.makeGenericHost() + image = ConfigUtil.makeImage() + bundle = ConfigUtil.makeConfigBundle() + opnfvRole = ConfigUtil.makeOPNFVRole() + return HostConfiguration.objects.create( + host=host, + image=image, + bundle=bundle, + opnfvRole=opnfvRole + ) + + +class ScenarioTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeScenario()) + +class InstallerTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeInstaller()) + +class OperatingSystemTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeOpsys()) + +class ConfigBundleTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeConfigBundle()) + +class OPNFVConfigTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeOPNFVConfig()) + +class OPNFVRoleTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeOPNFVRole()) + + +class HostConfigurationTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeHostConfiguration()) + + +class ImageTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeImage()) diff --git a/dashboard/src/resource_inventory/urls.py b/dashboard/src/resource_inventory/urls.py new file mode 100644 index 0000000..4e159ba --- /dev/null +++ b/dashboard/src/resource_inventory/urls.py @@ -0,0 +1,34 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# 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 +############################################################################## + + +"""pharos_dashboard URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import url +from resource_inventory.views import HostView + + +app_name = "resource" +urlpatterns = [ + url(r'^hosts$', HostView.as_view(), name='hosts') +] diff --git a/dashboard/src/resource_inventory/views.py b/dashboard/src/resource_inventory/views.py new file mode 100644 index 0000000..7e73006 --- /dev/null +++ b/dashboard/src/resource_inventory/views.py @@ -0,0 +1,24 @@ +############################################################################## +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, 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.views import View +from django.views.generic import TemplateView + +from resource_inventory.models import Host + +class HostView(TemplateView): + template_name = "resource/hosts.html" + + def get_context_data(self, **kwargs): + context = super(HostView, self).get_context_data(**kwargs) + hosts = Host.objects.filter(working=True) + context.update({'hosts':hosts, 'title':"Hardware Resources"}) + return context -- cgit 1.2.3-korg