Merge "Fix vlan leak"
[pharos-tools.git] / dashboard / src / booking / quick_deployer.py
index 9bc8c66..763c8a0 100644 (file)
@@ -22,7 +22,6 @@ from resource_inventory.models import (
     Image,
     GenericResourceBundle,
     ConfigBundle,
-    Vlan,
     Host,
     HostProfile,
     HostConfiguration,
@@ -30,14 +29,21 @@ from resource_inventory.models import (
     GenericHost,
     GenericInterface,
     OPNFVRole,
-    OPNFVConfig
+    OPNFVConfig,
+    Network,
+    NetworkConnection,
+    NetworkRole,
+    HostOPNFVConfig,
 )
 from resource_inventory.resource_manager import ResourceManager
+from resource_inventory.pdf_templater import PDFTemplater
+from notifier.manager import NotificationHandler
 from booking.models import Booking
 from dashboard.exceptions import (
     InvalidHostnameException,
     ResourceAvailabilityException,
-    ModelValidationException
+    ModelValidationException,
+    BookingLengthException
 )
 from api.models import JobFactory
 
@@ -87,22 +93,12 @@ class NoRemainingPublicNetwork(Exception):
     pass
 
 
-def create_from_form(form, request):
-    quick_booking_id = str(uuid.uuid4())
-
-    host_field = form.cleaned_data['filter_field']
-    host_json = json.loads(host_field)
-    purpose_field = form.cleaned_data['purpose']
-    project_field = form.cleaned_data['project']
-    users_field = form.cleaned_data['users']
-    host_name = form.cleaned_data['hostname']
-    length = form.cleaned_data['length']
+class BookingPermissionException(Exception):
+    pass
 
-    image = form.cleaned_data['image']
-    scenario = form.cleaned_data['scenario']
-    installer = form.cleaned_data['installer']
 
-    # get all initial info we need to validate
+def parse_host_field(host_field_contents):
+    host_json = json.loads(host_field_contents)
     lab_dict = host_json['labs'][0]
     lab_id = list(lab_dict.keys())[0]
     lab_user_id = int(lab_id.split("_")[-1])
@@ -114,136 +110,229 @@ def create_from_form(form, request):
     profile = HostProfile.objects.get(id=profile_id)
 
     # check validity of field data before trying to apply to models
+    if len(host_json['labs']) != 1:
+        raise NoLabSelectedError("No lab was selected")
     if not lab:
         raise LabDNE("Lab with provided ID does not exist")
     if not profile:
         raise HostProfileDNE("Host type with provided ID does not exist")
 
-    # check that hostname is valid
-    if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", host_name):
-        raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
-    # check that image os is compatible with installer
-    if installer in image.os.sup_installers.all():
-        #if installer not here, we can omit that and not check for scenario
-        if not scenario:
-            raise IncompatibleScenarioForInstaller("An OPNFV Installer needs a scenario to be chosen to work properly")
-        if scenario not in installer.sup_scenarios.all():
-            raise IncompatibleScenarioForInstaller("The chosen installer does not support the chosen scenario")
-    if image.from_lab != lab:
-        raise ImageNotAvailableAtLab("The chosen image is not available at the chosen hosting lab")
-    if image.host_type != profile:
-        raise IncompatibleImageForHost("The chosen image is not available for the chosen host type")
-    if not image.public and image.owner != request.user:
-        raise ImageOwnershipInvalid("You are not the owner of the chosen private image")
+    return lab, profile
+
 
-    # check if host type is available
-    #ResourceManager.getInstance().acquireHost(ghost, lab.name)
+def check_available_matching_host(lab, hostprofile):
     available_host_types = ResourceManager.getInstance().getAvailableHostTypes(lab)
-    if not profile in available_host_types:
+    if hostprofile not in available_host_types:
         # TODO: handle deleting generic resource in this instance along with grb
-        raise HostNotAvailable("Could not book selected host due to changed availability. Try again later")
+        raise HostNotAvailable('Requested host type is not available. Please try again later. Host availability can be viewed in the "Hosts" tab to the left.')
 
-    # check if any hosts with profile at lab are still available
-    hostset = Host.objects.filter(lab=lab, profile=profile).filter(booked=False).filter(working=True)
-    if not hostset.first():
+    hostset = Host.objects.filter(lab=lab, profile=hostprofile).filter(booked=False).filter(working=True)
+    if not hostset.exists():
         raise HostNotAvailable("Couldn't find any matching unbooked hosts")
 
-    # generate GenericResourceBundle
-    if len(host_json['labs']) != 1:
-        raise NoLabSelectedError("No lab was selected")
+    return True
+
 
-    grbundle = GenericResourceBundle(owner=request.user)
+def generate_grb(owner, lab, common_id):
+    grbundle = GenericResourceBundle(owner=owner)
     grbundle.lab = lab
-    grbundle.name = "grbundle for quick booking with uid " + quick_booking_id
+    grbundle.name = "grbundle for quick booking with uid " + common_id
     grbundle.description = "grbundle created for quick-deploy booking"
     grbundle.save()
 
-    # generate GenericResource, GenericHost
-    gresource = GenericResource(bundle=grbundle, name=host_name)
+    return grbundle
+
+
+def generate_gresource(bundle, hostname):
+    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)
     gresource.save()
 
