From 70dc20cf1e825975faffdd6009763c4596c87b9a Mon Sep 17 00:00:00 2001
From: Attila Farkas <ati@mail.muni.cz>
Date: Fri, 6 Mar 2020 14:17:01 +0100
Subject: [PATCH] rewrite vagrantfile generation

---
 conf/flavors.yml             |  40 +++++++++
 conf/router_attributes.yml   |   3 +
 conf/vagrant_mapping.yml     |   6 --
 conf/virtualbox_mapping.yml  |   3 +
 create.py                    |   7 +-
 modules/ansible_generator.py |   6 ++
 modules/file_generator.py    |  52 ++---------
 modules/file_manager.py      |  39 ++++++++
 modules/preprocessing.py     |  44 ++++++++-
 modules/vagrant_generator.py | 169 ++++++++++++++++++++++++++++++++---
 templates/newvagrantfile     | 111 +++++++++++++++++++++++
 11 files changed, 414 insertions(+), 66 deletions(-)
 create mode 100644 conf/flavors.yml
 create mode 100644 conf/router_attributes.yml
 create mode 100644 conf/virtualbox_mapping.yml
 create mode 100644 modules/ansible_generator.py
 create mode 100644 modules/file_manager.py
 create mode 100644 templates/newvagrantfile

diff --git a/conf/flavors.yml b/conf/flavors.yml
new file mode 100644
index 0000000..55a4ceb
--- /dev/null
+++ b/conf/flavors.yml
@@ -0,0 +1,40 @@
+csirtmu.tiny1x2:
+    cores: 1
+    memory: 2048
+    hd: 20480
+csirtmu.tiny1x4:
+    cores: 1
+    memory: 4096
+    hd: 20480
+csirtmu.small2x4:
+    cores: 2
+    memory: 4096
+    hd: 20480
+csirtmu.small2x8:
+    cores: 2
+    memory: 8192
+    hd: 40960
+csirtmu.medium4x8:
+    cores: 4
+    memory: 8192
+    hd: 40960
+csirtmu.medium4x16:
+    cores: 4
+    memory: 16384
+    hd: 40960
+csirtmu.large8x16:
+    cores: 8
+    memory: 16384
+    hd: 81920
+csirtmu.large8x32:
+    cores: 8
+    memory: 32768
+    hd: 81920
+csirtmu.jumbo16x32:
+    cores: 16
+    memory: 32768
+    hd: 102400
+csirtmu.jumbo16x64:
+    cores: 16
+    memory: 65536
+    hd: 102400
diff --git a/conf/router_attributes.yml b/conf/router_attributes.yml
new file mode 100644
index 0000000..18b02a8
--- /dev/null
+++ b/conf/router_attributes.yml
@@ -0,0 +1,3 @@
+base_box: generic/debian10
+memory: 256
+cpus: 1
diff --git a/conf/vagrant_mapping.yml b/conf/vagrant_mapping.yml
index cc903ad..3d7abf6 100644
--- a/conf/vagrant_mapping.yml
+++ b/conf/vagrant_mapping.yml
@@ -73,9 +73,3 @@ other:
     vagrant_host: vagrant.host
     vagrant_plugins: vagrant.plugins
     vagrant_sensitive: vagrant.sensitive
-need_provider:
-    flavor: flavor
-    memory: memory
-    cpus: cpus
-    
-
diff --git a/conf/virtualbox_mapping.yml b/conf/virtualbox_mapping.yml
new file mode 100644
index 0000000..46e9575
--- /dev/null
+++ b/conf/virtualbox_mapping.yml
@@ -0,0 +1,3 @@
+integer:
+    memory: memory
+    cpus: cpus
diff --git a/create.py b/create.py
index 1b6c65f..845d52b 100644
--- a/create.py
+++ b/create.py
@@ -6,8 +6,9 @@ of virtual machines and network topology.  See the documentation for details.
 
 import sys
 
