diff --git a/README.md b/README.md index fe7a5219ca4304d0b6ebf97a98235441ef61b46b..3dd81c9beff21e75dbe61312fa2475ffefe711a8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Sandbox Creator +# Cyber Sandbox Creator -Sandbox creator is a tool, which can generate portable input files for building lightweight virtual environments using Vagrant and Ansible from a simple YAML definition of topology. The combination of these three tools makes possible to build virtual machines connected with virtual networks even on a desktop computer. +Cyber Sandbox Creator is a tool, which can generate portable input files for building lightweight virtual environments using Vagrant and Ansible from a simple YAML definition of topology. The combination of these three tools makes possible to build virtual machines connected with virtual networks even on a desktop computer. ## Installation @@ -27,15 +27,19 @@ Sandbox creator is a tool, which can generate portable input files for building ### Linux (Ubuntu/Debian) -After the installation simply run the command `$ python3 create.py sandbox.yml` to generate the files. +1. After the installation simply run the command `$ python3 create.py topology.yml` to generate files. +2. Navigate to the newly created directory `sandbox` and run `$ vagrant up` to build the virtual environment. +3. The built environment can be deleted using the command `$ vagrant destroy -f`. ### Windows 10 -Generate the files using the command `python create.py -l sandbox.yml`. +1. Generate files using the command `python create.py --ansible_local topology.yml`. +2. Navigate to the newly created folder `sandbox` and run the command `vagrant up` to build the virtual environment. +3. The built environment can be deleted using the command `vagrant destroy -f`. ## Credits -**Cybersecurity laboratory**\ +**Cybersecurity Laboratory**\ **Faculty of Informatics**\ **Masaryk University** @@ -48,6 +52,7 @@ Generate the files using the command `python create.py -l sandbox.yml`. **Contributors**: - Valdemar Ĺ vábenskĂ˝ +- Kamil Andoniadis (KYPO cyber range platform) - Michal StanĂk - ZdenÄ›k Vydra - Adam Skrášek diff --git a/big_broker.yml b/big_broker.yml index 72fbd9515c1966a460944966a13367ec4eb9d39a..11921f92cb21a7615aace4231f7ab89335bbb03d 100644 --- a/big_broker.yml +++ b/big_broker.yml @@ -14,7 +14,7 @@ hosts: memory: 2048 - name: attacker - base_box: kalilinux/rolling-light + base_box: mu/kali-2019.4 memory: 2048 - name: client @@ -31,7 +31,7 @@ networks: cidr: 172.18.1.0/24 - name: internet - cidr: 10.1.0.0/16 + cidr: 10.1.0.0/24 net_mappings: - host: web @@ -48,11 +48,11 @@ net_mappings: - host: attacker network: internet - ip: 10.1.135.83 + ip: 10.1.0.83 - host: client network: internet - ip: 10.1.17.4 + ip: 10.1.0.4 router_mappings: - router: big-broker-router diff --git a/conf/border_router.py b/conf/border_router.py new file mode 100644 index 0000000000000000000000000000000000000000..4e28f249f30ae0eb822eee683f32c70a0518d6b0 --- /dev/null +++ b/conf/border_router.py @@ -0,0 +1,5 @@ +BORDER_ROUTER_NAME = "br" +BORDER_ROUTER_IP = "172.18.0.1" +BORDER_ROUTER_PUBLIC_IP = "172.18.10.1" +BORDER_ROUTER_NETWORK_NAME = "BR" +BORDER_ROUTER_NETWORK_IP = "172.18.0.0/24" diff --git a/conf/flavors.yml b/conf/flavors.yml new file mode 100644 index 0000000000000000000000000000000000000000..55a4cebae68f2388eb40203ac99e1fed4c54b4bf --- /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 0000000000000000000000000000000000000000..18b02a8dc939c4dabab70a166908cf386ff1459f --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..3d7abf6e6dea10495338a6020f080532a5ce8a33 --- /dev/null +++ b/conf/vagrant_mapping.yml @@ -0,0 +1,75 @@ +string: + base_mac: vm.base_mac + base_address: vm.base_address + base_box: vm.box + box_download_checksum: vm.box_download_checksum + box_download_checksum_type: vm.box_download_checksum_type + box_download_client_cert: vm.box_download_client_cert + box_download_ca_cert: vm.box_download_ca_cert + box_download_ca_path: vm.box_download_ca_path + box_version: vm.box_version + communicator: vm.communicator + name: vm.hostname + post_up_message: vm.post_up_message + ssh_config: ssh.config + ssh_export_command_template: ssh.export_command_template + ssh_host: ssh.host + ssh_password: ssh.password + ssh_proxy_command: ssh.proxy_command + ssh_remote_user: ssh.remote_user + ssh_shell: ssh.shell + ssh_sudo_command: ssh.sudo_command + ssh_username: ssh.username + winrm_username: winrm.username + winrm_password: winrm.password + winrm_host: winrm.host + winrm_codepage: winrm.codepage + winssh_proxy_command: winssh.proxy_command + winssh_shell: winssh.shell + winssh_export_command_template: winssh.export_command_template + winssh_sudo_command: winssh.sudo_command + winssh_upload_directory: winssh.upload_directory +integer: + boot_timeout: vm.boot_timeout + graceful_halt_timeout: vm.graceful_halt_timeout + ssh_guest_port: ssh.guest_port + ssh_port: ssh.port + winrm_port: winrm.port + winrm_guest_port: winrm.guest_port + winrm_timeout: winrm.timeout + winrm_retry_limit: winrm.retry_limit + winrm_retry_delay: winrm.retry_delay +boolean: + box_check_update: vm.box_check_update + box_download_insecure: vm.box_download_insecure + box_download_location_trusted: vm.box_download_location_trusted + ignore_box_vagrantfile: vm.ignore_box_vagrantfile + ssh_compression: ssh.compression + ssh_dsa_authentication: ssh.dsa_authentication + ssh_forward_agent: ssh.forward_agent + ssh_forward_x11: ssh.forward_x11 + ssh_insert_key: ssh.insert_key + ssh_keep_alive: ssh.keep_alive + ssh_keys_only: ssh.keys_only + ssh_paranoid: ssh.paranoid + ssh_pty: ssh.pty + winrm_basic_auth_only: winrm.basic_auth_only + winrm_ssl_peer_verification: winrm.ssl_peer_verification + winssh_forward_agent: winssh.forward_agent + winssh_keep_alive: winssh.keep_alive +other: + box_url: vm.box_url + guest: vm.guest + provider: vm.provider + provision: vm.provision + synced_folder: vm.synced_folder + usable_port_range: vm.usable_port_range + ssh_extra_args: ssh.extra_args + ssh_forward_env: ssh.forward_env + ssh_private_key_path: ssh.private_key_path + ssh_verify_host_key: ssh.verify_host_key + winrm_transport: winrm.transport + winssh_forward_env: winssh.forward_env + vagrant_host: vagrant.host + vagrant_plugins: vagrant.plugins + vagrant_sensitive: vagrant.sensitive diff --git a/conf/virtualbox_mapping.yml b/conf/virtualbox_mapping.yml new file mode 100644 index 0000000000000000000000000000000000000000..46e9575f636dfeae1763fa2e27a9629aba2e7adf --- /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 5ad7906f65143bf67d7be9a16a1240b1b3ae5591..19ba7678c619be82c27a558e77e40598737ca502 100644 --- a/create.py +++ b/create.py @@ -1,32 +1,26 @@ #!/usr/bin/python3 -""" A script that generates a Vagrantfile from yaml. """ +"""Generates Vagrantfile from YAML definition. -import sys -import jinja2 +Generate a Vagrantfile and ansible files from a YAML definition of virtual +machines and network topology. See the documentation for details. +""" -from modules.file_generator import generate_vagrantfile, generate_ansible_files -from modules.device_creator import open_file -from modules.routing import create_border_router +from modules.ansible_generator import generate_playbooks +from modules.vagrant_generator import generate_vagrantfile +from modules.file_manager import open_yaml, prepare_directories +from modules.input_argument_parser import parse_input_args +from modules.input_file_validator import validate_device_definitions +from modules.preprocessing import preprocess -if len(sys.argv) == 3: - if str(sys.argv[1]) == "-l": - ansible_local = True - input_file_name = str(sys.argv[2]) - elif str(sys.argv[2]) == "-l": - ansible_local = True - input_file_name = str(sys.argv[1]) - else: - print("Error: Expecting a yml file and optionally a flag -l.") - sys.exit() -elif len(sys.argv) == 2: - ansible_local = False - input_file_name = str(sys.argv[1]) -else: - print("Error: Expecting a yml file and optionally a flag -l.") - sys.exit() -device_definitions = open_file(input_file_name) -create_border_router(device_definitions) +INPUT_FILE_NAME, FLAGS = parse_input_args() +device_definitions = open_yaml(INPUT_FILE_NAME) -generate_vagrantfile(device_definitions, ansible_local) -generate_ansible_files(device_definitions) +validate_device_definitions(device_definitions) +preprocess(device_definitions, FLAGS) +prepare_directories(device_definitions) + +generate_vagrantfile(device_definitions, FLAGS) +generate_playbooks(device_definitions, FLAGS) + +print("Sandbox was successfully created.") diff --git a/modules/ansible_data_generator.py b/modules/ansible_data_generator.py deleted file mode 100644 index 99e8ee4a09a865f35f17c7eb21135506d539e373..0000000000000000000000000000000000000000 --- a/modules/ansible_data_generator.py +++ /dev/null @@ -1,64 +0,0 @@ -from modules.device_creator import open_file - -INTERFACE_FILE = "name_mapping/interface.yml" - -def create_network_map(definitions): - """ Creates a structure with network topology for Jinja2 template. """ - - routings = [] - - for router_mapping in definitions["router_mappings"]: - routing = dict() - routing["router_name"] = router_mapping["router"] - routing["router_network"] = router_mapping["network"] - routing["router_network_ip"] = _find_network_ip(router_mapping["network"], definitions) - routing["router_ip"] = router_mapping["ip"] - routings.append(routing) - - return routings - - -def _find_router_ip(network_name, router_mappings): - for router_mapping in router_mappings: - if router_mapping["network"] == network_name: - return router_mapping["ip"] - -def _find_network_ip(network_name, definitions): - for network in definitions["networks"]: - if network["name"] == network_name: - return network["cidr"] - -def _find_interface(host_name, hosts): - - for host in hosts: - if host["name"] == host_name: - interfaces = open_file(INTERFACE_FILE) - if host["base_box"] in interfaces: - return interfaces[host["base_box"]] - - return "eth1" - - -def create_host_map(net_mappings, router_mappings, host_list): - """ Creates a structure with hosts and their primary routers ip """ - - hosts = [] - - for net_mapping in net_mappings: - host = dict() - host["host_name"] = net_mapping["host"] - host["host_ip"] = net_mapping["ip"] - host["router_ip"] = _find_router_ip( - net_mapping["network"], router_mappings) - host["interface"] = _find_interface(net_mapping["host"], host_list) - hosts.append(host) - return hosts - -def create_network_ips(networks): - - network_ips = [] - for network in networks: - if network["cidr"] not in network_ips: - network_ips.append(network["cidr"]) - - return network_ips diff --git a/modules/ansible_generator.py b/modules/ansible_generator.py new file mode 100644 index 0000000000000000000000000000000000000000..182456da616c828701647fa0e9273cf08360de76 --- /dev/null +++ b/modules/ansible_generator.py @@ -0,0 +1,59 @@ +"""Contain functions for ansible file generation.""" + +from modules.file_manager import generate_file, copy_template_file +from modules.ansible_vars_generator import generate_ansible_vars +from conf.border_router import BORDER_ROUTER_NAME + + +def _create_config_playbooks(input_definitions, flags): + """Generate playbooks and roles for basic device configuration.""" + copy_template_file("device_configuration", + "base_provisioning/device_configuration.yml") + if input_definitions["hosts"]: + copy_template_file("hosts", + "base_provisioning/roles/hosts/tasks/main.yml") + + if input_definitions["routers"]: + copy_template_file("routers", + "base_provisioning/roles/routers/tasks/main.yml") + + for device in input_definitions["hosts"] + input_definitions["routers"]: + if "border_router" in flags and flags["border_router"] and\ + device["name"] == BORDER_ROUTER_NAME: + copy_template_file("br", "base_provisioning/roles/" + + device["name"] + "/tasks/main.yml") + else: + copy_template_file("separate_devices", "base_provisioning/roles/" + + device["name"] + "/tasks/main.yml") + + +def _create_user_playbooks(input_definitions): + """Generate template playbooks and roles for users.""" + copy_template_file("playbook", "provisioning/playbook.yml") + if input_definitions["hosts"]: + copy_template_file("user_hosts", + "provisioning/roles/hosts/tasks/main.yml") + if input_definitions["routers"]: + copy_template_file("user_routers", + "provisioning/roles/routers/tasks/main.yml") + + for host in input_definitions["hosts"]: + generate_file("user_separate_hosts", + "provisioning/roles/" + host["name"] + "/tasks/main.yml", + host_name=host["name"]) + + for router in input_definitions["routers"]: + generate_file("user_separate_routers", + "provisioning/roles/" + router["name"] + + "/tasks/main.yml", router_name=router["name"]) + + +def generate_playbooks(input_definitions, flags): + """Generate ansible vars and playbooks. + + :param input_definitions: device definitions structure + :param flags: command line input flags + """ + generate_ansible_vars(input_definitions, flags) + _create_config_playbooks(input_definitions, flags) + _create_user_playbooks(input_definitions) diff --git a/modules/ansible_vars_generator.py b/modules/ansible_vars_generator.py new file mode 100644 index 0000000000000000000000000000000000000000..14957318b207c51ed54376313c8855a12098e845 --- /dev/null +++ b/modules/ansible_vars_generator.py @@ -0,0 +1,139 @@ +"""Contains functions for generation of ansible variables and playbooks.""" + +from conf.border_router import BORDER_ROUTER_NETWORK_NAME +from modules.file_manager import generate_file, dump_to_yaml +from modules.routing_generator import add_routes + + +def _find_networks_of_device(name, input_definitions): + """Return a list of network names in which the device have an interface.""" + networks = [] + + for net_mapping in input_definitions["net_mappings"]: + if net_mapping["host"] == name: + networks.append(net_mapping["network"]) + + if len(networks) == 1: + return networks + + if len(networks) > 1: + print("Error: Hosts can have only one interface.") + raise AttributeError + + for router_mapping in input_definitions["router_mappings"]: + if router_mapping["router"] == name: + networks.append(router_mapping["network"]) + + return networks + + +def _add_aliases(device_name, input_definitions, flags): + """Generate aliases for the given device.""" + home_networks = _find_networks_of_device(device_name, input_definitions) + + aliases = dict() + + for host_mapping in input_definitions["net_mappings"]: + aliases[host_mapping["host"]] = host_mapping["ip"] + continue + + for router_mapping in input_definitions["router_mappings"]: + if router_mapping["network"] in home_networks: + aliases[router_mapping["router"]] = router_mapping["ip"] + continue + + for router in input_definitions["routers"]: + if router["name"] not in aliases: + if "border_router" not in flags: + print("Error: " + device_name + " has no connection to " + + router["name"] + " .") + raise AttributeError + for router_mapping in input_definitions["router_mappings"]: + if router_mapping["router"] == router["name"] and \ + router_mapping["network"] == BORDER_ROUTER_NETWORK_NAME: + aliases[router_mapping["router"]] = router_mapping["ip"] + continue + + for router in input_definitions["routers"]: + if router["name"] not in aliases: + print("Error: " + device_name + " has no connection to " + + router["name"] + " .") + raise AttributeError + + return aliases + + +def _generate_device_vars(input_definitions, flags): + """Generate vars files for all devices separately.""" + for target_host in input_definitions["hosts"]: + variables = dict() + variables["aliases"] = _add_aliases(target_host["name"], + input_definitions, flags) + variables["routes"] = add_routes(target_host["name"], "host", + input_definitions, flags) + dump_to_yaml(variables, "base_provisioning/roles/" + + target_host["name"] + "/vars/main.yml") + + for target_router in input_definitions["routers"]: + variables = dict() + variables["aliases"] = _add_aliases(target_router["name"], + input_definitions, flags) + variables["routes"] = add_routes(target_router["name"], "router", + input_definitions, flags) + dump_to_yaml(variables, "base_provisioning/roles/" + + target_router["name"] + "/vars/main.yml") + + +def _generate_hosts_vars(input_definitions, flags): + """Generate vars file for all hosts.""" + return + + +def _generate_routers_vars(input_definitions, flags): + """Generate vars file for all routers.""" + return + + +def _find_ip(device_name, input_definitions): + """Return a dictionary with all network names and ips of a device.""" + networks = dict() + + for host_mapping in input_definitions["net_mappings"]: + if host_mapping["host"] == device_name: + networks[host_mapping["network"]] = host_mapping["ip"] + + for router_mapping in input_definitions["router_mappings"]: + if router_mapping["router"] == device_name: + networks[router_mapping["network"]] = router_mapping["ip"] + + return networks + + +def _generate_config_vars(input_definitions, flags): + """Generate vars file for all devices.""" +# TODO check what vars needs to be in config +# TODO dump to yaml without jinja + hosts = [] + for host in input_definitions["hosts"]: + new_host = dict() + new_host["name"] = host["name"] + new_host["networks"] = _find_ip(host["name"], input_definitions) + hosts.append(new_host) + + routers = [] + for router in input_definitions["routers"]: + new_router = dict() + new_router["name"] = router["name"] + new_router["networks"] = _find_ip(router["name"], input_definitions) + routers.append(new_router) + + generate_file("config", "base_provisioning/config.yml", hosts=hosts, + routers=routers) + + +def generate_ansible_vars(input_definitions, flags): + """Generate files with variables for ansible.""" + _generate_config_vars(input_definitions, flags) + _generate_hosts_vars(input_definitions, flags) + _generate_routers_vars(input_definitions, flags) + _generate_device_vars(input_definitions, flags) diff --git a/modules/attribute_formatter.py b/modules/attribute_formatter.py deleted file mode 100644 index 4c6b6a128d7c2c0e0b6c55202294cbf470837f0d..0000000000000000000000000000000000000000 --- a/modules/attribute_formatter.py +++ /dev/null @@ -1,30 +0,0 @@ -""" This module handles the creation of simple vagrant commands from yaml -attributes. """ - -def _format_variable_name(key, variable_type, mappings): - """ Formats the variable to the required vagrantfile definition. """ - return "device." + mappings[variable_type][key] + " = " - - -def _format_and_add(key, value, mappings, device_definition): - """ Formats and adds the definition of a simple attribute. """ - - if key in mappings["string"]: - device_definition.append( - _format_variable_name(key, "string", mappings) - + '\"' + str(value) + '\"') - elif key in mappings["integer"]: - device_definition.append( - _format_variable_name(key, "integer", mappings) - + str(value)) - elif key in mappings["boolean"]: - device_definition.append( - _format_variable_name(key, "boolean", mappings) - + str(value).lower()) - - -def add_simple_commands(device, mappings, device_definition): - """ Adds definitions of string, integer and boolean vagrant attributes. """ - - for key, value in device.items(): - _format_and_add(key, value, mappings, device_definition) diff --git a/modules/border_router.py b/modules/border_router.py new file mode 100644 index 0000000000000000000000000000000000000000..9794bb293ccf2c955a024040e8ffbeb681a382e7 --- /dev/null +++ b/modules/border_router.py @@ -0,0 +1,68 @@ +"""Contains functions for border router creation.""" + +from conf.border_router import * + + +def _are_br_parameters_free(definitions): + """Check if border router parameters are not already taken.""" + for host in definitions["hosts"]: + if host["name"] == BORDER_ROUTER_NAME: + return False + + for router in definitions["routers"]: + if router["name"] == BORDER_ROUTER_NAME: + return False + + for network in definitions["networks"]: + if network["name"] == BORDER_ROUTER_NETWORK_NAME or \ + network["cidr"] == BORDER_ROUTER_NETWORK_IP: + return False + + for net_mapping in definitions["net_mappings"]: + if net_mapping["ip"] == BORDER_ROUTER_IP: + return False + + for router_mapping in definitions["router_mappings"]: + if router_mapping["ip"] == BORDER_ROUTER_IP: + return False + + return True + + +def _create_mappings_to_border_router(definitions): + """Create router_mapping entries from routers to border router.""" + for router in definitions["routers"]: + num = definitions["routers"].index(router) + 5 + if num > 255: + raise IndexError("Too many routers.") + + ip_address = BORDER_ROUTER_IP[:(0-len(str(num)))] + ip_address += str(num) + + border_router = {"router": router["name"], + "network": BORDER_ROUTER_NETWORK_NAME, + "ip": ip_address} + definitions["router_mappings"].append(border_router) + + +def create_border_router(definitions): + """Add the definition of border router to definitions. + + :param definitions: device definition structure + """ + # TODO this should be later moved to input check + if not _are_br_parameters_free(definitions): + raise ValueError("A device with the same name as border router " + "already exists.") + + _create_mappings_to_border_router(definitions) + + definitions["routers"].insert(0, {"name": BORDER_ROUTER_NAME}) + + networks = {"name": BORDER_ROUTER_NETWORK_NAME, + "cidr": BORDER_ROUTER_NETWORK_IP} + definitions["networks"].insert(0, networks) + + router_mappings = {"router": BORDER_ROUTER_NAME, "ip": BORDER_ROUTER_IP, + "network": BORDER_ROUTER_NETWORK_NAME} + definitions["router_mappings"].insert(0, router_mappings) diff --git a/modules/device_creator.py b/modules/device_creator.py deleted file mode 100644 index 12f67b6064f2c9a0c621f1854ac0dc04bb40b170..0000000000000000000000000000000000000000 --- a/modules/device_creator.py +++ /dev/null @@ -1,84 +0,0 @@ -""" This package handles file import and creation of an input structure for -Jinja2 """ - -import yaml -import sys - -from modules.attribute_formatter import add_simple_commands -from modules.provider import add_prov_attributes, add_router_specification -from modules.network_parser import add_networks, add_router_ip - -MAPPING_FILE = "name_mapping/mapping.yml" -FLAVORS_FILE = "name_mapping/flavors.yml" - - -def open_file(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 find a required file: " + str(file_name)) - sys.exit(1) - -def _add_provisioning(hostname, host_definitions): - """ Adds provisioning to the device if the file exists. """ - try: - provision_file = open("provision/" + str(hostname) + ".yml") - host_definitions[hostname].append("device.vm.provision \"ansible\" do |ansible|") - host_definitions[hostname].append(" ansible.playbook = \"provision/" + hostname + ".yml\"") - host_definitions[hostname].append("end") - except IOError: - pass - -def _add_rsync(box, host_name, definitions): - """ add rsync to debian machines """ - - if box == "generic/debian10": - definitions[host_name].append("# standard shared folder doesn't work on debian") - definitions[host_name].append("device.vm.synced_folder \".\", \"/vagrant\", type: \"rsync\", rsync__exclude: \".git/\"") - - if box == "kalilinux/rolling-light": - definitions[host_name].append("device.ssh.password = \"vagrant\"") - -def _create_hosts(yml, mappings, flavors, ansible_local): - """ Creates a dictionary with formatted definition of each host. """ - host_definitions = {} - - for host in yml['hosts']: - host_definitions[host['name']] = [] - - add_simple_commands(host, mappings, host_definitions[host['name']]) - add_networks(host["name"], yml, host_definitions) - add_prov_attributes( - host, flavors, mappings['need_provider'], host_definitions) - _add_provisioning(host["name"], host_definitions) - if ansible_local: - _add_rsync(host["base_box"], host["name"], host_definitions) - - return host_definitions - - -def _create_routers(yml, ansible_local): - """ Creates a dictionary with formatted definition of each router. """ - router_definitions = {} - - for router in yml['routers']: - router_definitions[router['name']] = [] - add_router_ip(router["name"], yml, router_definitions) - add_router_specification(router, router_definitions, ansible_local) - if ansible_local: - _add_rsync("generic/debian10", router["name"], router_definitions) - - return router_definitions - - -def create_devices(definitions, ansible_local): - """ Returns a merged dictionary of host and router definitions. """ - - mappings = open_file(MAPPING_FILE) - flavors = open_file(FLAVORS_FILE) - - return { - **_create_hosts(definitions, mappings, flavors, ansible_local), - **_create_routers(definitions, ansible_local)} diff --git a/modules/file_generator.py b/modules/file_generator.py deleted file mode 100644 index d73861e69095902cdff418c15719fe3962cbd528..0000000000000000000000000000000000000000 --- a/modules/file_generator.py +++ /dev/null @@ -1,217 +0,0 @@ -import jinja2 -import os - -from modules.device_creator import create_devices -from modules.ansible_data_generator import create_network_map, create_host_map, create_network_ips -from modules.routing import BORDER_ROUTER_IP, BORDER_ROUTER_NAME, BORDER_ROUTER_NETWORK_NAME, BORDER_ROUTER_PUBLIC_IP - -def _load_template(template_name): - """ Returns a loaded jinja2 template. """ - - template_loader = jinja2.FileSystemLoader(searchpath="templates") - template_env = jinja2.Environment(loader=template_loader, trim_blocks=True) - return template_env.get_template(template_name) - - -def _generate_file(filename, output_string): - """ Generates a file from output string. """ - - try: - new_file = open(filename, "w") - new_file.write(output_string) - except IOError: - print("Error: cannot write to this location.") - - -def _create_role_directory(role_name, provisioning_dir): - """ Creates directory structure for a role. """ - - try: - os.mkdir(provisioning_dir) - except FileExistsError: - pass - try: - os.mkdir(provisioning_dir + "/roles") - except FileExistsError: - pass - try: - os.mkdir(provisioning_dir + "/roles/" + role_name) - except FileExistsError: - pass - try: - os.mkdir(provisioning_dir + "/roles/" + role_name +"/tasks") - except FileExistsError: - 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, ansible_local): - """ Writes the prepared output to a Vagrantfile. """ - - 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) - - try: - os.mkdir("provisioning") - except FileExistsError: - pass - - _generate_file("./provisioning/playbook.yml", output) - - -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_ips = create_network_ips(definitions["networks"]) - - template = _load_template("device_configuration") - output = template.render(hosts=host_map, routers=network, network_ips=network_ips, border_router_name = BORDER_ROUTER_NAME) - - try: - os.mkdir("base_provisioning") - except FileExistsError: - pass - - _generate_file("./base_provisioning/device_configuration.yml", output) - - -def _generate_hosts_role(definitions): - """ Generates hosts role. """ - - host_map = create_host_map(definitions["net_mappings"], definitions["router_mappings"], definitions["hosts"]) - - network = create_network_map(definitions) - - template = _load_template("hosts") - output = template.render(hosts=host_map, routers=network) - - _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() - - _create_role_directory("hosts", "provisioning") - _generate_file("./provisioning/roles/hosts/tasks/main.yml", output) - - -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"]: - - for host_attributes in host_map: - if host_attributes["host_name"] == host["name"]: - host_name = host_attributes["host_name"] - router_ip = host_attributes["router_ip"] - interface = host_attributes["interface"] - - template = _load_template("separate_hosts") - output = template.render(host_name=host_name, router_ip=router_ip, interface=interface) - - _create_role_directory(host["name"], "base_provisioning") - _generate_file("./base_provisioning/roles/" + host["name"] + "/tasks/main.yml", output) - - - template = _load_template("user_separate_hosts") - output = template.render(host_name=host_name) - - _create_role_directory(host["name"], "provisioning") - _generate_file("./provisioning/roles/" + host["name"] + "/tasks/main.yml", output) - -def _generate_routers_role(definitions): - """ Generates routers role. """ - - if not definitions['routers'] or not definitions['router_mappings']: - print("Info: No router definition was found. Skipping router creation.") - return - - host_map = create_host_map(definitions["net_mappings"], definitions["router_mappings"], definitions["hosts"]) - - network = create_network_map(definitions) - - template = _load_template("routers") - output = template.render(hosts=host_map, routers=network, border_router_ip=BORDER_ROUTER_IP) - - _create_role_directory("routers", "base_provisioning") - _generate_file("./base_provisioning/roles/routers/tasks/main.yml", output) - - -def _find_cidr(network_name, definitions): - """ Finds cidr of a network from name. """ - - for network in definitions["networks"]: - if network["name"] == network_name: - return network["cidr"] - -def _get_br_routers(definitions): - """ Returns a list of router ips that are in the border router network. """ - - br_mappings = dict() - for router_mapping in definitions["router_mappings"]: - if router_mapping["network"] == BORDER_ROUTER_NETWORK_NAME: - for router_mapping2 in definitions["router_mappings"]: - if router_mapping["router"] == router_mapping2["router"] and router_mapping["network"] != router_mapping2["network"]: - br_mappings[_find_cidr(router_mapping2["network"], definitions)] = router_mapping["ip"] - - return br_mappings - -def _generate_br_role(definitions): - """ Generates br role. """ - - if not definitions['routers'] or not definitions['router_mappings']: - print("Info: No router definition was found. Skipping border router creation.") - return - - 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) - - template = _load_template("br") - output = template.render(hosts = host_map, routers=network, br_routes=routers_in_br_network, border_router_name=BORDER_ROUTER_NAME, border_router_public_ip=BORDER_ROUTER_PUBLIC_IP) - - _create_role_directory("br", "base_provisioning") - _generate_file("./base_provisioning/roles/br/tasks/main.yml", output) - - -def generate_ansible_files(device_definitions): - """ Generates files for ansible. """ - - _generate_playbook(device_definitions) - _generate_device_configuration(device_definitions) - _generate_hosts_role(device_definitions) - _generate_separate_hosts_role(device_definitions) - _generate_routers_role(device_definitions) - _generate_br_role(device_definitions) - - print("Info: Ansible files successfully created.") diff --git a/modules/file_manager.py b/modules/file_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..8b4a8b541ae606b60c98595a947118e8bab56aa9 --- /dev/null +++ b/modules/file_manager.py @@ -0,0 +1,158 @@ +"""This module handles file imports and creations in general.""" + +import os +import shutil +import sys + +import jinja2 +import yaml + +OUTPUT_DIRECTORY = "sandbox" + + +def open_yaml(file_name): + """Open and return a file from the argument.""" + try: + with open(str(file_name)) as input_file: + return yaml.safe_load(input_file) + except yaml.YAMLError: + cleanup_and_exit("Could not parse yaml file " + str(file_name) + ".") + except IOError: + cleanup_and_exit("Could not open yaml file " + str(file_name) + ".") + + +def copy_template_file(template, destination): + """Copy playbook templates directly to the destination.""" + try: + shutil.copyfile("templates/" + template, "sandbox/" + destination) + except IOError: + cleanup_and_exit("Could not copy template file " + str(template) + + ".") + + +def dump_to_yaml(data, filename): + """Write a data structure to a YAML document. + + :param data: a dict or list which should be written to file + :param filename: name of the target file + """ + file_path = OUTPUT_DIRECTORY + "/" + str(filename) + + try: + with open(file_path, 'w') as stream: + yaml.dump(data, stream) + except IOError: + cleanup_and_exit("Could not create yaml file " + file_path + ".") + + +def generate_file(template, filename, **template_args): + """Generate a file using a template. + + :param template: name of the template file + :param filename: path to the output file inside the output directory + :param template_args: arbitrary number of named args for the template + """ + inventory_template = _load_template(template) + output = inventory_template.render(**template_args) + _write_to_file(filename, output) + + +def _write_to_file(filename, output_string): + """Generate a file from output string.""" + try: + new_file = open(OUTPUT_DIRECTORY + "/" + filename, "w") + new_file.write(output_string) + except IOError: + cleanup_and_exit("Could not create file " + str(filename) + ".") + + +def _load_template(template_name): + """Return a loaded jinja2 template.""" + try: + 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) + except jinja2.TemplateNotFound: + cleanup_and_exit("Could not find template " + str(template_name) + ".") + + +def _copy_directory(source, destination): + """Copy directory recursively form templates dir to the destination.""" + try: + shutil.copytree("./templates/" + source, destination) + except OSError: + cleanup_and_exit("Could not copy directory " + str(source) + ".") + + +def _create_provisioning_directories(directory, device_definitions): + """Create subdirectories for privisioning.""" + try: + os.mkdir(OUTPUT_DIRECTORY + "/" + directory) + os.mkdir(OUTPUT_DIRECTORY + "/" + directory + "/roles") + + if device_definitions["hosts"]: + os.mkdir(OUTPUT_DIRECTORY + "/" + directory + "/roles/hosts") + os.mkdir(OUTPUT_DIRECTORY + "/" + directory + "/roles/hosts/tasks") + os.mkdir(OUTPUT_DIRECTORY + "/" + directory + "/roles/hosts/vars") + for host in device_definitions["hosts"]: + os.mkdir(OUTPUT_DIRECTORY + "/" + directory + "/roles/" + + host["name"]) + os.mkdir(OUTPUT_DIRECTORY + "/" + directory + "/roles/" + + host["name"] + "/tasks") + os.mkdir(OUTPUT_DIRECTORY + "/" + directory + "/roles/" + + host["name"] + "/vars") + + if device_definitions["routers"]: + os.mkdir(OUTPUT_DIRECTORY + "/" + directory + "/roles/routers") + os.mkdir(OUTPUT_DIRECTORY + "/" + directory + + "/roles/routers/tasks") + os.mkdir(OUTPUT_DIRECTORY + "/" + directory + + "/roles/routers/vars") + for router in device_definitions["routers"]: + os.mkdir(OUTPUT_DIRECTORY + "/" + directory + "/roles/" + + router["name"]) + os.mkdir(OUTPUT_DIRECTORY + "/" + directory + "/roles/" + + router["name"] + "/tasks") + os.mkdir(OUTPUT_DIRECTORY + "/" + directory + "/roles/" + + router["name"] + "/vars") + + except FileExistsError: + pass + except IOError: + cleanup_and_exit("Could not create directories for provisioning.") + + +def prepare_directories(device_definitions): + """Prepare the necessary directory structure.""" + _remove_sandbox() + + try: + os.mkdir(OUTPUT_DIRECTORY) + except IOError: + cleanup_and_exit("Could not create directory ./" + OUTPUT_DIRECTORY + + ".") + + _create_provisioning_directories("base_provisioning", device_definitions) + _create_provisioning_directories("provisioning", device_definitions) + + _copy_directory("interface", OUTPUT_DIRECTORY + + "/base_provisioning/roles/interface") + _copy_directory("common", OUTPUT_DIRECTORY + + "/base_provisioning/roles/common") + + +def _remove_sandbox(): + """Remove the existing sandbox.""" + shutil.rmtree(OUTPUT_DIRECTORY, True) + + +def cleanup_and_exit(error): + """Cleanup function that is called in case of an error.""" + _remove_sandbox() + + print("Sandbox creation was NOT successful:") + print("Error: ", error) + + sys.exit(1) diff --git a/modules/input_argument_parser.py b/modules/input_argument_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..7d5c43fc97921f8aca9fa94b1191afd9d584b426 --- /dev/null +++ b/modules/input_argument_parser.py @@ -0,0 +1,34 @@ +"""This module contains functions to parse input arguments.""" + +import argparse + + +def parse_input_args(): + """Parse the given input arguments for input file name and flags. + + :returns: name of the input file and flags + """ + + input_file_name = None + flags = dict() + parser = argparse.ArgumentParser() + + parser.add_argument("definition_file", + help="path to the input yaml definition") + parser.add_argument("--ansible_local", + help="uses ansible_local for provisioning instead " + "of ansible", action="store_true") + parser.add_argument("--verbose_ansible", + help="sets verbose output for ansible (-vv)", + action="store_true") + parser.add_argument("--border_router", + help="creates a border router with connection to " + "all routers", action="store_true") + args = parser.parse_args() + + input_file_name = args.definition_file + flags["ansible_local"] = args.ansible_local + flags["verbose_ansible"] = args.verbose_ansible + flags["border_router"] = args.border_router + + return input_file_name, flags diff --git a/modules/input_file_validator.py b/modules/input_file_validator.py new file mode 100644 index 0000000000000000000000000000000000000000..8faf8328fc15ea334f8bafb7ba49615c428a584e --- /dev/null +++ b/modules/input_file_validator.py @@ -0,0 +1,10 @@ +"""This module contains functions for input file validation.""" + + +def validate_device_definitions(definitions): + """Validate the device definitions structure. + + Throw an error if the syntax is not as expected. + :param definitions: a device definition structure (dictionary) + """ + # TODO implement validation diff --git a/modules/network_parser.py b/modules/network_parser.py deleted file mode 100644 index a7224025003e77dd926ce0809e41df667cf94817..0000000000000000000000000000000000000000 --- a/modules/network_parser.py +++ /dev/null @@ -1,83 +0,0 @@ -""" This module handles network creation. """ - -import sys -from modules.routing import BORDER_ROUTER_NAME, BORDER_ROUTER_PUBLIC_IP - - -def _find_networks(hostname, mappings, device_type): - """ Matches the device to networks. Returns a list of maiching networks. """ - network_list = [] - - for mapping in mappings: - if device_type in ('host', 'router'): - if mapping[device_type] and mapping[device_type] == hostname: - network_list.append(mapping) - - return network_list - -def _add_ip(hostname, mappings, device_type, definitions): - """ Adds a formatted ip address and network name to device definition. """ - - networks = _find_networks(hostname, mappings, device_type) - - for network in networks: - if not network["ip"]: - print("Cannot find network mapping.") - sys.exit() - - definitions[hostname].append( - "device.vm.network :private_network, ip: \"" - + network["ip"] + '\", virtualbox__intnet: ' + network["network"]) - - - - -def _add_netmask(hostname, my_network, networks, definitions): - """ Adds netmask to the end of a formatted ip definition. """ - - for network in networks: - if network['name'] == my_network: - address, mask = network['cidr'].split('/') - definitions[hostname][-1] += (', netmask: \"' + mask + "\"") - - -def _add_interfaces(hostname, mapping, device_type, networks, definitions): - """ Adds all network interfaces to a device. """ - - if not mapping["ip"]: - print("Cannot find network mapping.") - sys.exit() - - definitions[hostname].append( - "device.vm.network :private_network, ip: \"" + mapping["ip"] - + "\", virtualbox__intnet: \"" + mapping["network"] + "\"") - - if hostname == BORDER_ROUTER_NAME: - definitions[BORDER_ROUTER_NAME].append("device.vm.network :public_network, ip: \" " + BORDER_ROUTER_PUBLIC_IP + "\"") - - _add_netmask(hostname, mapping["network"], networks, definitions) - -def add_networks(hostname, yml, definitions): - """ Adds ip address and natmask to a host. """ - - if not yml['net_mappings']: - return - - for mapping in yml['net_mappings']: - if mapping['host'] == hostname: - _add_interfaces( - hostname, mapping, - 'host', yml['networks'], definitions) - - -def add_router_ip(routername, yml, definitions): - """ Adds ip address to a router. """ - - if not yml['router_mappings']: - return - - for mapping in yml['router_mappings']: - if mapping['router'] == routername: - _add_interfaces( - routername, mapping, - 'router', yml['networks'], definitions) diff --git a/modules/preprocessing.py b/modules/preprocessing.py new file mode 100644 index 0000000000000000000000000000000000000000..a1c8d4764097ca661f43ca0a2059f8a5d2c8acd0 --- /dev/null +++ b/modules/preprocessing.py @@ -0,0 +1,101 @@ +"""This module contains functions for preprocessing. + +They are supposed to be called after validating the input but before device +creation. +""" + +import itertools + +from modules.border_router import create_border_router +from modules.file_manager import open_yaml, cleanup_and_exit + +FLAVORS = open_yaml("conf/flavors.yml") +ROUTER_ATTRIBUTES = open_yaml("conf/router_attributes.yml") + + +def _add_missing_tags(definitions): + """Add necessary structures to the input if they are missing.""" + if "routers" not in definitions: + definitions["routers"] = [] + if "router_mappings" not in definitions: + definitions["router_mappings"] = [] + if "hosts" not in definitions: + definitions["hosts"] = [] + if "net_mappings" not in definitions: + definitions["net_mappings"] = [] + if "networks" not in definitions: + definitions["networks"] = [] + + +def _configure_routers(definitions): + """Add predefined parameters to all routers. + + Adds them 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_extra_arguments(definitions, flags): + """Add extra arguments to definitions if they are not present.""" + if "border_router" in flags and flags["ansible_local"]: + for device in itertools.chain(definitions["hosts"], + definitions["routers"]): + if "synced_folder" not in device: + device["synced_folder"] = "\".\", \"/vagrant\", type: \"rsync"\ + "\", rsync__exclude: \".git/\"" + + +def _add_flavors(definitions): + """Change 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["cpus"] = FLAVORS[host["flavor"]]["cores"] + host.pop("flavor") + + +def preprocess(definitions, flags): + """Run preprocessing. + + Handles perations that need to be done before the actual device creation. + :param definitions: device definition structure + :param flags: a structure with command line flags + """ + try: + _add_missing_tags(definitions) + except Exception: + cleanup_and_exit("Preprocessing not successful: " + "Could not add missing tags.") + + try: + if "border_router" in flags and flags["border_router"]: + create_border_router(definitions) + except (ValueError, IndexError) as error: + cleanup_and_exit("Preprocessing not successful: " + "Could not create border router (" + error + ")") + + try: + _configure_routers(definitions) + except Exception: + cleanup_and_exit("Preprocessing not successful: " + "Could not add router configurations to definitions.") + + try: + _add_extra_arguments(definitions, flags) + except Exception: + cleanup_and_exit("Preprocessing not successful: " + "Could not add extra arguments to definitions.") + + try: + _add_flavors(definitions) + except Exception: + cleanup_and_exit("Preprocessing not successful: " + "Could not add flavors.") diff --git a/modules/provider.py b/modules/provider.py deleted file mode 100644 index f3279ce8ef0d39b919ee58b1d5746b2d2e39b0a5..0000000000000000000000000000000000000000 --- a/modules/provider.py +++ /dev/null @@ -1,67 +0,0 @@ -""" This module handles VirtualBox attributes. """ - - -def _print_flavor(host, flavors, provider_attributes, definitions): - """ Formats and add a flavor for a device. """ - - if 'memory' not in host: - definitions[host['name']].append( - ' vb.' + provider_attributes['memory'] + ' = ' - + str(flavors[host['flavor']]['memory'])) - if 'cpus' not in host: - definitions[host['name']].append( - ' vb.' + provider_attributes['cpus'] + ' = ' - + str(flavors[host['flavor']]['cores'])) - - -def _add_params(host, flavors, provider_attributes, definitions): - """ Formats and adds simple provision attributes. """ - - if 'memory' in host: - definitions[host['name']].append( - ' vb.' + provider_attributes['memory'] + ' = ' - + str(host['memory'])) - if 'cpus' in host: - definitions[host['name']].append( - ' vb.' + provider_attributes['cpus'] + ' = ' - + str(host['cpus'])) - if 'flavor' in host and host['flavor'] in flavors: - _print_flavor(host, flavors, provider_attributes, definitions) - - -def _need_provider(host, provider_attributes): - """ Checks if provision attributes are present. """ - - for attribute in provider_attributes: - if attribute in host: - return True - return False - - -def add_prov_attributes(host, flavors, provider_attributes, definitions): - """ Adds provider attributes. """ - - if _need_provider(host, provider_attributes): - definitions[host['name']].append( - "device.vm.provider \"virtualbox\" do |vb|") - _add_params(host, flavors, provider_attributes, definitions) - definitions[host['name']].append("end") - - -def add_router_specification(router, definitions, ansible_local): - """ Adds the default specification for a router. """ - - router_box = "generic/debian10" - router_memory = 256 - router_cpus = 1 - - definitions[router['name']].append( - "device.vm.hostname = \"" + router['name'] + "\"") - definitions[router['name']].append( - "device.vm.box = \"" + router_box + "\"") - definitions[router['name']].append( - "device.vm.provider \"virtualbox\" do |vb|") - definitions[router['name']].append(" vb.memory = " + str(router_memory)) - definitions[router['name']].append( - " vb.cpus = " + str(router_cpus)) - definitions[router['name']].append("end") diff --git a/modules/routing.py b/modules/routing.py deleted file mode 100644 index f1b556f0b45f4292075b1c8c56ffe352f2f80ca0..0000000000000000000000000000000000000000 --- a/modules/routing.py +++ /dev/null @@ -1,73 +0,0 @@ -import sys - -BORDER_ROUTER_NAME = "br" -BORDER_ROUTER_IP = "172.18.0.1" -BORDER_ROUTER_PUBLIC_IP = "172.18.10.1" -BORDER_ROUTER_NETWORK_NAME = "BR" -BORDER_ROUTER_NETWORK_IP = "172.18.0.0/24" - -def _are_br_parameters_free(definitions): - """ Checks if border router parameters are not already taken. """ - - for host in definitions["hosts"]: - if host["name"] == BORDER_ROUTER_NAME: - return False - - for router in definitions["routers"]: - if router["name"] == BORDER_ROUTER_NAME: - return False - - for network in definitions["networks"]: - if network["name"] == BORDER_ROUTER_NETWORK_NAME or network["cidr"] == BORDER_ROUTER_NETWORK_IP: - return False - - for net_mapping in definitions["net_mappings"]: - if net_mapping["ip"] == BORDER_ROUTER_IP: - return False - - - for router_mapping in definitions["router_mappings"]: - if router_mapping["ip"] == BORDER_ROUTER_IP: - return False - - return True - - -def _create_mappings_to_border_router(definitions): - """ Creates router_mapping entries from routers to border router. """ - - for router in definitions["routers"]: - num = definitions["routers"].index(router) + 5 - if num > 255: - print("Error: too many routers.") - sys.exit(1) - - ip = BORDER_ROUTER_IP[:(0-len(str(num)))] - ip += str(num) - - definitions["router_mappings"].append({"router":router["name"] - ,"network":BORDER_ROUTER_NETWORK_NAME - ,"ip":ip}) - - -def create_border_router(definitions): - """ Adds the definition of border router to definitions """ - - # TODO this should be later moved to input check - if not _are_br_parameters_free: - print("Error: Device parameter conflict.") - - """ Last number in the ip of routers in border network. """ - router_n = 5 - - _create_mappings_to_border_router(definitions) - - - definitions["routers"].append({"name":BORDER_ROUTER_NAME }) - definitions["networks"].append({"name":BORDER_ROUTER_NETWORK_NAME - ,"cidr":BORDER_ROUTER_NETWORK_IP}) - definitions["router_mappings"].append({"router":BORDER_ROUTER_NAME - ,"network":BORDER_ROUTER_NETWORK_NAME - ,"ip":BORDER_ROUTER_IP} ) - - diff --git a/modules/routing_generator.py b/modules/routing_generator.py new file mode 100644 index 0000000000000000000000000000000000000000..58f528ebf8fc50e7df676bf1b8a66880750950b3 --- /dev/null +++ b/modules/routing_generator.py @@ -0,0 +1,225 @@ +"""Contains generation of vars for routing of different types of devices.""" + +from conf.border_router import BORDER_ROUTER_NAME, BORDER_ROUTER_NETWORK_NAME,\ + BORDER_ROUTER_IP, BORDER_ROUTER_NETWORK_IP + + +def _find_router_in_network(network_name, input_definitions): + """Find a router in a network and return its ip.""" + for router_mapping in input_definitions["router_mappings"]: + if router_mapping["network"] == network_name: + return router_mapping["ip"] + return None + + +def _find_networks_of_device(device_name, input_definitions): + """Return the names of all networks a device is in.""" + networks = [] + + for mapping in input_definitions["router_mappings"]: + if mapping["router"] == device_name: + networks.append(mapping["network"]) + if networks: + return networks + + for mapping in input_definitions["net_mappings"]: + if mapping["host"] == device_name: + networks.append(mapping["network"]) + + return networks + + +def _find_iface_ip_in_network(device_name, network, input_definitions): + """Return the ip of the device interface inside a network.""" + for router_mapping in input_definitions["router_mappings"]: + if router_mapping["router"] == device_name and \ + router_mapping["network"] == network: + return router_mapping["ip"] + + for net_mapping in input_definitions["net_mappings"]: + if net_mapping["host"] == device_name and \ + net_mapping["network"] == network: + return net_mapping["ip"] + + print("Error: Could not find an interface for the device \"" + + device_name + "\" in the network \"" + network + "\".") + raise AttributeError + + +def _find_router_ip_in_br_network(other_network, input_definitions): + """Find a router inside of a network and returns its ip in br network.""" + router = None + + for router_mapping in input_definitions["router_mappings"]: + if router_mapping["network"] == other_network: + router = router_mapping["router"] + + if not router: + return None + + for router_mapping in input_definitions["router_mappings"]: + if router_mapping["router"] == router and\ + router_mapping["network"] == BORDER_ROUTER_NETWORK_NAME: + return router_mapping["ip"] + + +def _find_netmask(ip, input_definitions): + """Find netmask to an ip.""" + network_name = None + for mapping in [*input_definitions["net_mappings"],\ + *input_definitions["router_mappings"]]: + if mapping["ip"] == ip: + network_name = mapping["network"] + break + + if not network_name: + raise AttributeError("ip " + ip + " was not found.") + + for network in input_definitions["networks"]: + if network["name"] == network_name: + net_ip, netmask = network["cidr"].split("/") + return netmask + + +def _add_interface_route(route, target_interface_ip, interface_mask, + target_routes_list): + """Add route to list of routes with the given interface.""" + for interface in target_routes_list: + if interface["interface_ip"] == target_interface_ip: + interface["interface_routes"].append(route) + return + + new_interface = dict() + new_interface["interface_ip"] = target_interface_ip + new_interface["interface_netmask"] = interface_mask + new_interface["interface_default_gateway"] = "" + new_interface["interface_routes"] = [route] + target_routes_list.append(new_interface) + + +def _add_default_route(default_gateway, target_interface_ip, interface_mask, + target_routes_list): + """Add default route to the given interface.""" + for interface in target_routes_list: + if interface["interface_ip"] == target_interface_ip: + interface["interface_default_gateway"] = default_gateway + return + + new_interface = dict() + new_interface["interface_ip"] = target_interface_ip + new_interface["interface_netmask"] = interface_mask + new_interface["interface_default_gateway"] = default_gateway + new_interface["interface_routes"] = [] + target_routes_list.append(new_interface) + + +def _configure_auto_on_ansible_interface(target_routes_list, + is_border_router=False): + """Call interfaces role on the main interface to set auto.""" + new_interface = dict() + new_interface["interface_ip"] = "{{ ansible_default_ipv4.address | " \ + "default(ansible_all_ipv4_addresses[0]) }}" + new_interface["interface_netmask"] = "{{ ansible_default_ipv4.netmask | " \ + "default('24') }}" + if is_border_router: + new_interface["interface_default_gateway"] =\ + "{{ ansible_default_ipv4.gateway }}" + target_routes_list.insert(0, new_interface) + + +def _create_host_routing(target_host_name, input_definitions, flags): + """Generate list of routes for the given host.""" + routes = [] + if "border_router" in flags and flags["border_router"]: + _configure_auto_on_ansible_interface(routes) + mapping = None + + for host_mapping in input_definitions["net_mappings"]: + if host_mapping["host"] == target_host_name: + mapping = host_mapping + break + + if not mapping: + raise ValueError("Host was not found: " + target_host_name) + + if flags["border_router"]: + gateway = _find_router_in_network(mapping["network"], input_definitions) + if_netmask = _find_netmask(mapping["ip"], input_definitions) + _add_default_route(gateway, mapping["ip"], if_netmask, routes) + else: + for network in input_definitions["networks"]: + if network["name"] == mapping["network"]: + continue + routing_to_other_hosts = dict() + gateway = _find_router_in_network(mapping["network"], + input_definitions) + routing_to_other_hosts["gateway"] = gateway + net_ip, mask = network["cidr"].split('/') + routing_to_other_hosts["network"] = net_ip + routing_to_other_hosts["mask"] = mask + if_mask = _find_netmask(mapping["ip"], input_definitions) + _add_interface_route(routing_to_other_hosts, mapping["ip"], if_mask, + routes) + + return routes + + +def _create_router_routing(router_name, input_definitions, flags): + """Generate list of routes for the given router.""" + routes = [] + if "border_router" in flags and flags["border_router"]: + _configure_auto_on_ansible_interface(routes) + + if flags["border_router"]: + interface_ip = _find_iface_ip_in_network(router_name, + BORDER_ROUTER_NETWORK_NAME, + input_definitions) + net_ip, mask = BORDER_ROUTER_NETWORK_IP.split("/") + _add_default_route(BORDER_ROUTER_IP, interface_ip, mask, routes) + + return routes + + +def _create_border_router_routing(input_definitions): + """Generate routes for the border router.""" + routes = [] + _configure_auto_on_ansible_interface(routes, True) + + for network in input_definitions["networks"]: + if network["name"] == BORDER_ROUTER_NETWORK_NAME: + continue + routing_to_hosts = dict() + net_ip, mask = network["cidr"].split('/') + routing_to_hosts["network"] = net_ip + routing_to_hosts["mask"] = mask + gateway = _find_router_ip_in_br_network(network["name"], + input_definitions) + routing_to_hosts["gateway"] = gateway + net_ip, if_mask = BORDER_ROUTER_NETWORK_IP.split("/") + _add_interface_route(routing_to_hosts, BORDER_ROUTER_IP, if_mask, + routes) + + return routes + + +def add_routes(device_name, device_type, input_definitions, flags): + """Generate simple and default routes for the given device. + + Returns a list of dicts with the syntax: + - gateway: ip of the target device + interface_ip: ip of the device on the given interface + network: ip of the network (if default is False) + mask: mask of the network (if default is False) + """ + if not input_definitions["routers"]: + return [] + + if device_type == "host": + return _create_host_routing(device_name, input_definitions, flags) + elif device_type == "router": + if device_name != BORDER_ROUTER_NAME: + return _create_router_routing(device_name, input_definitions, flags) + else: + return _create_border_router_routing(input_definitions) + else: + raise KeyError("Unsupported device type: " + str(device_type)) diff --git a/modules/vagrant_generator.py b/modules/vagrant_generator.py new file mode 100644 index 0000000000000000000000000000000000000000..44dc52d5a2505339826d4599616d9e3a1e82b9d7 --- /dev/null +++ b/modules/vagrant_generator.py @@ -0,0 +1,224 @@ +"""Contains functions for generating a Vagrantfile from input definitions.""" + +from modules.file_manager import generate_file, open_yaml + +VAGRANT_MAPPING = open_yaml("conf/vagrant_mapping.yml") +VIRTUALBOX_MAPPING = open_yaml("conf/virtualbox_mapping.yml") +BASE_PLAYBOOK = "base_provisioning/device_configuration.yml" +USER_PLAYBOOK = "provisioning/playbook.yml" + + +def _create_simple_attribute(key, value, attribute_type): + """Create simple vagrant attributes like string, integer or boolean.""" + attribute = dict() + attribute["type"] = attribute_type + attribute["command"] = key + attribute["value"] = value + + return attribute + + +def _create_complex_attribute(key, value): + """Create complex vagrant attributes that are not string, int or bool.""" + separators = {VAGRANT_MAPPING["other"]["synced_folder"]: ""} + + attribute = dict() + attribute["type"] = "other" + attribute["command"] = key + attribute["separator"] = separators[key] + attribute["value"] = value + + return attribute + + +def _create_commands(device_attributes): + """Create basic vagrant definition commands for a device.""" + commands = [] + vb_commands = [] + + for attribute, value in device_attributes.items(): + if attribute in VAGRANT_MAPPING["string"]: + vagrant_attribute = VAGRANT_MAPPING["string"][attribute] + commands.append(_create_simple_attribute(vagrant_attribute, value, + "string")) + elif attribute in VAGRANT_MAPPING["boolean"]: + vagrant_attribute = VAGRANT_MAPPING["boolean"][attribute] + commands.append(_create_simple_attribute(vagrant_attribute, value, + "boolean")) + elif attribute in VAGRANT_MAPPING["integer"]: + vagrant_attribute = VAGRANT_MAPPING["integer"][attribute] + commands.append(_create_simple_attribute(vagrant_attribute, value, + "integer")) + elif attribute in VAGRANT_MAPPING["other"]: + vagrant_attribute = VAGRANT_MAPPING["other"][attribute] + commands.append(_create_complex_attribute(vagrant_attribute, + value)) + elif attribute in VIRTUALBOX_MAPPING["integer"]: + vagrant_attribute = VIRTUALBOX_MAPPING["integer"][attribute] + vb_commands.append(_create_simple_attribute(vagrant_attribute, + value, "integer")) + + if vb_commands: + virtual_box_command = dict() + virtual_box_command["type"] = "provider" + virtual_box_command["name"] = "virtualbox" + virtual_box_command["commands"] = vb_commands + commands.append(virtual_box_command) + + return commands + + +def _create_ansible_commands(playbook_location, input_definitions, flags): + """Create 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"] = "string" + verbosity["command"] = "verbose" + verbosity["value"] = "vv" + commands.append(verbosity) + + groups = dict() + groups["type"] = "groups" + groups["groups"] = dict() + host_names = [] + for host in input_definitions["hosts"]: + host_names.append(host["name"]) + groups["groups"]["hosts"] = host_names + router_names = [] + for router in input_definitions["routers"]: + router_names.append(router["name"]) + groups["groups"]["routers"] = router_names + commands.append(groups) + + if "ansible_local" in flags and flags["ansible_local"]: + install_mode = dict() + install_mode["type"] = "string" + install_mode["command"] = "install_mode" + install_mode["value"] = "pip" + commands.append(install_mode) + + extravars = dict() + extravars["type"] = "dictionary" + extravars["command"] = "extra_vars" + extravars["dictionary"] = dict() + extravars["dictionary"]["ansible_python_interpreter"] = \ + "\"/usr/bin/python3\"" + commands.append(extravars) + + return commands + + +def _find_netmask(network_name, networks): + """Return 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 + return None + + +def _add_networks_to_device(definition, mappings, input_definitions): + """Add 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): + """Add 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(input_definitions, flags): + """Create 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_PLAYBOOK, + input_definitions, + 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(USER_PLAYBOOK, + input_definitions, + flags) + + provisioner_calls.append(user_playbook) + + return provisioner_calls + + +def _build_vagrant_definitions(input_definitions, flags): + """Create a definition structure for vagrant. + + This structure is more suitable for Vagrantfile generation than input + definitions. + """ + vagrant_definitions = [] + for router in input_definitions["routers"]: + device = dict() + device["type"] = "router" + device["name"] = router["name"] + device["commands"] = _create_commands(router) + vagrant_definitions.append(device) + + for host in input_definitions["hosts"]: + device = dict() + device["type"] = "host" + device["name"] = host["name"] + device["commands"] = _create_commands(host) + vagrant_definitions.append(device) + + _add_all_networks(vagrant_definitions, input_definitions, flags) + + vagrant_definitions.extend(_call_provisioner(input_definitions, flags)) + + return vagrant_definitions + + +def generate_vagrantfile(input_definitions, flags): + """Generate the Vagrantfile. + + :param input_definitions: device definitions from the input file + :param flags: command line flags + """ + vagrant_definitions = _build_vagrant_definitions(input_definitions, flags) + + generate_file("vagrantfile", "Vagrantfile", defs=vagrant_definitions) diff --git a/templates/br b/templates/br index b3ce313ce718fbe9498d7b7a5e1d991af9d70067..c78f42c3b933c89aa7342c5775e8c8b3f45667e5 100644 --- a/templates/br +++ b/templates/br @@ -1,40 +1,27 @@ --- -# Configuration for the border router - -- name: Enable IP forwarding - copy: - dest: "/etc/sysctl.conf" - content: "net.ipv4.ip_forward=1" - -- name: Restarting procps service - command: /etc/init.d/procps restart - -{% for host in hosts %} -- name: Add {{ host.host_name }} alias - lineinfile: - path: /etc/hosts - line: {{ host.host_ip }} {{ host.host_name }} - -{% endfor %} -{% for router in routers %} -- name: Add {{ router.router_name }} alias +- name: Adding aliases + loop: "{{ aliases | dict2items }}" lineinfile: path: /etc/hosts - line: {{ router.router_ip }} {{ router.router_name }} - -{% endfor %} - -{% for target_cidr, router_ip in br_routes.items() %} -- name: Add routing to network {{ target_cidr }} - command: route add -net {{ target_cidr }} gw {{ router_ip }} eth1 -{% endfor %} - -- name: Add postrouting - # ssh connection fails without async after execution of iptables commands - shell: "sleep 2 && sudo iptables -t nat -A POSTROUTING -o eth0 -j SNAT --to-source {{ border_router_public_ip }}" - async: 1 - poll: 0 - -{# name: Save postrouting rule #} -{# command: su -c 'iptables-save > /etc/iptables.rules' #} + line: "{{ item.value }} {{ item.key }}" + +- name: Configuring routes + include_role: + name: interface + vars: + interface_ip: "{{ interface.interface_ip }}" + interface_netmask: "{{ interface.interface_netmask }}" + interface_default_gateway: "{{ interface.interface_default_gateway | default('') }}" + interface_routes: "{{ interface.interface_routes | default([]) }}" + loop: "{{ routes }}" + loop_control: + loop_var: interface + +- name: Set up postrouting + iptables: + table: nat + chain: POSTROUTING + out_interface: "{{ ansible_default_ipv4.interface }}" + jump: SNAT + to_source: "{{ ansible_default_ipv4.address }}" ... diff --git a/templates/common/meta/main.yml b/templates/common/meta/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..edff08cf19051df266730d50704938dbf642e748 --- /dev/null +++ b/templates/common/meta/main.yml @@ -0,0 +1,15 @@ + +galaxy_info: + role_name: common + author: Kamil Andoniadis + description: This role provide common macros, templates or files + licence: MIT + min_ansible_version: 2.3.3 + platforms: + - name: Debian + versions: + - all + - name: Ubuntu + versions: + - all + diff --git a/templates/common/templates/config.j2 b/templates/common/templates/config.j2 new file mode 100644 index 0000000000000000000000000000000000000000..929abb78e17c01dca63d9b607556121a5283fa02 --- /dev/null +++ b/templates/common/templates/config.j2 @@ -0,0 +1,3 @@ +{%- macro yaml_config_key_regexp(key) -%} + ^(#.*)?{{ key }}:.* +{%- endmacro -%} diff --git a/templates/common/templates/network.j2 b/templates/common/templates/network.j2 new file mode 100644 index 0000000000000000000000000000000000000000..f409e1ca0c933d39d1f5db773aa7b5ae52ca68d2 --- /dev/null +++ b/templates/common/templates/network.j2 @@ -0,0 +1,30 @@ +{%- set common_network = namespace( + interfaces=[] +) -%} +{%- for ansible_interface in ansible_interfaces -%} + {%- set common_network.interfaces = common_network.interfaces + [hostvars[inventory_hostname]['ansible_' + ansible_interface]] -%} +{%- endfor -%} + +{%- macro mac_to_interface(mac) -%} + {{ + ( + common_network.interfaces | selectattr('macaddress', 'defined') | + selectattr('macaddress', 'equalto', mac) | map(attribute='device') | list + ) [0] | default('') + }} +{%- endmacro -%} + +{%- macro ip_to_interface(ip) -%} + {{ + ( + common_network.interfaces | selectattr('ipv4', 'defined') | selectattr('ipv4.address', 'defined') | + selectattr('ipv4.address', 'equalto', ip) | map(attribute='device') | list + ) [0] | default('') + }} +{%- endmacro -%} + +{%- macro get_inactive_interfaces() -%} + {{ + common_network.interfaces | selectattr("active", "equalto", False) | list + }} +{%- endmacro -%} diff --git a/templates/config b/templates/config new file mode 100644 index 0000000000000000000000000000000000000000..9c73ad703a3a50a2c1d5fbd1ed2a8c6e1b0f53a5 --- /dev/null +++ b/templates/config @@ -0,0 +1,29 @@ +{% if hosts %} +hosts: + {% for host in hosts %} + - name: {{ host.name }} + {% if "networks" in host %} + - networks: + {% endif %} + {% for network, ip in host.networks.items() %} + {{ network }}: {{ ip }} + {% endfor %} + {% endfor %} +{% else %} +hosts: [] +{% endif %} + +{% if routers %} +routers: + {% for router in routers %} + - name: {{ router.name }} + {% if "networks" in router %} + - networks: + {% endif %} + {% for network, ip in router.networks.items() %} + {{ network }}: {{ ip }} + {% endfor %} + {% endfor %} +{% else %} +routers: [] +{% endif %} diff --git a/templates/device_configuration b/templates/device_configuration index e8ba8322319433a12718201c9e7823f43b14a95a..056e19806fae62a00131a4ae13a3c87ec4a7cae8 100644 --- a/templates/device_configuration +++ b/templates/device_configuration @@ -1,40 +1,35 @@ --- # Basic configuration of all defined devices -- name: Configuring all hosts - hosts: {{ hosts|map(attribute='host_name')|unique|join(',') }} - become: yes - roles: - - hosts +- name: Including variables + hosts: all + tasks: + + - name: Including common variables + include_vars: + file: config.yml + name: config -{% for host in hosts %} -- name: Configuring host {{ host.host_name }} separately - hosts: {{ host.host_name }} +- name: Configuring hosts + hosts: hosts become: yes roles: - - {{ host.host_name }} + - hosts -{% endfor %} -{% for host in hosts %} -- name: Configuring host {{ host.host_name }} - hosts: {{ host.host_name }} +- name: Configuring routers + hosts: routers become: yes tasks: -{% for network_ip in network_ips %} - - name: Add gateway for {{ network_ip }} - command: route add -net {{ network_ip }} gw {{ host.router_ip }} {{ host.interface }} -{% endfor %} + - name: include role + include_role: + name: routers + when: config.routers -{% endfor %} -- name: Configuring all routers - hosts: {{ routers|map(attribute='router_name')|unique|reject('eq', border_router_name)|join(',') }} +- name: Configuring devices separately + hosts: all become: yes - roles: - - routers - -- name: Configuring border router - hosts: {{ border_router_name }} - become: yes - roles: - - br + tasks: + - name: include role + include_role: + name: "{{ inventory_hostname }}" ... diff --git a/templates/hosts b/templates/hosts index e9254f11c31650636be65efaf1f4cdc53326191e..34e8296b1bf6323068655bdd749d110801b52869 100644 --- a/templates/hosts +++ b/templates/hosts @@ -2,23 +2,6 @@ # Basic configuration of all host devices - name: Install net-tools - command: apt install net-tools - -{% for host in hosts %} -- name: Add {{ host.host_name }} alias - lineinfile: - path: /etc/hosts - line: {{ host.host_ip }} {{ host.host_name }} - -{% endfor %} -{% for router in routers %} -- name: Add {{ router.router_name }} alias - lineinfile: - path: /etc/hosts - line: {{ router.router_ip }} {{ router.router_name }} - -{% endfor %} - -- name: Delete default gateway - command: route del default + apt: + name: "net-tools" ... diff --git a/templates/interface/defaults/main.yml b/templates/interface/defaults/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..0910da5b26381303fbc25deb652f190995339756 --- /dev/null +++ b/templates/interface/defaults/main.yml @@ -0,0 +1,40 @@ + +interface_default_file: /etc/network/interfaces +interface_directory: '{{ interface_default_file }}.d' +interface_file_name: +interface_file: ' + {%- if interface_file_name is defined and interface_file_name -%} + {{ interface_directory }}/{{ interface_file_name }} + {%- else -%} + {{ interface_default_file }} + {%- endif %}' + +interface_clean: True +interface_mtu: 1442 + +interface_ip: +interface_mac: +interface_name: +interface_default_gateway: +interface_routes: [] +# - gateway: +# network: +# mask: + +interface_device: ' + {%- import "roles/common/templates/network.j2" as network with context -%} + {%- if interface_ip is defined and interface_ip -%} + {{ network.ip_to_interface(interface_ip) | default("") }} + {%- endif -%} + {%- if interface_mac is defined and interface_mac -%} + {{ network.mac_to_interface(interface_mac) | default("") }} + {%- endif -%} + {%- if interface_name is defined and interface_name -%} + {{ interface_name }} + {%- endif -%}' +interface_identifiers: + interface_ip: '{{ interface_ip }}' + interface_mac: '{{ interface_mac }}' + interface_name: '{{ interface_name }}' +interface_condition_single_interface_identifier: '{{ interface_identifiers | dict2items | map(attribute="value") | select("string") | select("ne", "") | list | length != 1 }}' + diff --git a/templates/interface/handlers/main.yml b/templates/interface/handlers/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..34553f66237ce5bfa2b75cff663b452a2402eae4 --- /dev/null +++ b/templates/interface/handlers/main.yml @@ -0,0 +1,6 @@ + +- name: interface_networking_restart + service: + name: networking + state: restarted + diff --git a/templates/interface/meta/main.yml b/templates/interface/meta/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..58ecda581c150aac8a05d5d99e63955a1cae7073 --- /dev/null +++ b/templates/interface/meta/main.yml @@ -0,0 +1,19 @@ + +dependencies: + - src: git@gitlab.ics.muni.cz:CSIRT-MU-public/ansible-roles/common.git + scm: git + +galaxy_info: + role_name: interface + author: Kamil Andoniadis + description: Basic network interface configuration + licence: MIT + min_ansible_version: 2.3.3 + platforms: + - name: Debian + versions: + - all + - name: Ubuntu + versions: + - all + diff --git a/templates/interface/tasks/clean.yml b/templates/interface/tasks/clean.yml new file mode 100644 index 0000000000000000000000000000000000000000..36a651b9e5b6bff74029196dc471219b519fcc64 --- /dev/null +++ b/templates/interface/tasks/clean.yml @@ -0,0 +1,31 @@ + +- name: find all interfaces configuration files + find: + paths: + - '{{ interface_directory }}' + register: interface_extra_files + +- set_fact: + interface_config_files: '{{ (interface_extra_files.files | map(attribute="path") | list) + [interface_default_file] }}' + +- name: remove old iface settings for retrieved interface name + replace: + path: '{{ item }}' + regexp: ^iface[ \t]{{ interface_device }}[ \t].*(\n[ \t]+.*)* + notify: interface_networking_restart + with_items: '{{ interface_config_files }}' + +- name: remove the rest of old settings for retrieved interface name + replace: + path: '{{ item }}' + regexp: '^.*(?<=\s){{ interface_device }}(?=\s).*$' + notify: interface_networking_restart + with_items: '{{ interface_config_files }}' + +- name: remove multiple consecutive new line characters + replace: + path: '{{ item }}' + regexp: '(\n)+' + replace: '\n' + with_items: '{{ interface_config_files }}' + diff --git a/templates/interface/tasks/interface.yml b/templates/interface/tasks/interface.yml new file mode 100644 index 0000000000000000000000000000000000000000..5f78cca50fef9c49df709de26ec1b26379235549 --- /dev/null +++ b/templates/interface/tasks/interface.yml @@ -0,0 +1,25 @@ + +- name: configure interface + blockinfile: + path: '{{ interface_file }}' + create: yes + marker: '# {mark} {{ interface_device }}' + block: | + allow-hotplug {{ interface_device }} + auto {{ interface_device }} + iface {{ interface_device }} inet static + address {{ interface_ip }} + netmask {{ interface_netmask }} + mtu {{ interface_mtu }} + {% if interface_default_gateway -%} + gateway {{ interface_default_gateway }} + up route add default gw {{ interface_default_gateway }} + {% endif -%} + {% if interface_routes -%} + {% for route in interface_routes -%} + post-up ip route add {{ route['network'] }}/{{ route['mask'] }} via {{ route['gateway'] }} + pre-down ip route del {{ route['network'] }}/{{ route['mask'] }} via {{ route['gateway'] }} + {% endfor %} + {% endif %} + notify: interface_networking_restart + diff --git a/templates/interface/tasks/main.yml b/templates/interface/tasks/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..7e69f74724ae2ed1bf954c99f929b0a53dceccf5 --- /dev/null +++ b/templates/interface/tasks/main.yml @@ -0,0 +1,11 @@ + +- name: check existence of single interface identifier + fail: + msg: there must be set exactly one of [interface_ip|interface_mac|interface_name] variables, got {{ interface_identifiers }} + when: interface_condition_single_interface_identifier + +- include: clean.yml + when: interface_clean is defined and interface_clean + +- include: interface.yml + diff --git a/templates/playbook b/templates/playbook index 7f3e1c7089a960a7da5b952b974f818a23081acf..2fe3cd9f7613d46390332339c044d12c74b3b36a 100644 --- a/templates/playbook +++ b/templates/playbook @@ -1,5 +1,5 @@ --- -# Main user ansible playbook +# Main user ansible playbook. # Write your custom configuration here: - name: Hello world diff --git a/templates/routers b/templates/routers index 96d9d5926138897fa1e51f290842f753bd671b60..2e1724cbf267a7eb1af1ecacd07ac08d3087782b 100644 --- a/templates/routers +++ b/templates/routers @@ -2,30 +2,9 @@ # Configuration of all router devices - name: Enable IP forwarding - copy: - dest: "/etc/sysctl.conf" - content: "net.ipv4.ip_forward=1" - -- name: Restarting procps service - command: /etc/init.d/procps restart - -{% for host in hosts %} -- name: Add {{ host.host_name }} alias - lineinfile: - path: /etc/hosts - line: {{ host.host_ip }} {{ host.host_name }} - -{% endfor %} -{% for router in routers %} -- name: Add {{ router.router_name }} alias - lineinfile: - path: /etc/hosts - line: {{ router.router_ip }} {{ router.router_name }} - -{% endfor %} -- name: Delete default gateway - command: route del default - -- name: Add default path to border router - command: route add default gw {{ border_router_ip }} eth3 + sysctl: + name: net.ipv4.ip_forward + value: '1' + sysctl_set: yes + reload: yes ... diff --git a/templates/separate_devices b/templates/separate_devices new file mode 100644 index 0000000000000000000000000000000000000000..5e398eb9f746d2f24c588bf6848e5d9a2a73aad1 --- /dev/null +++ b/templates/separate_devices @@ -0,0 +1,19 @@ +--- +- name: Adding aliases + loop: "{{ aliases | dict2items }}" + lineinfile: + path: /etc/hosts + line: "{{ item.value }} {{ item.key }}" + +- name: Configuring routes + include_role: + name: interface + vars: + interface_ip: "{{ interface.interface_ip }}" + interface_netmask: "{{ interface.interface_netmask }}" + interface_default_gateway: "{{ interface.interface_default_gateway | default('') }}" + interface_routes: "{{ interface.interface_routes | default([])}}" + loop: "{{ routes }}" + loop_control: + loop_var: interface +... \ No newline at end of file diff --git a/templates/user_hosts b/templates/user_hosts index 0dccf70c06e2ec8580828c2c8c6693e870030b59..c79ff48449939fcf1fe5406771c6d21cbbc846f1 100644 --- a/templates/user_hosts +++ b/templates/user_hosts @@ -1,7 +1,6 @@ --- # This is a role for all hosts. # You can write your tasks here. -# These changes will affect all hosts. diff --git a/templates/user_routers b/templates/user_routers new file mode 100644 index 0000000000000000000000000000000000000000..00eb86eec6c3124109e067240692ae0085cf78cb --- /dev/null +++ b/templates/user_routers @@ -0,0 +1,7 @@ +--- +# This is a role for all routers. +# You can write your tasks here. + + + +... diff --git a/templates/user_separate_hosts b/templates/user_separate_hosts index f49a8bafc9297f21682d4a170677d597336f6544..25921c0776e23af7285a5f5fb20574ecc9fb4f37 100644 --- a/templates/user_separate_hosts +++ b/templates/user_separate_hosts @@ -1,7 +1,6 @@ --- # This is a role for the host {{ host_name }}. # You can write your tasks here. -# These changes will affect only the host {{ host_name }}. diff --git a/templates/user_separate_routers b/templates/user_separate_routers new file mode 100644 index 0000000000000000000000000000000000000000..c92c2bd41008a10d5710bb1127eb092c1111054b --- /dev/null +++ b/templates/user_separate_routers @@ -0,0 +1,7 @@ +--- +# This is a role for the router {{ router_name }}. +# You can write your tasks here. + + + +... diff --git a/templates/vagrantfile b/templates/vagrantfile index 946446c71fc08127a3026740579c095125186d20..664f569a5b63a022d605442c9e515a138fd51485 100644 --- a/templates/vagrantfile +++ b/templates/vagrantfile @@ -1,51 +1,152 @@ -# Generated vagrant file +# Vagrantfile generated by Sandbox Creator. # # -*- mode: ruby -*- # vi: set ft=ruby : -{# Macro that prints out attributes of a device #} -{% macro printAttributes(device_name) %} -{% for command in devices[device_name] %} - {{ command }} -{% endfor %} -{% endmacro %} +{# Macro for router items #} +{% macro router(item, namespace) %} + # device (router): {{ item.name }} + {{ namespace }}.vm.define "{{ item.name }}" do |device| +{{ layer2(item.commands, "device") }} +{% endmacro -%} -{# Device definitions #} -Vagrant.configure("2") do |config| -{% for name, attributes in devices.items() %} +{# Macro for host items #} +{% macro host(item, namespace) %} + # device (host): {{ item.name }} + {{ namespace }}.vm.define "{{ item.name }}" do |device| +{{ layer2(item.commands, "device") }} +{% endmacro -%} - # device: {{ name }} - config.vm.define "{{ name }}" do |device| -{{ printAttributes(name) }} end -{% endfor %} +{# Macro for provider items #} +{% macro provider(item, namespace) %} + {{ namespace }}.vm.provider "{{ item.name }}" do |provider| +{{ layer3(item.commands, "provider") }} +{% endmacro -%} - # basic ansible configuration of devices and networks - config.vm.provision :ansible{% if ansible_local %}_local{% endif %} do |ansible| - ansible.playbook = "base_provisioning/device_configuration.yml" - ansible.verbose = true - ansible.extra_vars = { - ansible_python_interpreter: "/usr/bin/python3", - } - end +{# 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 -%} - # user configuration of devices with ansible - config.vm.provision :ansible{% if ansible_local %}_local{% endif %} do |ansible| - ansible.playbook = "provisioning/playbook.yml" - ansible.verbose = true - ansible.extra_vars = { - ansible_python_interpreter: "/usr/bin/python3", - } - end +{# 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 general items (not str int or bool) #} +{% macro other(item, namespace) %} +{% if item.separator %} + {{ namespace }}.{{ item.command }} {{ item.separator }} {{ item.value }} +{% else %} + {{ namespace }}.{{ item.command }} {{ item.value }} +{% endif %} +{% endmacro -%} - {% for name in user_files %} - config.vm.provision :ansible{% if ansible_local %}_local{% endif %} do |ansible| - ansible.playbook = "provisioning/{{ name }}.yml" - ansible.verbose = true - ansible.extra_vars = { - ansible_python_interpreter: "/usr/bin/python3", +{# Macro for dictionaries #} +{% macro dictionary(item, namespace) %} + {{ namespace }}.{{ item.command }} = { + {% for key, value in item.dictionary.items() %} + {% if loop.last %} + {{ key }}: {{ value }} + {% else %} + {{ key }}: {{ value }}, + {% endif %} + {% endfor %} } - end +{% endmacro -%} + +{# Macro for dictionaries #} +{% macro groups(item, namespace) %} + {{ namespace }}.groups = { + {% for key, value in item.groups.items() %} + {% if loop.last %} + "{{ key }}" => {{ value }} + {% else %} + "{{ key }}" => {{ value }}, + {% endif %} {% endfor %} + } +{% 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 == "other" %} + {{ other(item, namespace) -}} +{% elif item.type == "provider" %} + {{ provider(item, namespace) -}} +{% elif item.type == "network" %} + {{ network(item, namespace) -}} +{% elif item.type == "dictionary" %} + {{ dictionary(item, namespace) -}} +{% elif item.type == "groups" %} + {{ groups(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") -}} diff --git a/topologies/0-routers-1-network-1-host.yml b/topologies/0-routers-1-network-1-host.yml new file mode 100644 index 0000000000000000000000000000000000000000..7d2afb3a99fd3e4d5248674ebf8c134138bb016e --- /dev/null +++ b/topologies/0-routers-1-network-1-host.yml @@ -0,0 +1,16 @@ +# A simple topology with 1 debian 10 host in a network +name: 0-routers-1-networks-1-hosts +hosts: + - name: debian10 + base_box: generic/debian10 + flavor: csirtmu.tiny1x4 + cpus: 2 + +networks: + - name: network + cidr: 10.10.30.0/24 + +net_mappings: + - host: debian10 + network: network + ip: 10.10.30.5 diff --git a/topologies/0-routers-1-network-5-hosts.yml b/topologies/0-routers-1-network-5-hosts.yml new file mode 100644 index 0000000000000000000000000000000000000000..b389367ad52edb8da39be84efe86a96c04f7e71d --- /dev/null +++ b/topologies/0-routers-1-network-5-hosts.yml @@ -0,0 +1,43 @@ +# A simple topology with various boxes in 1 network +name: 1-network-various-boxes +hosts: + - name: debian10 + base_box: generic/debian10 + memory: 512 + + - name: debian9 + base_box: generic/debian9 + memory: 512 + + - name: debian-stretch + base_box: debian/contrib-stretch64 + memory: 512 + + - name: ubuntu-xenial + base_box: ubuntu/xenial64 + memory: 1024 + + - name: mu-kali + base_box: mu/kali-2019.4 + memory: 2048 + +networks: + - name: network + cidr: 10.10.30.0/24 + +net_mappings: + - host: debian10 + network: network + ip: 10.10.30.5 + - host: debian9 + network: network + ip: 10.10.30.6 + - host: debian-stretch + network: network + ip: 10.10.30.7 + - host: ubuntu-xenial + network: network + ip: 10.10.30.8 + - host: mu-kali + network: network + ip: 10.10.30.9 diff --git a/topologies/1-router-1-network-1-host.yml b/topologies/1-router-1-network-1-host.yml new file mode 100644 index 0000000000000000000000000000000000000000..100178292eab6f42af221f3520f415fa4a78d1d2 --- /dev/null +++ b/topologies/1-router-1-network-1-host.yml @@ -0,0 +1,23 @@ + +# 1 network with a host and a router +name: 1-router-1-host +hosts: + - name: debian10 + base_box: generic/debian10 + memory: 512 + +routers: + - name: router + +networks: + - name: network + +net_mappings: + - host: debian10 + network: network + ip: 10.10.30.5 + +router_mappings: + - router: router + network: network + ip: 10.10.30.1 diff --git a/topologies/1-router-2-networks-2-hosts.yml b/topologies/1-router-2-networks-2-hosts.yml new file mode 100644 index 0000000000000000000000000000000000000000..4fba3a2015b38cc806e5786e215c5d7c6f10a803 --- /dev/null +++ b/topologies/1-router-2-networks-2-hosts.yml @@ -0,0 +1,37 @@ +# Topology with one router connecting 2 separate networks, each with 1 host +name: 2-networks +hosts: + - name: server + base_box: generic/debian10 + memory: 512 + + - name: home + base_box: generic/debian10 + memory: 512 + +routers: + - name: router + +networks: + - name: server-switch + cidr: 10.10.20.0/24 + + - name: home-switch + cidr: 10.10.30.0/24 + +net_mappings: + - host: server + network: server-switch + ip: 10.10.20.5 + - host: home + network: home-switch + ip: 10.10.30.5 + +router_mappings: + - router: router + network: server-switch + ip: 10.10.20.1 + - router: router + network: home-switch + ip: 10.10.30.1 + diff --git a/sandbox.yml b/topology.yml similarity index 79% rename from sandbox.yml rename to topology.yml index c53b54d0e2ac286fe728197f90e4f7f631fb0322..9a686249ec1a37139ff783e9c770d39836233e66 100644 --- a/sandbox.yml +++ b/topology.yml @@ -16,24 +16,24 @@ routers: networks: - name: server-switch - cidr: 10.10.20.0/24 + cidr: 192.168.20.0/24 - name: home-switch - cidr: 10.10.30.0/24 + cidr: 192.168.30.0/24 net_mappings: - host: server network: server-switch - ip: 10.10.20.5 + ip: 192.168.20.5 - host: home network: home-switch - ip: 10.10.30.5 + ip: 192.168.30.5 router_mappings: - router: router network: server-switch - ip: 10.10.20.1 + ip: 192.168.20.1 - router: router network: home-switch - ip: 10.10.30.1 + ip: 192.168.30.1