+    return gresource
+
+
+def generate_ghost(generic_resource, host_profile):
     ghost = GenericHost()
-    ghost.resource = gresource
-    ghost.profile = profile
+    ghost.resource = generic_resource
+    ghost.profile = host_profile
     ghost.save()
 
-    # generate config bundle
+    return ghost
+
+
+def generate_config_bundle(owner, common_id, grbundle):
     cbundle = ConfigBundle()
-    cbundle.owner = request.user
-    cbundle.name = "configbundle for quick booking  with uid " + quick_booking_id
+    cbundle.owner = owner
+    cbundle.name = "configbundle for quick booking with uid " + common_id
     cbundle.description = "configbundle created for quick-deploy booking"
     cbundle.bundle = grbundle
     cbundle.save()
 
-    # generate OPNFVConfig pointing to cbundle
-    if installer:
-        opnfvconfig = OPNFVConfig()
-        opnfvconfig.scenario = scenario
-        opnfvconfig.installer = installer
-        opnfvconfig.bundle = cbundle
-        opnfvconfig.save()
+    return cbundle
+
+
+def generate_opnfvconfig(scenario, installer, config_bundle):
+    opnfvconfig = OPNFVConfig()
+    opnfvconfig.scenario = scenario
+    opnfvconfig.installer = installer
+    opnfvconfig.bundle = config_bundle
+    opnfvconfig.save()
+
+    return opnfvconfig
+
 
-    # generate HostConfiguration pointing to cbundle
+def generate_hostconfig(generic_host, image, config_bundle):
     hconf = HostConfiguration()
-    hconf.host = ghost
+    hconf.host = generic_host
     hconf.image = image
-    hconf.opnfvRole = OPNFVRole.objects.get(name="Jumphost")
-    if not hconf.opnfvRole:
-        raise OPNFVRoleDNE("No jumphost role was found")
-    hconf.bundle = cbundle
+    hconf.bundle = config_bundle
+    hconf.is_head_node = True
     hconf.save()
 
-    # construct generic interfaces
-    for interface_profile in profile.interfaceprofile.all():
-        generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
-        generic_interface.save()
-    ghost.save()
-
-    # get vlan, assign to first interface
-    publicnetwork = lab.vlan_manager.get_public_vlan()
-    publicvlan = publicnetwork.vlan
-    if not publicnetwork:
-        raise NoRemainingPublicNetwork("No public networks were available for your pod")
-    lab.vlan_manager.reserve_public_vlan(publicvlan)
+    return hconf
 
-    vlan = Vlan.objects.create(vlan_id=publicvlan, tagged=False, public=True)
-    vlan.save()
-    ghost.generic_interfaces.first().vlans.add(vlan)
-    ghost.generic_interfaces.first().save()
 
-    # generate resource bundle
+def generate_hostopnfv(hostconfig, opnfvconfig):
+    config = HostOPNFVConfig()
+    role = None
     try:
-        resource_bundle = ResourceManager.getInstance().convertResourceBundle(grbundle, config=cbundle)
+        role = OPNFVRole.objects.get(name="Jumphost")
+    except Exception:
+        role = OPNFVRole.objects.create(
+            name="Jumphost",
+            description="Single server jumphost role"
+        )
+    config.role = role
+    config.host_config = hostconfig
+    config.opnfv_config = opnfvconfig
+    config.save()
+    return config
+
+
+def generate_resource_bundle(generic_resource_bundle, config_bundle):  # warning: requires cleanup
+    try:
+        resource_manager = ResourceManager.getInstance()
+        resource_bundle = resource_manager.convertResourceBundle(generic_resource_bundle, config=config_bundle)
+        return resource_bundle
     except ResourceAvailabilityException:
         raise ResourceAvailabilityException("Requested resources not available")
     except ModelValidationException:
         raise ModelValidationException("Encountered error while saving grbundle")
 