-from modules.file_generator import generate_vagrantfile, generate_ansible_files
-from modules.device_creator import open_file
+from modules.file_generator import generate_ansible_files # TODO change this to from modules.ansible_generator import generate_playbooks
+from modules.vagrant_generator import generate_vagrantfile
+from modules.file_manager import open_yaml
 from modules.input_argument_parser import parse_input_args
 from modules.input_file_validator import validate_device_definitions
 from modules.preprocessing import preprocess
@@ -24,7 +25,7 @@ except Exception:
 
 """ Parsing the definitions file. """
 try:
-    device_definitions = open_file(input_file_name)
+    device_definitions = open_yaml(input_file_name)
 except Exception:
     print("Definitions file could not be parsed.")
     sys.exit(1)
diff --git a/modules/ansible_generator.py b/modules/ansible_generator.py
new file mode 100644
index 0000000..9d20533
--- /dev/null
+++ b/modules/ansible_generator.py
@@ -0,0 +1,6 @@
+def generate_playbooks(input_definitions, flags):
+    """ Generates ansible playbooks.
+
+    :param definitions: device definitions structure
+    :param flags: command line input flags
+    """
\ No newline at end of file
diff --git a/modules/file_generator.py b/modules/file_generator.py
index dc5c932..087a0cc 100644
--- a/modules/file_generator.py
+++ b/modules/file_generator.py
@@ -1,8 +1,6 @@
 import jinja2
 import os
-import conf.border_router
 
-from modules.device_creator import create_devices
 from modules.ansible_data_generator import create_network_map, create_host_map, create_network_ips
 
 def _load_template(template_name):
@@ -44,44 +42,12 @@ def _create_role_directory(role_name, provisioning_dir):
         pass
 
 
-def _find_user_ansible_files(definitions):
-    """ Finds the user ansible files and returns a list of host names. """
-    host_names = []
-
-    for host in definitions["hosts"]:
-        if os.path.isfile("provisioning/" + host["name"] + ".yml" ):
-            host_names.append(host["name"])
-
-    return host_names
-
-def generate_vagrantfile(definitions, flags):
-    """
-    Creates Vagrantfile from definitions.
-
-    :param definitions: device definitions structure
-    :param flags: command line input flags
-    """
-    
-    if "ansible_local" in flags and flags["ansible_local"]:
-        ansible_local = True
-    else:
-        ansible_local = False
-
-    device_definitions = create_devices(definitions, ansible_local)
-    user_ansible_files = _find_user_ansible_files(definitions)
-    template = _load_template("vagrantfile")
-    output = template.render(devices=device_definitions, user_files=user_ansible_files, ansible_local=ansible_local)
-    _generate_file("Vagrantfile", output)
-    
-    print("Info: Vagrantfile successfully created.")
-
-
 def _generate_playbook(definitions):
     """ Generates the main playbook. """
 
     host_map = create_host_map(definitions["net_mappings"], definitions["router_mappings"], definitions["hosts"])
     network = create_network_map(definitions)
-    
+
     template = _load_template("playbook")
     output = template.render(hosts=host_map, routers=network)
 
@@ -97,7 +63,7 @@ def _generate_device_configuration(definitions):
     """ Generates a playbook with basic device configutarion. """
 
     host_map = create_host_map(definitions["net_mappings"], definitions["router_mappings"], definitions["hosts"])
-    network = create_network_map(definitions) 
+    network = create_network_map(definitions)
     network_ips = create_network_ips(definitions["networks"])
 
     template = _load_template("device_configuration")
@@ -123,7 +89,7 @@ def _generate_hosts_role(definitions):
 
     _create_role_directory("hosts", "base_provisioning")
     _generate_file("./base_provisioning/roles/hosts/tasks/main.yml", output)
-    
+
     user_template = _load_template("user_hosts")
     user_output = template.render()
 
@@ -133,7 +99,7 @@ def _generate_hosts_role(definitions):
 
 def _generate_separate_hosts_role(definitions):
     """ Generate roles for separate host devices. """
-    
+
     host_map = create_host_map(definitions["net_mappings"], definitions["router_mappings"], definitions["hosts"])
 
     for host in definitions["hosts"]:
@@ -166,7 +132,7 @@ def _generate_routers_role(definitions):
 
     host_map = create_host_map(definitions["net_mappings"], definitions["router_mappings"], definitions["hosts"])
 
-    network = create_network_map(definitions) 
+    network = create_network_map(definitions)
 
     template = _load_template("routers")
     output = template.render(hosts=host_map, routers=network, border_router_ip=BORDER_ROUTER_IP)
@@ -201,8 +167,8 @@ def _generate_br_role(definitions):
         print("Info: No router definition was found. Skipping border router creation.")
         return
 
-    network = create_network_map(definitions) 
-    
+    network = create_network_map(definitions)
+
     host_map = create_host_map(definitions["net_mappings"], definitions["router_mappings"], definitions["hosts"])
 
     routers_in_br_network = _get_br_routers(definitions)
@@ -220,8 +186,8 @@ def generate_ansible_files(definitions, flags):
 
     :param definitions: device definitions structure
     :param flags: command line input flags
-    """ 
-    
+    """
+
     _generate_playbook(definitions)
     _generate_device_configuration(definitions)
     _generate_hosts_role(definitions)
diff --git a/modules/file_manager.py b/modules/file_manager.py
new file mode 100644
index 0000000..e732883
--- /dev/null
+++ b/modules/file_manager.py
@@ -0,0 +1,39 @@
+""" This module handles file imports and creations in general. """
+
+import jinja2
+import yaml
+
+def open_yaml(file_name):
+    """ Opens and returns a file from the argument. """
+    try:
+        input_file = open(str(file_name))
+        return yaml.safe_load(input_file)
+    except IOError:
+        print("Error: Cannot open the required file: " + str(file_name))
+        raise
+
+def generate_file(filename, output_string):
+    """
+    Generates a file from output string.
+    
+    :param filename: name of the file to create
+    :param output_string: string to write to the file
+    """
+
+    try:
+        new_file = open(filename, "w")
+        new_file.write(output_string)
+    except IOError:
+        print("Error: cannot write to this location.")
+        raise
+
+def load_template(template_name):
+    """
+    Returns a loaded jinja2 template.
+    
+    :param template_name: name of the template file
+    """
+
+    template_loader = jinja2.FileSystemLoader(searchpath="templates")
+    template_env = jinja2.Environment(loader=template_loader, trim_blocks=True, lstrip_blocks=True)
+    return template_env.get_template(template_name)
diff --git a/modules/preprocessing.py b/modules/preprocessing.py
index f21b6cc..cc70c65 100644
--- a/modules/preprocessing.py
+++ b/modules/preprocessing.py
@@ -3,6 +3,37 @@ called after validating the input but before device creation.
 """
 
 from modules.border_router import create_border_router
+from modules.file_manager import open_yaml
+
+FLAVORS = open_yaml("conf/flavors.yml")
+ROUTER_ATTRIBUTES = open_yaml("conf/router_attributes.yml")
+
+
+def _configure_routers(definitions):
+    """ Adds predefined parameters to all routers if they are not defined in
+    the source yaml.
+    """
+
+    for router in definitions["routers"]:
+        for parameter, value in ROUTER_ATTRIBUTES.items():
+            if parameter not in router:
+                router[parameter] = value
+
+
+def _add_flavors(definitions): 
+    """ Changes flavor attribute to cpus and memory. """
+
+    for host in definitions["hosts"]:
+        if "flavor" in host:
+            if host["flavor"] not in FLAVORS:
+                print("Error: Not supported flavor: " + host["flavor"])
+                raise AttributeError
+            if "memory" not in host:
+                host["memory"] = FLAVORS[host["flavor"]]["memory"]
+            if "cpus" not in host:
+                host["memory"] = FLAVORS[host["flavor"]]["cpus"]
+            host.pop("flavor")
+
 
 def preprocess(definitions, flags):
     """
