This change adds a ton of comments and documentation across all the code.
Change-Id: Ifee0a2f534e8584f14b0f13af4dda8dc70eb7553
Signed-off-by: Parker Berberian <pberberian@iol.unh.edu>
--- /dev/null
+[flake8]
+ignore = E501,D10
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG=False
+# TEST should be True if you want to run some tests in your local dev environment
TEST=False
+# These configure the postgres container and
+# tell django how to access the database
+# You shouldn't really need to change these, unless
+# You want a specific user / pass on the DB
+# The POSTGRES_ vars and DB_ vars should be kept in sync, eg
+# POSTGRES_DB == DB_NAME
+# POSTGRES_USER == DB_USER
+# POSTGRES_PASSWORD == DB_PASS
POSTGRES_DB=sample_name
POSTGRES_USER=sample_user
POSTGRES_PASSWORD=sample_pass
OAUTH_CONSUMER_KEY=sample_key
OAUTH_CONSUMER_SECRET=sample_secret
+# access information for Jira
+# In addition to this, the rsa keys from your jira admin
+# need to go into src/account
JIRA_URL=sample_url
JIRA_USER_NAME=sample_jira_user
JIRA_USER_PASSWORD=sample_jira_pass
RABBITMQ_DEFAULT_USER=opnfv
RABBITMQ_DEFAULT_PASS=opnfvopnfv
-#Jenkins Build Server
+# Jenkins Build Server
JENKINS_URL=https://build.opnfv.org/ci
# Email Settings
- install docker, docker-compose
- run 'make data'
- run 'make up' to run the dashboard (or 'make dev-up' for development)
+- get the rsa.pem and rsa.pub keys from your jira admin and place them in src/account
Production will be running on port 80 by default.
Development will be running on port 8000 by default.
return key, raw
def sign(self, request, consumer, token):
- """Builds the base signature string."""
+ """Build the base signature string."""
key, raw = self.signing_base(request, consumer, token)
module_dir = os.path.dirname(__file__) # get current directory
class TimezoneMiddleware(MiddlewareMixin):
"""
+ Manage user's Timezone preference.
+
Activate the timezone from request.user.userprofile if user is authenticated,
deactivate the timezone otherwise and use default (UTC)
"""
+
def process_request(self, request):
if request.user.is_authenticated:
try:
class LabStatus(object):
+ """
+ A Poor man's enum for the status of a lab.
+
+ If everything is working fine at a lab, it is UP.
+ If it is down temporarily e.g. for maintenance, it is TEMP_DOWN
+ If its broken, its DOWN
+ """
+
UP = 0
TEMP_DOWN = 100
DOWN = 200
class UserProfile(models.Model):
+ """Extend the Django User model."""
+
user = models.OneToOneField(User, on_delete=models.CASCADE)
timezone = models.CharField(max_length=100, blank=False, default='UTC')
ssh_public_key = models.FileField(upload_to=upload_to, null=True, blank=True)
class VlanManager(models.Model):
+ """
+ Keeps track of the vlans for a lab.
+
+ Vlans are represented as indexes into a 4096 element list.
+ This list is serialized to JSON for storing in the DB.
+ """
+
# list of length 4096 containing either 0 (not available) or 1 (available)
vlans = models.TextField()
+ # list of length 4096 containing either 0 (not reserved) or 1 (reserved)
+ reserved_vlans = models.TextField()
+
block_size = models.IntegerField()
+
+ # True if the lab allows two different users to have the same private vlans
+ # if they use QinQ or a vxlan overlay, for example
allow_overlapping = models.BooleanField()
- # list of length 4096 containing either 0 (not rexerved) or 1 (reserved)
- reserved_vlans = models.TextField()
def get_vlan(self, count=1):
+ """
+ Return the ID of available vlans, but does not reserve them.
+
+ Will throw index exception if not enough vlans are available.
+ If count == 1, the return value is an int. Otherwise, it is a list of ints.
+ """
allocated = []
vlans = json.loads(self.vlans)
for i in range(count):
return allocated
def get_public_vlan(self):
+ """Return reference to an available public network without reserving it."""
return PublicNetwork.objects.filter(lab=self.lab_set.first(), in_use=False).first()
def reserve_public_vlan(self, vlan):
+ """Reserves the Public Network that has the given vlan."""
net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan, in_use=False)
net.in_use = True
net.save()
def release_public_vlan(self, vlan):
+ """Un-reserves a public network with the given vlan."""
net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan, in_use=True)
net.in_use = False
net.save()
def public_vlan_is_available(self, vlan):
+ """
+ Whether the public vlan is available.
+
+ returns true if the network with the given vlan is free to use,
+ False otherwise
+ """
net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan)
return not net.in_use
def is_available(self, vlans):
"""
+ If the vlans are available.
+
'vlans' is either a single vlan id integer or a list of integers
will return true (available) or false
"""
def release_vlans(self, vlans):
"""
+ Make the vlans available for another booking.
+
'vlans' is either a single vlan id integer or a list of integers
will make the vlans available
doesnt return a value
self.save()
def reserve_vlans(self, vlans):
+ """
+ Reserves all given vlans or throws a ValueError.
+
+ vlans can be an integer or a list of integers.
+ """
my_vlans = json.loads(self.vlans)
try:
class Lab(models.Model):
+ """
+ Model representing a Hosting Lab.
+
+ Anybody that wants to host resources for LaaS needs to have a Lab model
+ We associate hardware with Labs so we know what is available and where.
+ """
+
lab_user = models.OneToOneField(User, on_delete=models.CASCADE)
name = models.CharField(max_length=200, primary_key=True, unique=True, null=False, blank=False)
contact_email = models.EmailField(max_length=200, null=True, blank=True)
status = models.IntegerField(default=LabStatus.UP)
vlan_manager = models.ForeignKey(VlanManager, on_delete=models.CASCADE, null=True)
location = models.TextField(default="unknown")
+ # This token must apear in API requests from this lab
api_token = models.CharField(max_length=50)
description = models.CharField(max_length=240)
@staticmethod
def make_api_token():
+ """Generate random 45 character string for API token."""
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
key = ""
for i in range(45):
class PublicNetwork(models.Model):
+ """L2/L3 network that can reach the internet."""
+
vlan = models.IntegerField()
lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
in_use = models.BooleanField(default=False)
class Downtime(models.Model):
+ """
+ A Downtime event.
+
+ Labs can create Downtime objects so the dashboard can
+ alert users that the lab is down, etc
+ """
+
start = models.DateTimeField()
end = models.DateTimeField()
lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
def test_timezone_middleware(self):
"""
- The timezone should be UTC for anonymous users, for authenticated users it should be set
- to user.userprofile.timezone
+ Verify timezone is being set by Middleware.
+
+ The timezone should be UTC for anonymous users,
+ for authenticated users it should be set to user.userprofile.timezone
"""
# default
self.assertEqual(timezone.get_current_timezone_name(), 'UTC')
##############################################################################
-"""laas_dashboard URL Configuration
+"""
+laas_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/
def account_resource_view(request):
"""
+ Display a user's resources.
+
gathers a users genericResoureBundles and
turns them into displayable objects
"""
class JobStatus(object):
+ """
+ A poor man's enum for a job's status.
+
+ A job is NEW if it has not been started or recognized by the Lab
+ A job is CURRENT if it has been started by the lab but it is not yet completed
+ a job is DONE if all the tasks are complete and the booking is ready to use
+ """
+
NEW = 0
CURRENT = 100
DONE = 200
@classmethod
def get(cls, lab_name, token):
"""
+ Get a LabManager.
+
Takes in a lab name (from a url path)
returns a lab manager instance for that lab, if it exists
+ Also checks that the given API token is correct
"""
try:
lab = Lab.objects.get(name=lab_name)
class LabManager(object):
"""
- This is the class that will ultimately handle all REST calls to
- lab endpoints.
+ Handles all lab REST calls.
+
handles jobs, inventory, status, etc
may need to create helper classes
"""
def create_downtime(self, form):
"""
- takes in a dictionary that describes the model.
+ Create a downtime event.
+
+ Takes in a dictionary that describes the model.
{
"start": utc timestamp
"end": utc timestamp
class Job(models.Model):
"""
+ A Job to be performed by the Lab.
+
+ The API uses Jobs and Tasks to communicate actions that need to be taken to the Lab
+ that is hosting a booking. A booking from a user has an associated Job which tells
+ the lab how to configure the hardware, networking, etc to fulfill the booking
+ for the user.
This is the class that is serialized and put into the api
"""
+
booking = models.OneToOneField(Booking, on_delete=models.CASCADE, null=True)
status = models.IntegerField(default=JobStatus.NEW)
complete = models.BooleanField(default=False)
def is_fulfilled(self):
"""
+ If a job has been completed by the lab.
+
This method should return true if all of the job's tasks are done,
and false otherwise
"""
class BridgeConfig(models.Model):
- """
- Displays mapping between jumphost interfaces and
- bridges
- """
+ """Displays mapping between jumphost interfaces and bridges."""
+
interfaces = models.ManyToManyField(Interface)
opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.CASCADE)
class SoftwareConfig(TaskConfig):
- """
- handled opnfv installations, etc
- """
+ """Handles software installations, such as OPNFV or ONAP."""
+
opnfv = models.ForeignKey(OpnfvApiConfig, on_delete=models.CASCADE)
def to_dict(self):
class HardwareConfig(TaskConfig):
- """
- handles imaging, user accounts, etc
- """
+ """Describes the desired configuration of the hardware."""
+
image = models.CharField(max_length=100, default="defimage")
power = models.CharField(max_length=100, default="off")
hostname = models.CharField(max_length=100, default="hostname")
class NetworkConfig(TaskConfig):
- """
- handles network configuration
- """
+ """Handles network configuration."""
+
interfaces = models.ManyToManyField(Interface)
delta = models.TextField()
class TaskRelation(models.Model):
+ """
+ Relates a Job to a TaskConfig.
+
+ superclass that relates a Job to tasks anc maintains information
+ like status and messages from the lab
+ """
+
status = models.IntegerField(default=JobStatus.NEW)
job = models.ForeignKey(Job, on_delete=models.CASCADE)
config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE)
class JobFactory(object):
+ """This class creates all the API models (jobs, tasks, etc) needed to fulfill a booking."""
@classmethod
def reimageHost(cls, new_image, booking, host):
- """
- This method will make all necessary changes to make a lab
- reimage a host.
- """
+ """Modify an existing job to reimage the given host."""
job = Job.objects.get(booking=booking)
# make hardware task new
hardware_relation = HostHardwareRelation.objects.get(host=host, job=job)
@classmethod
def makeCompleteJob(cls, booking):
+ """Create everything that is needed to fulfill the given booking."""
hosts = Host.objects.filter(bundle=booking.resource)
job = None
try:
@classmethod
def makeHardwareConfigs(cls, hosts=[], job=Job()):
+ """
+ Create and save HardwareConfig.
+
+ Helper function to create the tasks related to
+ configuring the hardware
+ """
for host in hosts:
hardware_config = None
try:
@classmethod
def makeAccessConfig(cls, users, access_type, revoke=False, job=Job(), context=False):
+ """
+ Create and save AccessConfig.
+
+ Helper function to create the tasks related to
+ configuring the VPN, SSH, etc access for users
+ """
for user in users:
relation = AccessRelation()
relation.job = job
@classmethod
def makeNetworkConfigs(cls, hosts=[], job=Job()):
+ """
+ Create and save NetworkConfig.
+
+ Helper function to create the tasks related to
+ configuring the networking
+ """
for host in hosts:
network_config = None
try:
@classmethod
def makeSoftware(cls, booking=None, job=Job()):
+ """
+ Create and save SoftwareConfig.
+ Helper function to create the tasks related to
+ configuring the desired software, e.g. an OPNFV deployment
+ """
if not booking.opnfv_config:
return None
def to_representation(self, booking):
"""
- Takes in a booking object.
+ Take in a booking object.
+
Returns a dictionary of primitives representing that booking
"""
ser = {}
def to_internal_value(self, data):
"""
- Takes in a dictionary of primitives
- Returns a booking object
+ Take in a dictionary of primitives, and return a booking object.
This is not going to be implemented or allowed.
If someone needs to create a booking through the api,
pass
def to_internal_value(self, data):
- """
- takes in a serialized interface and creates an Interface model
- """
+ """Take in a serialized interface and creates an Interface model."""
mac = data['mac']
bus_address = data['busaddr']
switch_name = data['switchport']['switch_name']
def make_networks(self, hostprofile, nets):
"""
- distributes nets accross hostprofile's interfaces
+ Distribute nets accross hostprofile's interfaces.
+
returns a 2D array
"""
network_struct = []
##############################################################################
-"""laas_dashboard URL Configuration
+"""
+laas_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/
from api.models import LabManagerTracker, get_task
from notifier.manager import NotificationHandler
+"""
+API views.
+
+All functions return a Json blob
+Most functions that deal with info from a specific lab (tasks, host info)
+requires the Lab auth token.
+ for example, curl -H auth-token:mylabsauthtoken url
+
+Most functions let you GET or POST to the same endpoint, and
+the correct thing will happen
+"""
+
class BookingViewSet(viewsets.ModelViewSet):
queryset = Booking.objects.all()
def build_user_list(self):
"""
+ Build list of UserProfiles.
+
returns a mapping of UserProfile ids to displayable objects expected by
searchable multiple select widget
"""
class Booking(models.Model):
id = models.AutoField(primary_key=True)
+ # All bookings are owned by the user who requested it
owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='owner')
+ # an owner can add other users to the booking
collaborators = models.ManyToManyField(User, related_name='collaborators')
+ # start and end time
start = models.DateTimeField()
end = models.DateTimeField()
reset = models.BooleanField(default=False)
jira_issue_id = models.IntegerField(null=True, blank=True)
jira_issue_status = models.CharField(max_length=50, blank=True)
purpose = models.CharField(max_length=300, blank=False)
+ # bookings can be extended a limited number of times
ext_count = models.IntegerField(default=2)
+ # the hardware that the user has booked
resource = models.ForeignKey(ResourceBundle, on_delete=models.SET_NULL, null=True)
+ # configuration for the above hardware
config_bundle = models.ForeignKey(ConfigBundle, on_delete=models.SET_NULL, null=True)
opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.SET_NULL, null=True)
project = models.CharField(max_length=100, default="", blank=True, null=True)
def save(self, *args, **kwargs):
"""
Save the booking if self.user is authorized and there is no overlapping booking.
+
Raise PermissionError if the user is not authorized
Raise ValueError if there is an overlapping booking
"""
def parse_host_field(host_json):
+ """
+ Parse the json from the frontend.
+
+ returns a reference to the selected Lab and HostProfile objects
+ """
lab, profile = (None, None)
lab_dict = host_json['lab']
for lab_info in lab_dict.values():
def check_available_matching_host(lab, hostprofile):
+ """
+ Check the resources are available.
+
+ Returns true if the requested host type is availble,
+ Or throws an exception
+ """
available_host_types = ResourceManager.getInstance().getAvailableHostTypes(lab)
if hostprofile not in available_host_types:
# TODO: handle deleting generic resource in this instance along with grb
return True
+# Functions to create models
+
def generate_grb(owner, lab, common_id):
+ """Create a Generic Resource Bundle."""
grbundle = GenericResourceBundle(owner=owner)
grbundle.lab = lab
grbundle.name = "grbundle for quick booking with uid " + common_id
def generate_gresource(bundle, hostname):
+ """Create a Generic Resource."""
if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", hostname):
raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
gresource = GenericResource(bundle=bundle, name=hostname)
def generate_ghost(generic_resource, host_profile):
+ """Create a Generic Host."""
ghost = GenericHost()
ghost.resource = generic_resource
ghost.profile = host_profile
def generate_config_bundle(owner, common_id, grbundle):
+ """Create a Configuration Bundle."""
cbundle = ConfigBundle()
cbundle.owner = owner
cbundle.name = "configbundle for quick booking with uid " + common_id
def generate_opnfvconfig(scenario, installer, config_bundle):
+ """Create an OPNFV Configuration."""
opnfvconfig = OPNFVConfig()
opnfvconfig.scenario = scenario
opnfvconfig.installer = installer
def generate_hostconfig(generic_host, image, config_bundle):
+ """Create a Host Configuration."""
hconf = HostConfiguration()
hconf.host = generic_host
hconf.image = image
def generate_hostopnfv(hostconfig, opnfvconfig):
+ """Relate the Host and OPNFV Configs."""
config = HostOPNFVConfig()
role = None
try:
def generate_resource_bundle(generic_resource_bundle, config_bundle): # warning: requires cleanup
+ """Create a Resource Bundle."""
try:
resource_manager = ResourceManager.getInstance()
resource_bundle = resource_manager.convertResourceBundle(generic_resource_bundle, config=config_bundle)
def check_invariants(request, **kwargs):
+ """
+ Verify all the contraints on the requested booking.
+
+ verifies software compatibility, booking length, etc
+ """
installer = kwargs['installer']
image = kwargs['image']
scenario = kwargs['scenario']
def create_from_form(form, request):
+ """
+ Create a Booking from the user's form.
+
+ Large, nasty method to create a booking or return a useful error
+ based on the form from the frontend
+ """
quick_booking_id = str(uuid.uuid4())
host_field = form.cleaned_data['filter_field']
def drop_filter(user):
+ """
+ Return a dictionary that contains filters.
+
+ Only certain installlers are supported on certain images, etc
+ so the image filter indexed at [imageid][installerid] is truthy if
+ that installer is supported on that image
+ """
installer_filter = {}
for image in Image.objects.all():
installer_filter[image.id] = {}
@staticmethod
def getContinuousBookingTimeSeries(span=28):
"""
+ Calculate Booking usage data points.
+
Will return a dictionary of names and 2-D array of x and y data points.
e.g. {"plot1": [["x1", "x2", "x3"],["y1", "y2", "y3]]}
x values will be dates in string
class BookingModelTestCase(TestCase):
+ """
+ Test the Booking model.
+
+ Creates all the scafolding needed and tests the Booking model
+ """
count = 0
def setUp(self):
+ """
+ Prepare for Booking model tests.
+
+ Creates all the needed models, such as users, resources, and configurations
+ """
self.owner = User.objects.create(username='owner')
self.res1 = ResourceBundle.objects.create(
def test_start_end(self):
"""
+ Verify the start and end fields.
+
if the start of a booking is greater or equal then the end,
saving should raise a ValueException
"""
def test_conflicts(self):
"""
+ Verify conflicting dates are dealt with.
+
saving an overlapping booking on the same resource
should raise a ValueException
saving for different resources should succeed
def test_extensions(self):
"""
+ Test booking extensions.
+
saving a booking with an extended end time is allows to happen twice,
and each extension must be a maximum of one week long
"""
##############################################################################
-"""laas_dashboard URL Configuration
+"""
+laas_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/
class ResourceProvisioningException(Exception):
- """
- Resources could not be provisioned
- """
+ """Resources could not be provisioned."""
+
pass
class ModelValidationException(Exception):
- """
- Validation before saving model returned issues
- """
+ """Validation before saving model returned issues."""
+
pass
class ResourceAvailabilityException(ResourceProvisioningException):
- """
- Requested resources are not *currently* available
- """
+ """Requested resources are not *currently* available."""
+
pass
class ResourceExistenceException(ResourceAvailabilityException):
- """
- Requested resources do not exist or do not match any known resources
- """
+ """Requested resources do not exist or do not match any known resources."""
+
pass
def make_profile_data(self):
"""
+ Create Profile Data.
+
returns a dictionary of data from the yaml files
created by inspection scripts
"""
@shared_task
def free_hosts():
- """
- gets all hosts from the database that need to be freed and frees them
- """
+ """Free all hosts that should be freed."""
undone_statuses = [JobStatus.NEW, JobStatus.CURRENT, JobStatus.ERROR]
undone_jobs = Job.objects.filter(
hostnetworkrelation__status__in=undone_statuses,
##############################################################################
-"""laas_dashboard URL Configuration
+"""
+laas_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/
EMAIL_USE_TLS = True
DEFAULT_EMAIL_FROM = os.environ.get('DEFAULT_EMAIL_FROM', 'webmaster@localhost')
SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
-EXPIRE_LIFETIME = 12 # Minimum lifetime of booking to send notification
-EXPIRE_HOURS = 48 # Notify when booking is expiring within this many hours
+EXPIRE_LIFETIME = 12 # Minimum lifetime of booking to send notification
+EXPIRE_HOURS = 48 # Notify when booking is expiring within this many hours
##############################################################################
-"""laas_dashboard URL Configuration
+"""
+laas_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/
@classmethod
def booking_notify(cls, booking, template, titles):
"""
- Creates a notification for a booking owner and collaborators
- using the template.
+ Create a notification for a booking owner and collaborators using the template.
+
titles is a list - the first is the title for the owner's notification,
the last is the title for the collaborators'
"""
@classmethod
def task_updated(cls, task):
"""
+ Notification of task changing.
+
called every time a lab updated info about a task.
sends an email when 'task' changing state means a booking has
just been fulfilled (all tasks done, servers ready to use)
class Emailed(models.Model):
- """
- A simple record to remember who has already gotten an email
- to avoid resending
- """
+ """A simple record to remember who has already gotten an email to avoid resending."""
+
begin_booking = models.OneToOneField(
Booking,
null=True,
on_delete=models.CASCADE,
related_name="over_mail"
)
-
@shared_task
def notify_expiring():
- """
- Notify users if their booking is within 48 hours of expiring.
- """
+ """Notify users if their booking is within 48 hours of expiring."""
expire_time = timezone.now() + timezone.timedelta(hours=settings.EXPIRE_HOURS)
# Don't email people about bookings that have started recently
start_time = timezone.now() - timezone.timedelta(hours=settings.EXPIRE_LIFETIME)
- bookings = Booking.objects.filter(end__lte=expire_time,
+ bookings = Booking.objects.filter(
+ end__lte=expire_time,
end__gte=timezone.now(),
- start__lte=start_time)
+ start__lte=start_time
+ )
for booking in bookings:
if Emailed.objects.filter(almost_end_booking=booking).exists():
continue
class IDFTemplater:
- """
- Utility class to create a full IDF yaml file
- """
+ """Utility class to create a full Installer Descriptor File (IDF) yaml file."""
+
net_names = ["admin", "mgmt", "private", "public"]
bridge_names = {
"admin": "br-admin",
}
def makeIDF(self, booking):
- """
- fills the installer descriptor file template with info about the resource
- """
+ """Fill the IDF template with info about the resource."""
template = "dashboard/idf.yaml"
info = {}
info['version'] = "0.1"
def get_configuration(self, state):
"""
+ Get configuration of Resource.
+
Returns the desired configuration for this host as a
JSON object as defined in the rest api spec.
state is a ConfigState
- TODO: single method, or different methods for hw, network, snapshot, etc?
"""
raise NotImplementedError("Must implement in concrete Resource classes")
def reserve(self):
- """
- Reserves this resource for its currently
- assigned booking.
- """
+ """Reserve this resource for its currently assigned booking."""
raise NotImplementedError("Must implement in concrete Resource classes")
def release(self):
- """
- Makes this resource available again for new boookings
- """
+ """Make this resource available again for new boookings."""
raise NotImplementedError("Must implement in concrete Resource classes")
def get_configuration(self, state):
"""
- Returns the network configuration
+ Get the network configuration.
+
Collects info about each attached network interface and vlan, etc
"""
return {}
def reserve(self):
- """
- Reserves vlan(s) associated with this network
- """
+ """Reserve vlan(s) associated with this network."""
# vlan_manager = self.bundle.lab.vlan_manager
return False
class Image(models.Model):
- """
- model for representing OS images / snapshots of hosts
- """
+ """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)
class HostConfiguration(models.Model):
- """
- model to represent a complete configuration for a single
- physical host
- """
+ """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)
class OPNFV_SETTINGS():
- """
- This is a static configuration class
- """
+ """This is a static configuration class."""
+
# all the required network types in PDF/IDF spec
NETWORK_ROLES = ["public", "private", "admin", "mgmt"]
class PDFTemplater:
- """
- Utility class to create a full PDF yaml file
- """
+ """Utility class to create a full PDF yaml file."""
@classmethod
def makePDF(cls, booking):
- """
- fills the pod descriptor file template with info about the resource
- """
+ """Fill the pod descriptor file template with info about the resource."""
template = "dashboard/pdf.yaml"
info = {}
info['details'] = cls.get_pdf_details(booking.resource)
@classmethod
def get_pdf_details(cls, resource):
- """
- Info for the "details" section
- """
+ """Info for the "details" section."""
details = {}
owner = "Anon"
email = "email@mail.com"
@classmethod
def get_jumphost(cls, booking):
+ """Return the host designated as the Jumphost for the booking."""
jumphost = None
if booking.opnfv_config:
jumphost_opnfv_config = booking.opnfv_config.host_opnfv_config.get(
@classmethod
def get_pdf_jumphost(cls, booking):
- """
- returns a dict of all the info for the "jumphost" section
- """
+ """Return a dict of all the info for the "jumphost" section."""
jumphost = cls.get_jumphost(booking)
jumphost_info = cls.get_pdf_host(jumphost)
jumphost_info['os'] = jumphost.config.image.os.name
@classmethod
def get_pdf_nodes(cls, booking):
- """
- returns a list of all the "nodes" (every host except jumphost)
- """
+ """Return a list of all the "nodes" (every host except jumphost)."""
pdf_nodes = []
nodes = set(Host.objects.filter(bundle=booking.resource))
nodes.discard(cls.get_jumphost(booking))
@classmethod
def get_pdf_host(cls, host):
"""
- method to gather all needed info about a host
- returns a dict
+ Gather all needed info about a host.
+
+ returns a dictionary
"""
host_info = {}
host_info['name'] = host.template.resource.name
@classmethod
def get_pdf_host_node(cls, host):
- """
- returns "node" info for a given host
- """
+ """Return "node" info for a given host."""
d = {}
d['type'] = "baremetal"
d['vendor'] = host.vendor
@classmethod
def get_pdf_host_disk(cls, disk):
- """
- returns a dict describing the given disk
- """
+ """Return a dict describing the given disk."""
disk_info = {}
disk_info['name'] = disk.name
disk_info['capacity'] = str(disk.size) + "G"
@classmethod
def get_pdf_host_iface(cls, interface):
- """
- returns a dict describing given interface
- """
+ """Return a dict describing given interface."""
iface_info = {}
iface_info['features'] = "none"
iface_info['mac_address'] = interface.mac_address
@classmethod
def get_pdf_host_remote_management(cls, host):
- """
- gives the remote params of the host
- """
+ """Get the remote params of the host."""
man = host.remote_management
mgmt = {}
mgmt['address'] = man.address
def hostsAvailable(self, grb):
"""
- This method will check if the given GenericResourceBundle
- is available. No changes to the database
- """
+ Check if the given GenericResourceBundle is available.
+ No changes to the database
+ """
# count up hosts
profile_count = {}
for host in grb.getResources():
def convertResourceBundle(self, genericResourceBundle, config=None):
"""
- Takes in a GenericResourceBundle and 'converts' it into a ResourceBundle
+ Convert a GenericResourceBundle into a ResourceBundle.
+
+ Takes in a genericResourceBundle and reserves all the
+ Resources needed and returns a completed ResourceBundle.
"""
resource_bundle = ResourceBundle.objects.create(template=genericResourceBundle)
generic_hosts = genericResourceBundle.getResources()
##############################################################################
-"""laas_dashboard URL Configuration
+"""
+laas_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/
--- /dev/null
+This app creates "workflows", which are long and complex interactions from the user.
+Workflows are composed of multiple steps. At each step the user inputs some information.
+The content of one step may impact following steps.
+
+The WorkflowStep object is the abstract type for all the workflow steps.
+Important attributes and methods:
+
+template - the django template to use when rendering this step
+valid - the status code from WorkflowStepStatus
+
+get_context() - returns a dictionary that is used when rendering this step's template
+ You should always call super's get_context and add / overwrite any data into that
+ dictionary
+
+post(data, user) - this method is called when the step is POST'd to.
+ data is from the request object, suitable for a Form's constructor
+
+
+Repository
+Each step has a reference to a shared repository (self.repo).
+The repo is a key-value store that allows the steps to share data
+
+Steps render based on the current state of the repo. For example, a step
+may get information about each host the user said they want and ask for additional
+input for each machine.
+Because the steps render based on what is in the repo, a user can easily go back to
+a previous step and change some data. This data will change in the repo and
+affect later steps accordingly.
+
+Everything stored in the repo is temporary. After a workflow has been completed, the repo
+is translated into Django models and saved to the database.
items=None, queryset=None, show_from_noentry=True, show_x_results=-1,
results_scrollable=False, selectable_limit=-1, placeholder="search here",
name="searchable_select", initial=[], **kwargs):
- """from the documentation:
+ """
+ From the documentation.
+
# required -- Boolean that specifies whether the field is required.
# True by default.
# widget -- A Widget class, or instance of a Widget class, that should
# label_suffix -- Suffix to be added to the label. Overrides
# form's label_suffix.
"""
-
self.widget = widget
if self.widget is None:
self.widget = SearchableSelectMultipleWidget(
@staticmethod
def getLabData(multiple_hosts=False):
"""
- Gets all labs and thier host profiles and returns a serialized version the form can understand.
- Should be rewritten with a related query to make it faster
+ Get all labs and thier host profiles, returns a serialized version the form can understand.
+
+ Could be rewritten with a related query to make it faster
"""
# javascript truthy variables
true = 1
class BookingAuthManager():
+ """
+ Verifies Booking Authorization.
+
+ Class to verify that the user is allowed to book the requested resource
+ The user must input a url to the INFO.yaml file to prove that they are the ptl of
+ an approved project if they are booking a multi-node pod.
+ This class parses the url and checks the logged in user against the info file.
+ """
+
LFN_PROJECTS = ["opnfv"] # TODO
def parse_github_url(self, url):
def parse_url(self, info_url):
"""
- will return the PTL in the INFO file on success, or None
+ Parse the project URL.
+
+ Gets the INFO.yaml file from the project and returns the PTL info.
"""
if "github" in info_url:
return self.parse_github_url(info_url)
def booking_allowed(self, booking, repo):
"""
+ Assert the current Booking Policy.
+
This is the method that will have to change whenever the booking policy changes in the Infra
group / LFN. This is a nice isolation of that administration crap
currently checks if the booking uses multiple servers. if it does, then the owner must be a PTL,
class WorkflowStepStatus(object):
+ """
+ Poor man's enum for the status of a workflow step.
+
+ The steps in a workflow are not completed (UNTOUCHED)
+ or they have been completed correctly (VALID) or they were filled out
+ incorrectly (INVALID)
+ """
+
UNTOUCHED = 0
INVALID = 100
VALID = 200
def decomposeXml(self, xmlString):
"""
+ Translate XML into useable data.
+
This function takes in an xml doc from our front end
and returns dictionaries that map cellIds to the xml
nodes themselves. There is no unpacking of the
xml objects, just grouping and organizing
"""
-
connections = {}
networks = {}
hosts = {}
def build_filter_data(self, hosts_data):
"""
+ Build list of Images to filter out.
+
returns a 2D array of images to exclude
based on the ordering of the passed
hosts_data
##############################################################################
"""
-This file tests basic functionality of each step class
+This file tests basic functionality of each step class.
+
More in depth case coverage of WorkflowStep.post() must happen elsewhere.
"""
class TestConfig:
"""
- Basic class to instantiate and hold reference
+ Basic class to instantiate and hold reference.
+
to models we will need often
"""
+
def __init__(self, usr=None):
self.lab = make_lab()
self.user = usr or make_user()
def assertCorrectPostBehavior(self, post_data):
"""
+ Stub for validating step behavior on POST request.
+
allows subclasses to override and make assertions about
the side effects of self.step.post()
post_data is the data passed into post()
def add_to_repo(self, repo):
"""
+ Stub for modifying the step's repo.
+
This method is a hook that allows subclasses to modify
the contents of the repo before the step is created.
"""
def assertValidHtml(self, html_str):
"""
- This method should make sure that html_str is a valid
- html fragment.
+ Assert that html_str is a valid html fragment.
+
However, I know of no good way of doing this in python
"""
self.assertTrue(isinstance(html_str, str))
session.save()
def render_steps(self):
- """
- retrieves each step individually at /wf/workflow/step=<index>
- """
+ """Retrieve each step individually at /wf/workflow/step=<index>."""
for i in range(self.step_count):
# renders the step itself, not in an iframe
exception = None
##############################################################################
# first, basic lint with flake8
-find . -type f -name "*.py" -not -name "manage.py" | xargs flake8 --count --ignore E501
-
+find . -type f -name "*.py" -not -name "manage.py" -not -path "*/migrations/*" | xargs flake8 --count
# this file should be executed from the dir it is in
docker exec -it dg01 python manage.py test