summaryrefslogtreecommitdiff
path: root/roles/vm/guest
diff options
context:
space:
mode:
authorChristian Pointner <equinox@spreadspace.org>2020-07-07 22:09:19 +0200
committerChristian Pointner <equinox@spreadspace.org>2020-07-11 02:29:02 +0200
commit6eacc2ad5539abf37dc90cd378b44320f7758869 (patch)
treeeacbf207b610a8ee93f830b381d91491671f6ae5 /roles/vm/guest
parentch-oulu: interface config (diff)
refactor vm role names
Diffstat (limited to 'roles/vm/guest')
-rw-r--r--roles/vm/guest/base/defaults/main.yml (renamed from roles/vm/guest/defaults/main.yml)0
-rw-r--r--roles/vm/guest/base/handlers/main.yml (renamed from roles/vm/guest/handlers/main.yml)4
-rw-r--r--roles/vm/guest/base/tasks/main.yml (renamed from roles/vm/guest/tasks/main.yml)19
-rw-r--r--roles/vm/guest/define/defaults/main.yml5
-rw-r--r--roles/vm/guest/define/tasks/main.yml50
-rw-r--r--roles/vm/guest/define/templates/libvirt-domain.xml.j2102
-rw-r--r--roles/vm/guest/install/library/wait_for_virt.py179
-rw-r--r--roles/vm/guest/install/tasks/installer-debian.yml21
-rw-r--r--roles/vm/guest/install/tasks/installer-openbsd.yml19
-rw-r--r--roles/vm/guest/install/tasks/main.yml90
-rw-r--r--roles/vm/guest/network/handlers/main.yml3
-rw-r--r--roles/vm/guest/network/tasks/main.yml40
-rw-r--r--roles/vm/guest/network/templates/interfaces.j256
-rw-r--r--roles/vm/guest/network/templates/resolv.conf.j24
-rw-r--r--roles/vm/guest/network/templates/systemd.link.j25
15 files changed, 597 insertions, 0 deletions
diff --git a/roles/vm/guest/defaults/main.yml b/roles/vm/guest/base/defaults/main.yml
index ce072e95..ce072e95 100644
--- a/roles/vm/guest/defaults/main.yml
+++ b/roles/vm/guest/base/defaults/main.yml
diff --git a/roles/vm/guest/handlers/main.yml b/roles/vm/guest/base/handlers/main.yml
index 5b57f3bc..2dfdddcb 100644
--- a/roles/vm/guest/handlers/main.yml
+++ b/roles/vm/guest/base/handlers/main.yml
@@ -1,3 +1,7 @@
+---
+- name: update grub
+ command: /usr/sbin/update-grub
+
- name: restart rngd
service:
name: rng-tools
diff --git a/roles/vm/guest/tasks/main.yml b/roles/vm/guest/base/tasks/main.yml
index e68f04df..b76ee762 100644
--- a/roles/vm/guest/tasks/main.yml
+++ b/roles/vm/guest/base/tasks/main.yml
@@ -1,3 +1,4 @@
+---
- name: install rngd
apt:
name: rng-tools
@@ -40,3 +41,21 @@
[Service]
ExecStart=
ExecStart=-/sbin/agetty --keep-baud 115200,38400,9600 --noclear --autologin root --login-pause --host {{ vm_host_cooked.name }} %I $TERM
+
+
+- name: enable serial console in grub and for kernel
+ vars:
+ grub_options:
+ GRUB_TIMEOUT: 2
+ GRUB_CMDLINE_LINUX: '"console=ttyS0,115200n8"'
+ GRUB_TERMINAL: serial
+ GRUB_SERIAL_COMMAND: >-
+ "serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1"
+ loop: "{{ grub_options | dict2items }}"
+ loop_control:
+ label: "{{ item.key }}"
+ lineinfile:
+ dest: /etc/default/grub
+ regexp: "^{{ item.key }}="
+ line: "{{ item.key }}={{ item.value }}"
+ notify: update grub
diff --git a/roles/vm/guest/define/defaults/main.yml b/roles/vm/guest/define/defaults/main.yml
new file mode 100644
index 00000000..f0bcc4fd
--- /dev/null
+++ b/roles/vm/guest/define/defaults/main.yml
@@ -0,0 +1,5 @@
+---
+vm_define_autostart: "{{ not vm_define_installer and hostvars[install_hostname].install_cooked.vm.autostart | default(False) }}"
+vm_define_start: yes
+
+vm_define_installer: no
diff --git a/roles/vm/guest/define/tasks/main.yml b/roles/vm/guest/define/tasks/main.yml
new file mode 100644
index 00000000..d0790628
--- /dev/null
+++ b/roles/vm/guest/define/tasks/main.yml
@@ -0,0 +1,50 @@
+---
+- name: check if vm already exists
+ virt:
+ name: "{{ install_hostname }}"
+ command: info
+ register: vmhost_info
+
+- name: remove old vm
+ when: install_hostname in vmhost_info
+ block:
+ - name: destroy exisiting vm
+ virt:
+ name: "{{ install_hostname }}"
+ state: destroyed
+
+ - name: wait for vm to be destroyed
+ wait_for_virt:
+ name: "{{ install_hostname }}"
+ states: shutdown,crashed
+ timeout: 5
+
+ - name: undefining exisiting vm
+ virt:
+ name: "{{ install_hostname }}"
+ command: undefine
+
+- name: define vm
+ virt:
+ command: define
+ xml: "{{ lookup('template', 'libvirt-domain.xml.j2') }}"
+
+- name: start new vm
+ when: vm_define_start | bool
+ block:
+ - name: start vm
+ virt:
+ name: "{{ install_hostname }}"
+ state: running
+
+ - name: wait for VM to start
+ wait_for_virt:
+ name: "{{ install_hostname }}"
+ states: running
+ timeout: 10
+
+- name: mark vm as autostarted
+ virt:
+ name: "{{ install_hostname }}"
+ autostart: "{{ vm_define_autostart }}"
+ command: info ## virt module needs either command or state
diff --git a/roles/vm/guest/define/templates/libvirt-domain.xml.j2 b/roles/vm/guest/define/templates/libvirt-domain.xml.j2
new file mode 100644
index 00000000..ba0dcd5a
--- /dev/null
+++ b/roles/vm/guest/define/templates/libvirt-domain.xml.j2
@@ -0,0 +1,102 @@
+<domain type='kvm'>
+ <name>{{ install_hostname }}</name>
+ <memory>{{ ((hostvars[install_hostname].install_cooked.vm.memory | human_to_bytes) / 1024) | int }}</memory>
+ <currentMemory>{{ ((hostvars[install_hostname].install_cooked.vm.memory | human_to_bytes) / 1024) | int }}</currentMemory>
+ <vcpu>{{ hostvars[install_hostname].install_cooked.vm.numcpus }}</vcpu>
+ <os>
+ <type arch='x86_64' machine='pc-0.12'>hvm</type>
+{% if vm_define_installer %}
+{% if install_distro == 'debian' or install_distro == 'ubuntu' %}
+ <kernel>{{ installer_base_path }}/{{ install_distro }}-{{ install_codename }}/{{ hostvars[install_hostname].install_cooked.arch | default('amd64') }}-netboot/linux</kernel>
+ <initrd>{{ installer_tmpdir }}/initrd.{{ install_hostname }}.gz</initrd>
+ <cmdline>console=ttyS0,115200n8 DEBCONF_DEBUG=5</cmdline>
+ <boot dev='hd'/>
+{% elif install_distro == 'openbsd' %}
+ <boot dev='cdrom'/>
+{% endif %}
+{% else %}
+ <boot dev='hd'/>
+{% endif %}
+ </os>
+ <features>
+ <acpi/>
+ <apic/>
+ <pae/>
+ </features>
+ <clock offset='utc'/>
+ <on_poweroff>destroy</on_poweroff>
+{% if vm_define_installer %}
+ <on_reboot>destroy</on_reboot>
+ <on_crash>destroy</on_crash>
+{% else %}
+ <on_reboot>restart</on_reboot>
+ <on_crash>restart</on_crash>
+{% endif %}
+ <devices>
+ <emulator>/usr/bin/kvm</emulator>
+ <!-- Provide a virtualized RNG to the guest -->
+ <rng model='virtio'>
+ <!-- Allow consuming up to 10kb/s, measured over 2s -->
+ <rate period="2000" bytes="20480"/>
+ <backend model='random'>/dev/random</backend>
+ </rng>
+
+{% if vm_define_installer and install_distro == 'openbsd' %}
+ <disk type='file' device='cdrom'>
+ <driver name='qemu'/>
+ <source file='{{ installer_tmpdir }}/{{ install_hostname }}.iso'/>
+ <target dev='hdc' bus='ide'/>
+ <readonly/>
+ </disk>
+
+{% endif %}
+{% if 'virtio' in hostvars[install_hostname].install_cooked.disks %}
+{% for device, src in hostvars[install_hostname].install_cooked.disks.virtio.items() %}
+ <disk type='block' device='disk'>
+ <driver name='qemu' type='raw' cache='none' discard='unmap'/>
+{% if src.type == 'lvm' %}
+ <source dev='/dev/mapper/{{ src.vg | replace('-', '--') }}-{{ src.lv | replace('-', '--') }}'/>
+{% elif src.type == 'zfs' %}
+ <source dev='/dev/zvol/{{ vm_host_cooked.zfs[src.backend | default('default')].pool }}/{{ vm_host_cooked.zfs[src.backend | default('default')].name }}/{{ install_hostname }}/{{ src.name }}'/>
+{% endif %}
+ <target dev='{{ device }}' bus='virtio'/>
+ </disk>
+{% endfor %}
+{% endif %}
+
+{% if 'scsi' in hostvars[install_hostname].install_cooked.disks %}
+ <controller type='scsi' index='0' model='virtio-scsi'/>
+{% for device, src in hostvars[install_hostname].install_cooked.disks.scsi.items() %}
+ <disk type='block' device='disk'>
+ <driver name='qemu' type='raw' cache='none' discard='unmap'/>
+{% if src.type == 'lvm' %}
+ <source dev='/dev/mapper/{{ src.vg | replace('-', '--') }}-{{ src.lv | replace('-', '--') }}'/>
+{% elif src.type == 'zfs' %}
+ <source dev='/dev/zvol/{{ vm_host_cooked.zfs[src.backend | default('default')].pool }}/{{ vm_host_cooked.zfs[src.backend | default('default')].name }}/{{ install_hostname }}/{{ src.name }}'/>
+{% endif %}
+ <target dev='{{ device }}' bus='scsi'/>
+ </disk>
+{% endfor %}
+{% endif %}
+
+{% if hostvars[install_hostname].install_cooked.interfaces %}
+{% for if in hostvars[install_hostname].install_cooked.interfaces %}
+ <interface type='bridge'>
+{% if 'mac' in if %}
+ <mac address='{{ if.mac }}'/>
+{% endif %}
+ <source bridge='{{ if.bridge }}'/>
+ <model type='virtio'/>
+ <address type='pci' domain='0x0000' bus='0x01' slot='0x0{{ loop.index }}' function='0x0'/>
+ </interface>
+{% endfor %}
+{% endif %}
+
+ <serial type='pty'>
+ <target port='0'/>
+ </serial>
+ <console type='pty'>
+ <target type='serial' port='0'/>
+ </console>
+ </devices>
+</domain>
diff --git a/roles/vm/guest/install/library/wait_for_virt.py b/roles/vm/guest/install/library/wait_for_virt.py
new file mode 100644
index 00000000..6c49fae1
--- /dev/null
+++ b/roles/vm/guest/install/library/wait_for_virt.py
@@ -0,0 +1,179 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import traceback
+import time
+
+try:
+ import libvirt
+except ImportError:
+ HAS_VIRT = False
+else:
+ HAS_VIRT = True
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_native
+
+
+VIRT_FAILED = 1
+VIRT_SUCCESS = 0
+VIRT_UNAVAILABLE = 2
+
+VIRT_STATE_NAME_MAP = {
+ 0: "running",
+ 1: "running",
+ 2: "running",
+ 3: "paused",
+ 4: "shutdown",
+ 5: "shutdown",
+ 6: "crashed"
+}
+
+
+class VMNotFound(Exception):
+ pass
+
+
+class LibvirtConnection(object):
+
+ def __init__(self, uri, module):
+
+ self.module = module
+
+ cmd = "uname -r"
+ rc, stdout, stderr = self.module.run_command(cmd)
+
+ if "xen" in stdout:
+ conn = libvirt.open(None)
+ elif "esx" in uri:
+ auth = [[libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_NOECHOPROMPT], [], None]
+ conn = libvirt.openAuth(uri, auth)
+ else:
+ conn = libvirt.open(uri)
+
+ if not conn:
+ raise Exception("hypervisor connection failure")
+
+ self.conn = conn
+
+ def find_vm(self, vmid):
+ """
+ Extra bonus feature: vmid = -1 returns a list of everything
+ """
+ conn = self.conn
+
+ vms = []
+
+ # this block of code borrowed from virt-manager:
+ # get working domain's name
+ ids = conn.listDomainsID()
+ for id in ids:
+ vm = conn.lookupByID(id)
+ vms.append(vm)
+ # get defined domain
+ names = conn.listDefinedDomains()
+ for name in names:
+ vm = conn.lookupByName(name)
+ vms.append(vm)
+
+ if vmid == -1:
+ return vms
+
+ for vm in vms:
+ if vm.name() == vmid:
+ return vm
+
+ raise VMNotFound("virtual machine %s not found" % vmid)
+
+ def get_status(self, vmid):
+ state = self.find_vm(vmid).info()[0]
+ return VIRT_STATE_NAME_MAP.get(state, "unknown")
+
+
+class Virt(object):
+
+ def __init__(self, uri, module):
+ self.module = module
+ self.uri = uri
+
+ def __get_conn(self):
+ self.conn = LibvirtConnection(self.uri, self.module)
+ return self.conn
+
+ def status(self, vmid):
+ """
+ Return a state suitable for server consumption. Aka, codes.py values, not XM output.
+ """
+ self.__get_conn()
+ return self.conn.get_status(vmid)
+
+
+def core(module):
+
+ states = module.params.get('states', None)
+ guest = module.params.get('name', None)
+ uri = module.params.get('uri', None)
+ delay = module.params.get('delay', None)
+ sleep = module.params.get('sleep', None)
+ timeout = module.params.get('timeout', None)
+
+ v = Virt(uri, module)
+ res = {'changed': False, 'failed': True}
+
+ if delay > 0:
+ time.sleep(delay)
+
+ for _ in range(0, timeout, sleep):
+ state = v.status(guest)
+ if state in states:
+ res['state'] = state
+ res['failed'] = False
+ res['msg'] = "guest '%s' has reached state: %s" % (guest, state)
+ return VIRT_SUCCESS, res
+
+ time.sleep(sleep)
+
+ res['msg'] = "timeout waiting for guest '%s' to reach one of states: %s" % (guest, ', '.join(states))
+ return VIRT_FAILED, res
+
+
+def main():
+
+ module = AnsibleModule(argument_spec=dict(
+ name=dict(aliases=['guest'], required=True),
+ states=dict(type='list', required=True),
+ uri=dict(default='qemu:///system'),
+ delay=dict(type='int', default=0),
+ sleep=dict(type='int', default=1),
+ timeout=dict(type='int', default=300),
+ ))
+
+ if not HAS_VIRT:
+ module.fail_json(
+ msg='The `libvirt` module is not importable. Check the requirements.'
+ )
+
+ for state in module.params.get('states', None):
+ if state not in set(VIRT_STATE_NAME_MAP.values()):
+ module.fail_json(
+ msg="states contains invalid state '%s', must be one of %s" % (state, ', '.join(set(VIRT_STATE_NAME_MAP.values())))
+ )
+
+ rc = VIRT_SUCCESS
+ try:
+ rc, result = core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=traceback.format_exc())
+
+ if rc != 0: # something went wrong emit the msg
+ module.fail_json(rc=rc, msg=result)
+ else:
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/vm/guest/install/tasks/installer-debian.yml b/roles/vm/guest/install/tasks/installer-debian.yml
new file mode 100644
index 00000000..e0492969
--- /dev/null
+++ b/roles/vm/guest/install/tasks/installer-debian.yml
@@ -0,0 +1,21 @@
+---
+- name: fetch debian installer files
+ vars:
+ debian_installer_distro: "{{ install_distro }}"
+ debian_installer_codename: "{{ install_codename }}"
+ debian_installer_arch: "{{ hostvars[install_hostname].install_cooked.arch | default('amd64') }}"
+ debian_installer_variant: netboot
+ import_role:
+ name: installer/debian/fetch
+
+- name: generate host specific initial ramdisk
+ vars:
+ ssh_keys_root: "{{ hostvars[install_hostname].ssh_keys_root }}"
+ preseed_orig_initrd: "{{ installer_base_path }}/{{ install_distro }}-{{ install_codename }}/{{ hostvars[install_hostname].install_cooked.arch | default('amd64') }}-netboot/initrd.gz"
+ preseed_tmpdir: "{{ tmpdir.path }}"
+ preseed_virtual_machine: yes
+ preseed_force_net_ifnames_policy: path
+ preseed_no_netplan: yes
+ install_interface: enp1s1
+ import_role:
+ name: installer/debian/preseed
diff --git a/roles/vm/guest/install/tasks/installer-openbsd.yml b/roles/vm/guest/install/tasks/installer-openbsd.yml
new file mode 100644
index 00000000..afa17c45
--- /dev/null
+++ b/roles/vm/guest/install/tasks/installer-openbsd.yml
@@ -0,0 +1,19 @@
+---
+- name: fetch openbsd installer files
+ vars:
+ openbsd_installer_version: "{{ install_codename }}"
+ openbsd_installer_arch: "{{ hostvars[install_hostname].install_cooked.arch | default('amd64') }}"
+ import_role:
+ name: installer/openbsd/fetch
+
+- name: generate host specific autoinstall iso
+ vars:
+ ssh_keys_root: "{{ hostvars[install_hostname].ssh_keys_root }}"
+ obsd_autoinstall_orig_iso: "{{ installer_base_path }}/openbsd-{{ install_codename }}/{{ hostvars[install_hostname].install_cooked.arch | default('amd64') }}/install{{ openbsd_installer_version_short }}.iso"
+ obsd_autoinstall_tmpdir: "{{ tmpdir.path }}"
+ obsd_autoinstall_version: "{{ install_codename }}"
+ obsd_autoinstall_arch: "{{ hostvars[install_hostname].install_cooked.arch | default('amd64') }}"
+ obsd_autoinstall_serial_device: com0
+ install_interface: vio0
+ import_role:
+ name: installer/openbsd/autoinstall
diff --git a/roles/vm/guest/install/tasks/main.yml b/roles/vm/guest/install/tasks/main.yml
new file mode 100644
index 00000000..21a13b4d
--- /dev/null
+++ b/roles/vm/guest/install/tasks/main.yml
@@ -0,0 +1,90 @@
+---
+- name: create lvm-based disks for vm
+ loop: "{{ hostvars[install_hostname].install_cooked.disks.virtio | default({}) | combine(hostvars[install_hostname].install_cooked.disks.scsi | default({})) | dict2items | selectattr('value.type', 'eq', 'lvm') | list }}"
+ loop_control:
+ label: "{{ item.value.vg }} / {{ item.value.lv }} ({{ item.value.size }})"
+ lvol:
+ vg: "{{ item.value.vg }}"
+ lv: "{{ item.value.lv }}"
+ size: "{{ item.value.size }}"
+ state: present
+
+- name: create zfs base datasets for vm
+ loop: "{{ hostvars[install_hostname].install_cooked.disks.virtio | default({}) | combine(hostvars[install_hostname].install_cooked.disks.scsi | default({})) | dict2items | selectattr('value.type', 'eq', 'zfs') | map(attribute='value.backend') | map('default', 'default') | unique | list }}"
+ zfs:
+ name: "{{ vm_host_cooked.zfs[item].pool }}/{{ vm_host_cooked.zfs[item].name }}/{{ install_hostname }}"
+ state: present
+ extra_zfs_properties:
+ canmount: no
+ mountpoint: none
+
+- name: create zfs-based disk volumes for vm
+ loop: "{{ hostvars[install_hostname].install_cooked.disks.virtio | default({}) | combine(hostvars[install_hostname].install_cooked.disks.scsi | default({})) | dict2items | selectattr('value.type', 'eq', 'zfs') | list }}"
+ loop_control:
+ label: "{{ item.value.name }} on backend {{ item.value.backend | default('default') }} ({{ item.value.size }})"
+ zfs:
+ name: "{{ vm_host_cooked.zfs[item.value.backend | default('default')].pool }}/{{ vm_host_cooked.zfs[item.value.backend | default('default')].name }}/{{ install_hostname }}/{{ item.value.name }}"
+ state: present
+ extra_zfs_properties: "{{ item.value.properties | default({}) | combine({'volsize': item.value.size}) }}"
+
+
+- block:
+ - name: create a temporary workdir
+ tempfile:
+ path: "{{ installer_base_path }}/"
+ prefix: ".{{ install_hostname }}."
+ state: directory
+ register: tmpdir
+
+ - when: install_distro in ['debian', 'ubuntu']
+ import_tasks: installer-debian.yml
+
+ - when: install_distro in ['openbsd']
+ import_tasks: installer-openbsd.yml
+
+ - name: Make installer workdir readable by qemu
+ acl:
+ path: "{{ tmpdir.path }}"
+ state: present
+ entity: libvirt-qemu
+ etype: user
+ permissions: rx
+
+ - name: define installer vm
+ vars:
+ vm_define_installer: yes
+ installer_tmpdir: "{{ tmpdir.path }}"
+ import_role:
+ name: vm/guest/define
+
+ - debug:
+ msg: "you can check on the status of the installer running this command 'virsh console {{ install_hostname }}' on host {{ inventory_hostname }}."
+
+ - when: installer_manual_steps_msg is defined
+ pause:
+ prompt: |
+ Mind that this installer needs manual steps to be performed:
+
+ {{ installer_manual_steps_msg | indent(2) }}
+
+ When done press enter to continue or Ctrl-C + 'A' to abort.
+
+ - name: wait for installer to finish or crash
+ wait_for_virt:
+ name: "{{ install_hostname }}"
+ states: shutdown,crashed
+ timeout: 1800
+ register: installer_result
+ failed_when: installer_result.failed or installer_result.state == "crashed"
+
+ always:
+ - name: cleanup temporary workdir
+ file:
+ path: "{{ tmpdir.path }}"
+ state: absent
+
+- name: define vm
+ vars:
+ vm_define_installer: no
+ import_role:
+ name: vm/guest/define
diff --git a/roles/vm/guest/network/handlers/main.yml b/roles/vm/guest/network/handlers/main.yml
new file mode 100644
index 00000000..f967fa86
--- /dev/null
+++ b/roles/vm/guest/network/handlers/main.yml
@@ -0,0 +1,3 @@
+---
+- name: rebuild initramfs
+ command: update-initramfs -u
diff --git a/roles/vm/guest/network/tasks/main.yml b/roles/vm/guest/network/tasks/main.yml
new file mode 100644
index 00000000..27a7682a
--- /dev/null
+++ b/roles/vm/guest/network/tasks/main.yml
@@ -0,0 +1,40 @@
+---
+- name: configure systemd link units
+ when: network_cooked.systemd_link is defined
+ block:
+ - name: remove legacy systemd.link units
+ loop:
+ - 50-virtio-kernel-names.link
+ - 99-default.link
+ file:
+ name: "/etc/systemd/network/{{ item }}"
+ state: absent
+
+ - name: install systemd network link units
+ loop: "{{ network_cooked.systemd_link.interfaces }}"
+ loop_control:
+ label: "{{ item.name }}"
+ index_var: interface_index
+ template:
+ src: systemd.link.j2
+ dest: "/etc/systemd/network/{{ '%02d' | format(interface_index + 11) }}-{{ item.name }}.link"
+ notify: rebuild initramfs
+
+
+- name: install basic interface config
+ template:
+ src: interfaces.j2
+ dest: /etc/network/interfaces
+ mode: 0644
+
+- name: remove resolvconf package
+ apt:
+ name: resolvconf
+ state: absent
+ force_apt_get: yes
+ purge: yes
+
+- name: generate resolv.conf
+ template:
+ src: resolv.conf.j2
+ dest: /etc/resolv.conf
diff --git a/roles/vm/guest/network/templates/interfaces.j2 b/roles/vm/guest/network/templates/interfaces.j2
new file mode 100644
index 00000000..8c288669
--- /dev/null
+++ b/roles/vm/guest/network/templates/interfaces.j2
@@ -0,0 +1,56 @@
+# This file describes the network interfaces available on your system
+# and how to activate them. For more information, see interfaces(5).
+
+source /etc/network/interfaces.d/*
+
+# The loopback network interface
+auto lo
+iface lo inet loopback
+{% for interface in network_cooked.interfaces %}
+
+
+auto {{ interface.name }}
+iface {{ interface.name }} inet static
+ pre-up echo 0 > /proc/sys/net/ipv6/conf/$IFACE/accept_ra
+ pre-up echo 0 > /proc/sys/net/ipv6/conf/$IFACE/autoconf
+ address {{ interface.address | ipaddr('address') }}
+ netmask {{ interface.address | ipaddr('netmask') }}
+{% if 'overlay' in interface %}
+ up /bin/ip addr add dev $IFACE {{ interface.overlay }}/32
+{% for route in interface.static_routes | default([]) %}
+ up /bin/ip route add {{ route.destination }} via {{ route.gateway }} src {{ interface.overlay }}
+{% endfor %}
+{% if 'gateway' in interface %}
+ up /bin/ip route add default via {{ interface.gateway }} src {{ interface.overlay }}
+ down /bin/ip route del default via {{ interface.gateway }} src {{ interface.overlay }}
+{% for route in interface.static_routes | default([]) | reverse %}
+ down /bin/ip route del {{ route.destination }} via {{ route.gateway }} src {{ interface.overlay }}
+{% endfor %}
+{% endif %}
+ down /bin/ip addr del dev $IFACE {{ interface.overlay }}/32
+{% else %}
+{% if 'gateway' in interface %}
+ gateway {{ interface.gateway }}
+{% endif %}
+{% for route in interface.static_routes | default([]) %}
+ up /bin/ip route add {{ route.destination }} via {{ route.gateway }}
+{% endfor %}
+{% for route in interface.static_routes | default([]) | reverse %}
+ down /bin/ip route del {{ route.destination }} via {{ route.gateway }}
+{% endfor %}
+{% endif %}
+{% if 'address6' in interface %}
+
+iface {{ interface.name }} inet6 static
+ address {{ interface.address6 }}
+{% if 'gateway6' in interface %}
+ gateway {{ interface.gateway6 }}
+{% endif %}
+{% for route in interface.static_routes6 | default([]) %}
+ up /bin/ip -6 route add {{ route.destination }} via {{ route.gateway }}
+{% endfor %}
+{% for route in interface.static_routes6 | default([]) | reverse %}
+ down /bin/ip -6 route del {{ route.destination }} via {{ route.gateway }}
+{% endfor %}
+{% endif %}
+{% endfor %}
diff --git a/roles/vm/guest/network/templates/resolv.conf.j2 b/roles/vm/guest/network/templates/resolv.conf.j2
new file mode 100644
index 00000000..00aaafe3
--- /dev/null
+++ b/roles/vm/guest/network/templates/resolv.conf.j2
@@ -0,0 +1,4 @@
+{% for nsrv in network_cooked.nameservers %}
+nameserver {{ nsrv }}
+{% endfor %}
+search {{ network_cooked.domain }}
diff --git a/roles/vm/guest/network/templates/systemd.link.j2 b/roles/vm/guest/network/templates/systemd.link.j2
new file mode 100644
index 00000000..7093e164
--- /dev/null
+++ b/roles/vm/guest/network/templates/systemd.link.j2
@@ -0,0 +1,5 @@
+[Match]
+Path=*pci-0000:01:{{ "%02d" | format(interface_index + 1) }}.0
+
+[Link]
+Name={{ item.name }}