+
+def check_invariants(request, **kwargs):
+    installer = kwargs['installer']
+    image = kwargs['image']
+    scenario = kwargs['scenario']
+    lab = kwargs['lab']
+    host_profile = kwargs['host_profile']
+    length = kwargs['length']
+    # check that image os is compatible with installer
+    if installer in image.os.sup_installers.all():
+        # if installer not here, we can omit that and not check for scenario
+        if not scenario:
+            raise IncompatibleScenarioForInstaller("An OPNFV Installer needs a scenario to be chosen to work properly")
+        if scenario not in installer.sup_scenarios.all():
+            raise IncompatibleScenarioForInstaller("The chosen installer does not support the chosen scenario")
+    if image.from_lab != lab:
+        raise ImageNotAvailableAtLab("The chosen image is not available at the chosen hosting lab")
+    if image.host_type != host_profile:
+        raise IncompatibleImageForHost("The chosen image is not available for the chosen host type")
+    if not image.public and image.owner != request.user:
+        raise ImageOwnershipInvalid("You are not the owner of the chosen private image")
+    if length < 1 or length > 21:
+        raise BookingLengthException("Booking must be between 1 and 21 days long")
+
+
+def configure_networking(grb, config):
+    # create network
+    net = Network.objects.create(name="public", bundle=grb, is_public=True)
+    # connect network to generic host
+    grb.getHosts()[0].generic_interfaces.first().connections.add(
+        NetworkConnection.objects.create(network=net, vlan_is_tagged=False)
+    )
+    # asign network role
+    role = NetworkRole.objects.create(name="public", network=net)
+    opnfv_config = config.opnfv_config.first()
+    if opnfv_config:
+        opnfv_config.networks.add(role)
+
+
+def create_from_form(form, request):
+    quick_booking_id = str(uuid.uuid4())
+
+    host_field = form.cleaned_data['filter_field']
+    purpose_field = form.cleaned_data['purpose']
+    project_field = form.cleaned_data['project']
+    users_field = form.cleaned_data['users']
+    hostname = form.cleaned_data['hostname']
+    length = form.cleaned_data['length']
+
+    image = form.cleaned_data['image']
+    scenario = form.cleaned_data['scenario']
+    installer = form.cleaned_data['installer']
+
+    lab, host_profile = parse_host_field(host_field)
+    data = form.cleaned_data
+    data['lab'] = lab
+    data['host_profile'] = host_profile
+    check_invariants(request, **data)
+
+    # check booking privileges
+    if Booking.objects.filter(owner=request.user, end__gt=timezone.now()).count() >= 3 and not request.user.userprofile.booking_privledge:
+        raise BookingPermissionException("You do not have permission to have more than 3 bookings at a time.")
+
+    check_available_matching_host(lab, host_profile)  # requires cleanup if failure after this point
+
+    grbundle = generate_grb(request.user, lab, quick_booking_id)
+    gresource = generate_gresource(grbundle, hostname)
+    ghost = generate_ghost(gresource, host_profile)
+    cbundle = generate_config_bundle(request.user, quick_booking_id, grbundle)
+    hconf = generate_hostconfig(ghost, image, cbundle)
+
+    # if no installer provided, just create blank host
+    opnfv_config = None
+    if installer:
+        opnfv_config = generate_opnfvconfig(scenario, installer, cbundle)
+        generate_hostopnfv(hconf, opnfv_config)
+
+    # construct generic interfaces
+    for interface_profile in host_profile.interfaceprofile.all():
+        generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
+        generic_interface.save()
+
+    configure_networking(grbundle, cbundle)
+
+    # generate resource bundle
+    resource_bundle = generate_resource_bundle(grbundle, cbundle)
+
     # generate booking
-    booking = Booking()
-    booking.purpose = purpose_field
-    booking.project = project_field
-    booking.lab = lab
-    booking.owner = request.user
-    booking.start = timezone.now()
-    booking.end = timezone.now() + timedelta(days=int(length))
-    booking.resource = resource_bundle
-    booking.pdf = ResourceManager().makePDF(booking.resource)
-    booking.save()
-    print("users field:")
-    print(users_field)
-    print(type(users_field))
-    #users_field = json.loads(users_field)
+    booking = Booking.objects.create(
+        purpose=purpose_field,
+        project=project_field,
+        lab=lab,
+        owner=request.user,
+        start=timezone.now(),
+        end=timezone.now() + timedelta(days=int(length)),
+        resource=resource_bundle,
+        config_bundle=cbundle,
+        opnfv_config=opnfv_config
+    )
+    booking.pdf = PDFTemplater.makePDF(booking)
+
     users_field = users_field[2:-2]
-    if users_field: #may be empty after split, if no collaborators entered
+    if users_field:  # may be empty after split, if no collaborators entered
         users_field = json.loads(users_field)
         for collaborator in users_field:
             user = User.objects.get(id=collaborator['id'])
             booking.collaborators.add(user)
-        booking.save()
+
+    booking.save()
 
     # generate job
     JobFactory.makeCompleteJob(booking)
+    NotificationHandler.notify_new_booking(booking)
 
 
 def drop_filter(user):