@@ -13,9 +44,20 @@ def preprocess(definitions, flags):
     :param flags: a structure with command line flags
     """
 
-    """ Creating Border router """
     try:
         create_border_router(definitions)
     except Exception:
         print("Could not create border router.")
         raise
+
+    try:
+        _configure_routers(definitions)
+    except Exception:
+        print("Could not add router configurations to definitions.")
+        raise
+
+    try:
+        _add_flavors(definitions)
+    except Exception:
+        print("Could not add flavor.")
+        raise
diff --git a/modules/vagrant_generator.py b/modules/vagrant_generator.py
index f0c6ee8..1539b2e 100644
--- a/modules/vagrant_generator.py
+++ b/modules/vagrant_generator.py
@@ -1,10 +1,134 @@
 """ This module generates a Vagrantfile from input device definitions. """
 
+import jinja2
 
-def _create_commands(device_name, device_type, input_definitions, flags):
+from modules.file_manager import load_template, generate_file, open_yaml
+
+VAGRANT_MAPPING = open_yaml("conf/vagrant_mapping.yml")
+VIRTUALBOX_MAPPING = open_yaml("conf/virtualbox_mapping.yml")
+
+
+def _create_simple_attribute(key, value, attribute_type):
+    """ Creates simple vagrant attributes like string, integer or boolean. """
+
+    attribute = dict()
+    attribute["type"] = attribute_type
+    attribute["command"] = key
+    attribute["value"] = value
+
+    return attribute
+
+
+def _create_commands(device_attributes, device_type, input_definitions, flags):
     """ This function creates basic vagrant definition commands for a device. """
 
-    # TODO create vagrant commands
+    commands = []
+    vb_commands = []
+
+    for attribute, value in device_attributes.items():
+        if attribute in VAGRANT_MAPPING["string"]:
+            commands.append(_create_simple_attribute(VAGRANT_MAPPING["string"][attribute], value, "string"))
+        elif attribute in VAGRANT_MAPPING["boolean"]:
+            commands.append(_create_simple_attribute(VAGRANT_MAPPING["boolean"][attribute], value, "boolean"))
+        elif attribute in VAGRANT_MAPPING["integer"]:
+            commands.append(_create_simple_attribute(VAGRANT_MAPPING["integer"][attribute], value, "integer"))
+        elif attribute in VIRTUALBOX_MAPPING["integer"]:
+            vb_commands.append(_create_simple_attribute(VIRTUALBOX_MAPPING["integer"][attribute], value, "integer"))
+
+    if vb_commands:
+        vb = dict()
+        vb["type"] = "provider"
+        vb["name"] = "virtualbox"
+        vb["commands"] = vb_commands
+        commands.append(vb)
+    
+    return commands
+
+
+def _create_ansible_commands(playbook_location, flags):
+    """ Creates commands for running a playbook from the Vagrantfile. """
+
+    commands = []
+    
+    playbook = dict()
+    playbook["type"] = "string"
+    playbook["command"] = "playbook"
+    playbook["value"] = playbook_location
+    commands.append(playbook)
+
+    if "verbose_ansible" in flags and flags["verbose_ansible"]:
+        verbosity = dict()
+        verbosity["type"] = "boolean"
+        verbosity["command"] = "verbose"
+        verbosity["value"] = True
+        commands.append(verbosity)
+
+    return commands
+
+
+def _find_netmask(network_name, networks):
+    """ Returns the netmask of a network address from network name. """
+
+    for network in networks:
+        if network['name'] == network_name:
+            address, netmask = network['cidr'].split('/')
+            return netmask
+
+
+def _add_networks_to_device(definition, mappings, input_definitions):
+    """ Adds networks to the vagrant definition of one device. """
+
+    for mapping in mappings:
+        if mapping[definition["type"]] == definition["name"]:
+            network = dict()
+            network["type"] = "network"
+            network["network_type"] = "private_network"
+            network["name"] = mapping["network"]
+            network["ip"] = mapping["ip"]
+            network["netmask"] = _find_netmask(mapping["network"], input_definitions["networks"])
+            definition["commands"].append(network)
+
+
+def _add_all_networks(vagrant_definitions, input_definitions, flags):
+    """ Adds all networks to vagrant definitions. """
+
+    for definition in vagrant_definitions:
+        if definition["type"] == "host":
+            _add_networks_to_device(definition, input_definitions["net_mappings"], input_definitions)      
+        elif definition["type"] == "router":
+            _add_networks_to_device(definition, input_definitions["router_mappings"], input_definitions)
+
+
+def _call_provisioner(flags):
+    """ Creates entry to vagrant definitions for calling the provisioner. """
+    
+    provisioner_calls = []
+
+    config_playbook = dict()
+    config_playbook["type"] = "provision"
+    if "ansible_local" in flags and flags["ansible_local"]:
+        config_playbook["provisioner"] = "ansible_local"
+    else:
+        config_playbook["provisioner"] = "ansible"
+    config_playbook["note"] = "basic configuration of devices and networks"
+    config_playbook["commands"] = _create_ansible_commands(
+            "base_provisioning/device_configuration.yml", flags)
+
+    provisioner_calls.append(config_playbook)
+
+    user_playbook = dict()
+    user_playbook["type"] = "provision"
+    if "ansible_local" in flags and flags["ansible_local"]:
+        user_playbook["provisioner"] = "ansible_local"
+    else:
+        user_playbook["provisioner"] = "ansible"
+    user_playbook["note"] = "user configuration of devices"
+    user_playbook["commands"] = _create_ansible_commands(
+            "base_provisioning/playbook.yml", flags)
+    
+    provisioner_calls.append(user_playbook)
+
+    return provisioner_calls
 
 
 def _build_vagrant_definitions(input_definitions, flags):
@@ -14,24 +138,39 @@ def _build_vagrant_definitions(input_definitions, flags):
     """
 
     vagrant_definitions = []
