From 58bab5d0818e50b0052bfdcad7d6cbd0251a99b2 Mon Sep 17 00:00:00 2001 From: Benjamin Bertrand <benjamin.bertrand@esss.se> Date: Thu, 1 Mar 2018 13:23:30 +0100 Subject: [PATCH] Add Create VM button Allow to create a VM or Virtual IOC --- app/models.py | 14 ++++++++ app/network/forms.py | 11 ++++++ app/network/views.py | 21 +++++++++--- app/settings.py | 8 +++++ app/tasks.py | 33 ++++++++++++++++++ app/templates/_helpers.html | 32 ++++++++++++++++-- app/templates/network/view_host.html | 50 +++++++++++++++++++--------- 7 files changed, 147 insertions(+), 22 deletions(-) diff --git a/app/models.py b/app/models.py index 2dd4a6b..fdc1f1a 100644 --- a/app/models.py +++ b/app/models.py @@ -646,6 +646,13 @@ class Host(CreatedMixin, db.Model): for item in kwargs['items']] super().__init__(**kwargs) + @property + def is_ioc(self): + for interface in self.interfaces: + if interface.is_ioc: + return True + return False + def __str__(self): return str(self.name) @@ -734,6 +741,13 @@ class Interface(CreatedMixin, db.Model): def address(self): return ipaddress.ip_address(self.ip) + @property + def is_ioc(self): + for tag in self.tags: + if tag.name == 'IOC': + return True + return False + def __str__(self): return str(self.name) diff --git a/app/network/forms.py b/app/network/forms.py index 37b9a7f..989f1fd 100644 --- a/app/network/forms.py +++ b/app/network/forms.py @@ -10,6 +10,7 @@ This module defines the network blueprint forms. """ import ipaddress +from flask import current_app from flask_login import current_user from wtforms import (SelectField, StringField, TextAreaField, IntegerField, SelectMultipleField, BooleanField, validators) @@ -157,3 +158,13 @@ class InterfaceForm(CSEntryForm): class HostInterfaceForm(HostForm, InterfaceForm): pass + + +class CreateVMForm(CSEntryForm): + cores = SelectField('Cores', default=2, coerce=int) + memory = SelectField('Memory (GB)', default=2, coerce=int) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.cores.choices = utils.get_choices(current_app.config['VM_CORES_CHOICES']) + self.memory.choices = utils.get_choices(current_app.config['VM_MEMORY_CHOICES']) diff --git a/app/network/views.py b/app/network/views.py index 5fb7e3c..e6cc053 100644 --- a/app/network/views.py +++ b/app/network/views.py @@ -13,9 +13,9 @@ import ipaddress import sqlalchemy as sa from flask import (Blueprint, render_template, jsonify, session, redirect, url_for, request, flash, current_app) -from flask_login import login_required +from flask_login import login_required, current_user from .forms import (HostForm, InterfaceForm, HostInterfaceForm, NetworkForm, - NetworkScopeForm, DomainForm) + NetworkScopeForm, DomainForm, CreateVMForm) from ..extensions import db from ..decorators import login_groups_accepted from .. import models, utils, helpers, tasks @@ -79,11 +79,24 @@ def create_host(): return render_template('network/create_host.html', form=form) -@bp.route('/hosts/view/<name>') +@bp.route('/hosts/view/<name>', methods=('GET', 'POST')) @login_required def view_host(name): host = models.Host.query.filter_by(name=name).first_or_404() - return render_template('network/view_host.html', host=host) + form = CreateVMForm() + if host.is_ioc: + form.cores.choices = utils.get_choices(current_app.config['VIOC_CORES_CHOICES']) + form.memory.choices = utils.get_choices(current_app.config['VIOC_MEMORY_CHOICES']) + if form.validate_on_submit(): + if not current_user.is_admin: + flash(f'Only admin users are allowed to create a VM!', 'info') + else: + interface = host.interfaces[0] + job = tasks.trigger_vm_creation(name, interface, form.memory.data, form.cores.data) + current_app.logger.info(f'Creation of {name} requested: job {job.get_id()}') + flash(f'Creation of {name} requested!', 'success') + return redirect(url_for('network.view_host', name=name)) + return render_template('network/view_host.html', host=host, form=form) @bp.route('/hosts/edit/<name>', methods=('GET', 'POST')) diff --git a/app/settings.py b/app/settings.py index cbfa3be..363c14c 100644 --- a/app/settings.py +++ b/app/settings.py @@ -70,3 +70,11 @@ DOCUMENTATION_URL = 'http://ics-infrastructure.pages.esss.lu.se/csentry/index.ht # AWX job templates AWX_JOB_ENABLED = False AWX_CORE_SERVICES_UPDATE = 'ics-ans-core @ DHCP test' +AWX_CREATE_VM = 'ics-ans-deploy-proxmox-vm' +AWX_CREATE_VIOC = 'ics-ans-deploy-vioc' + +VM_CORES_CHOICES = [1, 2, 4, 6, 8, 24] +VM_MEMORY_CHOICES = [2, 4, 8, 16, 32, 128] +VM_DEFAULT_DNS = '172.16.6.21' +VIOC_CORES_CHOICES = [1, 3, 6] +VIOC_MEMORY_CHOICES = [2, 4, 8] diff --git a/app/tasks.py b/app/tasks.py index db513ed..92d71d1 100644 --- a/app/tasks.py +++ b/app/tasks.py @@ -14,6 +14,39 @@ from flask import current_app from rq import Queue +def trigger_vm_creation(name, interface, memory, cores): + """Trigger a job to create a virtual machine or virtual IOC""" + extra_vars = [ + f'vmname={name}', + f'memory={memory}', + f'cores={cores}', + f'vcpus={cores}', + f'vlan_name={interface.network.vlan_name}', + f'vlan_id={interface.network.vlan_id}', + f'mac={interface.mac.address}', + ] + if interface.is_ioc: + job_template = current_app.config['AWX_CREATE_VIOC'] + else: + job_template = current_app.config['AWX_CREATE_VM'] + extra_vars.extend([ + f'ip_address={interface.ip}', + f'domain={interface.network.domain.name}', + f'dns={current_app.config["VM_DEFAULT_DNS"]}', + f'netmask={interface.network.netmask}', + f'gateway={interface.network.gateway}', + ]) + q = Queue() + current_app.logger.info(f'Launch new job to create the {name} VM: {job_template} with {extra_vars}') + job = q.enqueue( + launch_job_template, + job_template=job_template, + extra_vars=extra_vars, + timeout=500, + ) + return job + + def trigger_core_services_update(): """Trigger a job to update the core services on the TN (DNS and DHCP) diff --git a/app/templates/_helpers.html b/app/templates/_helpers.html index 5bb36c4..c6f049a 100644 --- a/app/templates/_helpers.html +++ b/app/templates/_helpers.html @@ -36,21 +36,24 @@ {% macro render_field(field) -%} {% set field_class = kwargs.pop('class_', '') + ' form-control' %} + {% set label_size = kwargs.pop('label_size', '2') %} + {% set input_size = kwargs.pop('input_size', '10') %} + {% if field.errors %} {% set field_class = field_class + ' is-invalid' %} {% endif %} <div class="form-group row"> {% if field.type == 'BooleanField' %} - <div class="col-sm-10 offset-sm-2"> + <div class="col-sm-{{ input_size }} offset-sm-{{ label_size }}"> <div class="form-check"> {{ field(class_="form-check-input") }} {{ field.label(class_="form-check-label") }} </div> </div> {% else %} - {{ field.label(class_="col-sm-2 col-form-label") }} - <div class="col-sm-10"> + {{ field.label(class_="col-sm-" + label_size + " col-form-label") }} + <div class="col-sm-{{ input_size }}"> {{ field(class_=field_class, **kwargs) }} {% if field.description %} <small class="form-text text-muted"> @@ -94,6 +97,29 @@ </div> {%- endmacro %} +{% macro submit_button_with_confirmation(title, message) -%} + <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#submitModal" }}> + {{ title }} + </button> + <!-- Modal --> + <div class="modal fade" id="submitModal" tabindex="-1" role="dialog" aria-labelledby="submit-confirmation" aria-hidden="true"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h6 class="modal-title">{{ message }}</h6> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> + <button type="submit" class="btn btn-primary">OK</button> + </div> + </div> + </div> + </div> +{%- endmacro %} + {% macro figure(filename, description) -%} <figure class="figure"> <img src="{{ url_for('static', filename='img/' + filename) }}" class="img-fluid mx-auto d-block" alt="{{ description }}"> diff --git a/app/templates/network/view_host.html b/app/templates/network/view_host.html index c07f214..6489328 100644 --- a/app/templates/network/view_host.html +++ b/app/templates/network/view_host.html @@ -1,5 +1,6 @@ {% extends "network/hosts.html" %} -{% from "_helpers.html" import link_to_items, link_to_stack_members, delete_button_with_confirmation %} +{% from "_helpers.html" import link_to_items, link_to_stack_members, + delete_button_with_confirmation, render_field, submit_button_with_confirmation %} {% block title %}View Host - CSEntry{% endblock %} @@ -16,22 +17,41 @@ {% endblock %} {% block hosts_main %} - <dl class="row"> - <dt class="col-sm-3">Hostname</dt> - <dd class="col-sm-9">{{ host.name }}</dd> - <dt class="col-sm-3">Device Type</dt> - <dd class="col-sm-9">{{ host.device_type }}</dd> - {% if host.items %} - <dt class="col-sm-3">Items</dt> - <dd class="col-sm-9">{{ link_to_items(host.items) }}</dd> + <div class="row"> + <div class="col-sm-9"> + <dl class="row"> + <dt class="col-sm-3">Hostname</dt> + <dd class="col-sm-9">{{ host.name }}</dd> + <dt class="col-sm-3">Device Type</dt> + <dd class="col-sm-9">{{ host.device_type }}</dd> + {% if host.items %} + <dt class="col-sm-3">Items</dt> + <dd class="col-sm-9">{{ link_to_items(host.items) }}</dd> + {% endif %} + {% if host.stack_members() %} + <dt class="col-sm-3">Stack Members</dt> + <dd class="col-sm-9">{{ link_to_stack_members(host.stack_members()) }}</dd> + {% endif %} + <dt class="col-sm-3">Description</dt> + <dd class="col-sm-9">{{ host.description }}</dd> + </dl> + </div> + {% if host.device_type.name.startswith('Virtual') and current_user.is_admin %} + {% if host.is_ioc %} + {% set vm_type = 'Virtual IOC' %} + {% else %} + {% set vm_type = 'VM' %} {% endif %} - {% if host.stack_members() %} - <dt class="col-sm-3">Stack Members</dt> - <dd class="col-sm-9">{{ link_to_stack_members(host.stack_members()) }}</dd> + <div class="col-sm-3"> + <form id="createVMForm" method="POST"> + {{ form.hidden_tag() }} + {{ render_field(form.cores, label_size='6', input_size='6') }} + {{ render_field(form.memory, label_size='6', input_size='6') }} + {{ submit_button_with_confirmation('Create ' + vm_type, 'Do you really want to create the ' + vm_type + ' ' + host.name + '?') }} + </form> + </div> {% endif %} - <dt class="col-sm-3">Description</dt> - <dd class="col-sm-9">{{ host.description }}</dd> - </dl> + </div> <h3>Interfaces</h3> <table id="interfaces_table" class="table table-hover table-sm"> <thead> -- GitLab