-
     for router in input_definitions["routers"]:
         device = dict()
-        device["device_name"] = router["name"]
-        device["device_type"] = "router"
-        device["commands"] = _create_commands(router["name"], "router", input_definitions, flags)
+        device["type"] = "router"
+        device["name"] = router["name"]
+        device["commands"] = _create_commands(router, "router", input_definitions, flags)
         vagrant_definitions.append(device)
 
-    for host in input_definitons["hosts"]:
+    for host in input_definitions["hosts"]:
         device = dict()
-        device["device_name"] = host["name"]
-        device["device_type"] = "host"
-        device["commands"] = _create_commands(host["name"], "host", input_definitions, flags)
+        device["type"] = "host"
+        device["name"] = host["name"]
+        device["commands"] = _create_commands(host, "host", input_definitions, flags)
         vagrant_definitions.append(device)
-        
+
+    _add_all_networks(vagrant_definitions, input_definitions, flags)
+    
+    vagrant_definitions.extend(_call_provisioner(flags))
+    
     return vagrant_definitions
 
 
+def _build_vagrantfile(vagrant_definitions):
+    """
+    Generates the Vagrantfile using the vagrantfile template and vagrant
+    definitions.
+    """
+
+# TODO change newvagrantfile to vagrantfile 
+    template = load_template("newvagrantfile")
+    output = template.render(defs=vagrant_definitions)
+    generate_file("Vagrantfile", output)
+    
+
 def generate_vagrantfile(input_definitions, flags):
     """
     This method is responsible for Vagrantfile generation.
@@ -42,8 +181,12 @@ def generate_vagrantfile(input_definitions, flags):
 
     try:
         vagrant_definitions = _build_vagrant_definitions(input_definitions, flags)
-   except Exception:
+    except Exception:
        print("Could not create definitions for Vagrantfile.")
        raise
 
-   # TODO build Vagrantfile using a template
+    try:
+       _build_vagrantfile(vagrant_definitions)
+    except Exception:
+       print("Could not generate Vagrantfile.")
+       raise
diff --git a/templates/newvagrantfile b/templates/newvagrantfile
new file mode 100644
index 0000000..c87f2c6
--- /dev/null
+++ b/templates/newvagrantfile
@@ -0,0 +1,111 @@
+# Vagrantfile generated by Sandbox Creator.
+#
+# -*- mode: ruby -*-
+# vi: set ft=ruby :
+
+{# Macro for router items #}
+{% macro router(item, namespace) %}
+  # device (router): {{ item.name }}
+  {{ namespace }}.vm.define "{{ item.name }}" do |device|
+{{ layer2(item.commands, "device") }}
+{% endmacro -%}
+
+{# Macro for host items #}
+{% macro host(item, namespace) %}
+  # device (host): {{ item.name }}
+  {{ namespace }}.vm.define "{{ item.name }}" do |device|
+{{ layer2(item.commands, "device") }}
+{% endmacro -%}
+
+{# Macro for provider items #}
+{% macro provider(item, namespace) %}
+  {{ namespace }}.vm.provider "{{ item.name }}" do |provider|
+{{ layer3(item.commands, "provider") }}
+{% endmacro -%}
+
+{# Macro for provision items #}
+{% macro provision(item, namespace) %}
+  # {{ item.note }}
+  {{ namespace }}.vm.provision :{{ item.provisioner }} do |provisioner|
+{{ layer2(item.commands, "provisioner") }}
+{% endmacro -%}
+
+{# Macro for string items #}
+{% macro string(item, namespace) %}
+  {{ namespace }}.{{ item.command }} = "{{ item.value }}"
+{% endmacro -%}
+
+{# Macro for boolean items #}
+{% macro boolean(item, namespace) %}
+{% if item.value %}
+  {{ namespace }}.{{ item.command }} = true
+{% else %}
+  {{ namespace }}.{{ item.command }} = false
+{% endif %}
+{% endmacro -%}
+
+{# Macro for integer items #}
+{% macro integer(item, namespace) %}
+  {{ namespace }}.{{ item.command }} = {{ item.value }}
+{% endmacro -%}
+
+{# Macro for network items #}
+{% macro network(item, namespace) %}
+  {{ namespace }}.vm.network :{{ item.network_type }}, ip: "{{ item.ip }}"
+{%- if item.netmask %}
+, netmask: "{{ item.netmask }}"
+{%- endif %}
+{%- if item.network_type == "private_network" %}
+, virtualbox__intnet: "{{ item.name }}"  
+{% endif %}
+{% endmacro -%}
+
+{# A macro that generates the first level of indentation. #}
+{% macro layer1(structure, namespace) %}
+{% for item in structure %}
+{% if item.type == "router" %}
+{{ router(item, namespace) }}
+{% elif item.type == "host" %}
+{{ host(item, namespace) }}
+{% elif item.type == "provision" %}
+{{ provision(item, namespace) }}
+{% endif %}
+{% endfor %}
+end
+{%- endmacro -%}
+
+{# A macro that generates the second level of indentation. #}
+{% macro layer2(structure, namespace) %}
+{% for item in structure %}
+{% if item.type == "string" %}
+  {{ string(item, namespace) -}}
+{% elif item.type == "boolean" %}
+  {{ boolean(item, namespace) -}}
+{% elif item.type == "integer" %}
+  {{ integer(item, namespace) -}}
+{% elif item.type == "provider" %}
+  {{ provider(item, namespace) -}}
+{% elif item.type == "network" %}
+  {{ network(item, namespace) -}}
+{% endif %}
+{% endfor %}
+  end
+{%- endmacro -%}
+
+{# A macro that generates the third level of indentation. #}
+{% macro layer3(structure, namespace) %}
+{% for item in structure %}
+{% if item.type == "string" %}
+    {{ string(item, namespace) -}}
+{% elif item.type == "boolean" %}
+    {{ boolean(item, namespace) -}}
+{% elif item.type == "integer" %}
+    {{ integer(item, namespace) -}}
+{% endif %}
+{% endfor %}
+    end
+{%- endmacro -%}
+
+Vagrant.configure("2") do |config|
+
+{{ layer1(defs, "config") -}}
-- 
GitLab