diff --git a/Vagrantfile b/Vagrantfile index 542ded3..bdbf648 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -30,9 +30,10 @@ end def configure_router(i, config) config.vm.define "router#{i}" do |router| - router.vm.box = "generic/debian11" + router.vm.box = "generic/debian12" + router.vm.provision "shell", reboot: true, inline: "sudo systemctl enable systemd-networkd.service" lip = $ip.clone - router.vm.provision "shell", reboot: true, path:"dev/router-networking.sh", args: [lip] + router.vm.provision "shell", reboot: true, path:"dev/router-networking.sh", args: [lip, $host_only.to_s] if $host_only == false then if $bridge_interface != nil then router.vm.network "public_network", @@ -59,7 +60,7 @@ def configure_router(i, config) end configure_ram(router, $router_ram) - configure_private_network(router, true) + configure_private_network(router, false) router.vm.provision "shell" do |s| s.inline = "hostnamectl set-hostname $1" s.args = ["router"+i.to_s] @@ -70,8 +71,21 @@ end def configure_cluster_node(i, config) config.vm.define "cluster#{i}" do |clustervm| clustervm.vm.box = "NIAEFEUP/rocky-NInux" - clustervm.vm.box_version = "0.4.1" + clustervm.vm.box_version = "0.5.1" lip = $ip.clone + + # We enable nested virtualization for vm build tests in vagrant + clustervm.vm.provider "virtualbox" do |vb| + vb.customize ['modifyvm', :id, '--nested-hw-virt', 'on'] + end + + clustervm.vm.provider :libvirt do |libvirt| + # Enable KVM nested virtualization + libvirt.nested = true + libvirt.cpu_mode = "host-model" + end + + clustervm.vm.provision "shell" do |s| s.path = "dev/node-networking.sh" s.args = [lip] diff --git a/ansible-inventory.example.ini b/ansible-inventory.example.ini index 4ac348d..2cbe261 100644 --- a/ansible-inventory.example.ini +++ b/ansible-inventory.example.ini @@ -4,10 +4,11 @@ [routers] #while unusual, you can define multiple routers. -#You MUST always have only 1 router that is master, for VRRP +#You MUST always have only 1 router that is master, for VRRP. +# while bootstrapping you MUST leave this group out, but you will be asked to fill it in. -#10.0.0.1 master=true -#10.0.0.2 master=false +#router1 ansible_ssh_host=10.0.0.1 master=true +#router2 ansible_ssh_host=10.0.0.2 master=false [controlplane] # These are the kind of nodes that are responsible for managing @@ -17,10 +18,13 @@ # if you wish, you can specify an alias for a node, or you can just specify # the ip address as shown below: -#node1 ansible_ssh_host=10.0.0.2 +# you need to always define the ansible_ssh_host and ansible_ssh_private_key because they will be changed automatically +# you maybe to need to define the external interface (that will be given to the router, alongside the whole PCI device). + + +#node1 ansible_ssh_host=10.0.0.2 ansible_ssh_private_key=/path/to/private_key +#node1 ansible_ssh_host=10.0.0.2 ansible_ssh_private_key=/path/to/private_key external_interface=enp1f0 -#10.0.0.3 -#10.0.0.4 [workers] # These kinds of nodes are also connected to Kubernetes cluster, diff --git a/deploy-playbook.yaml b/deploy-playbook.yaml index 85d21d9..3e09edf 100644 --- a/deploy-playbook.yaml +++ b/deploy-playbook.yaml @@ -1,13 +1,44 @@ --- +- name: Install Router VMs + ansible.builtin.import_playbook: router/build-router-vm-playbook.yaml - name: Create a set new SSH key for clusters and routers ansible.builtin.import_playbook: networking/add-ssh-key-to-nodes-playbook.yaml -- name: Accept ssh keys for the first time - ansible.builtin.import_playbook: networking/accept-ssh-keys-playbook.yaml +- name: Networking - Setup static internal IPs + ansible.builtin.import_playbook: networking/dhcp-server-config-playbook.yaml +- name: Wait for connection + hosts: all + connection: local + gather_facts: false + tasks: + - name: Wait for nodes to change dhcp address + ansible.builtin.wait_for: + port: 22 + host: '{{ (ansible_ssh_host | default(ansible_host)) | default(inventory_hostname) }}' + search_regex: OpenSSH + delay: 10 + timeout: 120 +- name: Pre-setup - enable NTP syncronization + hosts: all + tasks: + - name: "Enable NTP client" + become: true + ansible.builtin.command: /usr/bin/timedatectl set-ntp on + changed_when: true + - name: "Switch to UTC timezone" + become: true + community.general.timezone: + name: "UTC" - name: Pre-setup - get correct interfaces ansible.builtin.import_playbook: networking/get-interface-playbook.yaml +- name: Pre-setup - Node re-enable NetworkManager + ansible.builtin.import_playbook: node/reenable-networkmanager-playbook.yaml +- name: Networking - Router initial config + ansible.builtin.import_playbook: networking/router-setup-playbook.yaml - name: Networking - Router BGP ansible.builtin.import_playbook: networking/router-bgp-playbook.yaml - name: Networking - VRRP ansible.builtin.import_playbook: networking/router-vrrp-playbook.yaml - name: Networking - Router Controlplane HA ansible.builtin.import_playbook: networking/controlplane-ha-playbook.yaml +- name: Nodes - Enable EPEL repositories + ansible.builtin.import_playbook: node/add-epel-repos-playbook.yaml diff --git a/dev/node-networking.sh b/dev/node-networking.sh index 8c013a4..e57fddd 100644 --- a/dev/node-networking.sh +++ b/dev/node-networking.sh @@ -1,6 +1,10 @@ #!/bin/sh -sudo ip r del 0.0.0.0 -sudo nmcli device modify ens5 ipv4.never-default yes -sudo nmcli con add type ethernet con-name main-network ifname ens6 ip4 10.10.0.$1/24 \ - gw4 10.10.0.254 -sudo nmcli con up main-network ifname ens6 \ No newline at end of file + +#TODO(luis): remove static ip configuration when dhcp server can be configured, to better replicate the physical node configuration +# sudo systemctl disable dhcpcd +# sudo systemctl enable --now NetworkManager +sudo ip r del default || true +# sudo nmcli device modify ens5 ipv4.never-default yes +# sudo nmcli con add type ethernet con-name main-network ifname ens6 ip4 10.10.0.$1/24 \ +# gw4 10.10.0.254 +# sudo nmcli con up main-network ifname ens6 \ No newline at end of file diff --git a/dev/router-networking.sh b/dev/router-networking.sh index eb8ef78..7ddc938 100644 --- a/dev/router-networking.sh +++ b/dev/router-networking.sh @@ -1,21 +1,42 @@ #!/bin/sh +apt-get purge -y ifupdown echo "net.ipv4.ip_forward = 1" > /etc/sysctl.d/10-router.conf +echo "[Match] +Name=eth0 + +[Network] +DHCP=yes +DefaultRouteOnDevice=false +" > /etc/systemd/network/01-vagrant.network + +if ["$2" -eq "true"]; then +echo "Configuring host-only" +echo "[Match] +Name=eth1 + +[Network] +Address=10.69.0.2/24 +Gateway=10.69.0.1 +DefaultRouteOnDevice=true +" > /etc/systemd/network/00-external.network +else +echo "Public network... fallback to dhcp" +fi +echo "[Match] +Name=* + +[Network] +DHCP=yes + +[Network] +LinkLocalAddressing=yes +IPv4LLRoute=true" > /etc/systemd/network/99-default-ipv4ll.network + echo " -allow-hotplug eth0 -auto lo -iface lo inet loopback -iface eth0 inet dhcp - post-up ip route del default dev eth0 || true - -auto eth2 -iface eth2 inet static - address 10.10.0.$1 - netmask 255.255.255.0 -" >> /etc/network/interfaces - -nft add table nat -nft add chain nat postrouting { type nat hook postrouting priority 100 \; } -nft add rule nat postrouting ip saddr 10.10.0.0/24 oif eth1 masquerade -nft list ruleset > /etc/nftables.conf -systemctl enable nftables \ No newline at end of file +nameserver 1.1.1.1 +" >> /etc/resolvconf/resolv.conf.d/tail + +apt-get install -y avahi-daemon avahi-utils avahi-autoipd + +sed -i 's/publish-workstation=no/publish-workstation=yes/g' /etc/avahi/avahi-daemon.conf \ No newline at end of file diff --git a/networking/accept-ssh-keys-playbook.yaml b/networking/accept-ssh-keys-playbook.yaml deleted file mode 100644 index 347e0bd..0000000 --- a/networking/accept-ssh-keys-playbook.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- name: Trust all ssh hosts if they don't exist yet - hosts: all - gather_facts: false - tasks: - - name: Accept SSH key for each host - connection: local - ansible.builtin.known_hosts: - state: present - name: "{{ hostvars[inventory_hostname]['ansible_ssh_host'] }}" - key: "{{ lookup('pipe', 'ssh-keyscan -T 10 -H -t ssh-ed25519 ' + hostvars[inventory_hostname]['ansible_ssh_host']) }}" diff --git a/networking/add-ssh-key-to-nodes-playbook.yaml b/networking/add-ssh-key-to-nodes-playbook.yaml index d173a4a..06fe028 100644 --- a/networking/add-ssh-key-to-nodes-playbook.yaml +++ b/networking/add-ssh-key-to-nodes-playbook.yaml @@ -39,7 +39,7 @@ when: inventory_hostname | regex_replace('\d', '') == 'cluster' throttle: 1 block: - - name: Propagate the publicg key + - name: Propagate the public key become: true ansible.posix.authorized_key: user: ni @@ -50,8 +50,8 @@ connection: local ansible.builtin.lineinfile: path: '{{ inventory_dir }}/ansible-inventory-dev.ini' - regexp: '^{{ inventory_hostname }}(.*)ansible_ssh_private_key_file=(.*)( .*)?' - line: '{{ inventory_hostname }}\1ansible_ssh_private_key_file={{ playbook_dir | dirname }}/.ssh/new_key\3' + regexp: '^{{ inventory_hostname }}(.*)ansible_ssh_private_key_file=(\S*) *(.*)' + line: '{{ inventory_hostname }}\1ansible_ssh_private_key_file={{ playbook_dir | dirname }}/.ssh/new_key \3' backrefs: true - name: Add the key and mofidy inventory if router @@ -59,7 +59,7 @@ block: - name: Propagate public key to this node ansible.posix.authorized_key: - user: vagrant + user: "{{ 'vagrant' if dev_cluster == 'true' else 'ni' }}" state: present key: "{{ new_ssh_key.stdout }}" exclusive: "{{ not dev_cluster }}" @@ -69,7 +69,7 @@ connection: local ansible.builtin.lineinfile: path: '{{ inventory_dir }}/ansible-inventory-dev.ini' - regexp: '^{{ inventory_hostname }}(.*)ansible_ssh_private_key_file=(.*) +(.*)' + regexp: '^{{ inventory_hostname }}(.*)ansible_ssh_private_key_file=(\S*) *(.*)' line: '{{ inventory_hostname }}\1ansible_ssh_private_key_file={{ playbook_dir | dirname }}/.ssh/new_key \3' backrefs: true diff --git a/networking/dhcp-server-config-playbook.yaml b/networking/dhcp-server-config-playbook.yaml new file mode 100644 index 0000000..96ab330 --- /dev/null +++ b/networking/dhcp-server-config-playbook.yaml @@ -0,0 +1,162 @@ +# TODO: Delay ansible for the IP addresses of the nodes to be changed +# TODO: Make this playbook only run once + +- name: Setup Kea config file + hosts: nodes + gather_facts: true + vars: + target_interface: 169.254.0.0 + # target_interface: 10.10.0.0 + tasks: + - name: Extract nodes MAC addresses + run_once: true # noqa: run-once[task] + ansible.builtin.set_fact: + mac_addresses: > + {{ (mac_addresses | default([])) + (ansible_play_hosts_all | map('extract', hostvars) | json_query('[].ansible_%s.macaddress' % item)) }} + when: > + ansible_play_hosts_all | map('extract', hostvars) | json_query('[].ansible_%s.ipv4' % item) + | selectattr('network', 'defined') | selectattr('network', '==', target_interface) + loop: "{{ ansible_play_hosts_all | map('extract', hostvars) | json_query('[].ansible_interfaces') | flatten | unique }}" + + - name: Skip for empty macaddresess list + when: mac_addresses is undefined or not mac_addresses + ansible.builtin.meta: end_play + + - name: Export MAC addresses to the routers + ansible.builtin.set_fact: + mac_addresses: "{{ mac_addresses }}" + delegate_to: "{{ item }}" + delegate_facts: true + with_items: "{{ groups['routers'] }}" + +- name: Setup internal IP for the routers + hosts: routers + vars: + target_network: 169.254.0.0 + tasks: + - name: Get internal interfaces + ansible.builtin.set_fact: + router_interface: "{{ item }}" + cacheable: true # cache it to other playbooks in this run use it + when: (hostvars[inventory_hostname]['ansible_%s' % item] | default({})).get('ipv4', {}).get('network') == target_network + loop: "{{ ansible_interfaces }}" + - name: Get internal interface if router dhcp has been defined + ansible.builtin.set_fact: + router_interface: "{{ item }}" + cacheable: true # cache it to other playbooks in this run use it + # FIXME (luisd): remove hardcoded ip on refactor + when: > + router_interface is undefined and + (hostvars[inventory_hostname]['ansible_%s' % item] | default({})).get('ipv4', {}).get('network') == "10.10.0.0" + loop: "{{ ansible_interfaces }}" + +- name: Setup static IP + hosts: routers + tasks: + - name: Skip for empty macaddresess list + when: mac_addresses is undefined or not mac_addresses + ansible.builtin.meta: end_play + + - name: Setup internal network config to vm + become: true + ansible.builtin.template: + src: templates/02-internal.network.j2 + dest: /etc/systemd/network/02-internal.network + mode: "644" + - name: Restart network service on VM + become: true + ansible.builtin.systemd: + no_block: true + name: systemd-networkd + state: restarted + changed_when: true + ignore_unreachable: true + async: 10 + poll: 0 + - name: Update routers IP's + connection: local + ansible.builtin.lineinfile: + path: '{{ inventory_dir }}/ansible-inventory-dev.ini' + regexp: '^{{ inventory_hostname }}(.*)ansible_ssh_host=([^ ]*) (.*)' + line: '{{ inventory_hostname }}\1ansible_ssh_host=10.10.0.{{ (inventory_hostname[-1] | int + 1) | string }} \3' + backrefs: true + register: router_update + - name: Refresh inventory + ansible.builtin.meta: refresh_inventory + - name: Wait for systemd-networkd + when: router_update.changed # noqa: no-handler + ansible.builtin.wait_for: + port: 22 + host: '{{ (ansible_ssh_host | default(ansible_host)) | default(inventory_hostname) }}' + search_regex: OpenSSH + delay: 10 + timeout: 120 + delegate_to: localhost + - name: Add nameserver to VM + when: router_update.changed # noqa: no-handler + become: true + ansible.builtin.lineinfile: + path: /etc/resolvconf/resolv.conf.d/tail + line: nameserver 1.1.1.1 + mode: "644" + create: true + +- name: Run DHCP4 Server with Kea + hosts: routers + tasks: + - name: Install kea package + become: true + ansible.builtin.apt: + name: kea + update_cache: true + register: kea_install + + - name: Delete default config if kea was installed # noqa: no-handler + when: kea_install.changed + become: true + ansible.builtin.file: + path: /etc/kea/kea-dhcp4.conf + state: absent + + - name: Get old config file + ansible.builtin.command: + cmd: cat /etc/kea/kea-dhcp4.conf + failed_when: false # never fail because it will be handle in the next task + changed_when: true + register: old_config + + - name: Write old mac addresses + ansible.builtin.set_fact: + mac_addresses: > + {{ (old_config.stdout | from_json | json_query('Dhcp4.subnet4[0].reservations[*]."hw-address"') | default([])) + (mac_addresses | default([])) }} + when: old_config.rc == 0 + + - name: Write config file + become: true + ansible.builtin.template: + src: templates/kea-dhcp4.conf.j2 + dest: /etc/kea/kea-dhcp4.conf + mode: "644" + - name: Create leases file if not exists + become: true + ansible.builtin.file: + path: /var/lib/kea/dhcp4.leases + state: touch + mode: '0700' + - name: Update nodes IP's + connection: local + ansible.builtin.lineinfile: + path: '{{ inventory_dir }}/ansible-inventory-dev.ini' + regexp: '^{{ item }}(.*)ansible_ssh_host=([^ ]*) (.*)' + line: > + {{ item }}\1ansible_ssh_host=10.10.0.{{ (groups["nodes"].index(item) + 1 + (groups["routers"] | length) + 1) | string }} \3 + backrefs: true + loop: "{{ groups['nodes'] }}" + + - name: Restart kea dhcp server + become: true + ansible.builtin.systemd: + service: kea-dhcp4-server + state: restarted + - name: Refresh the inventory + ansible.builtin.meta: refresh_inventory diff --git a/networking/get-interface-playbook.yaml b/networking/get-interface-playbook.yaml index 5196618..eaeca7b 100644 --- a/networking/get-interface-playbook.yaml +++ b/networking/get-interface-playbook.yaml @@ -9,3 +9,8 @@ cacheable: true # cache it to other playbooks in this run use it when: (hostvars[inventory_hostname]['ansible_%s' % item] | default({})).get('ipv4', {}).get('network') == target_network loop: "{{ ansible_interfaces }}" + + - name: Fail if there's no dhcp IP + when: not target_interface + ansible.builtin.fail: + msg: "Node {{ inventory_hostname }} failed to get a DHCP lease." diff --git a/networking/router-setup-playbook.yaml b/networking/router-setup-playbook.yaml new file mode 100644 index 0000000..5eac0ac --- /dev/null +++ b/networking/router-setup-playbook.yaml @@ -0,0 +1,29 @@ +--- + +- name: Networking - Router basic setup + hosts: routers + tasks: + - name: Enable Layer 3 forwarding + become: true + ansible.posix.sysctl: + sysctl_set: true + name: net.ipv4.ip_forward + value: 1 + - name: Install nftables on router + become: true + ansible.builtin.apt: + name: + - nftables + state: present + - name: Copy nftables config + become: true + ansible.builtin.template: + src: templates/nftables.conf.j2 + dest: /etc/nftables.conf + mode: "755" + - name: Restart nftables + become: true + ansible.builtin.systemd: + name: nftables + state: restarted + changed_when: true diff --git a/networking/templates/02-internal.network.j2 b/networking/templates/02-internal.network.j2 new file mode 100644 index 0000000..e8701a6 --- /dev/null +++ b/networking/templates/02-internal.network.j2 @@ -0,0 +1,6 @@ +[Match] +Name={{router_interface}} + +[Network] +Address=10.10.0.{{ (inventory_hostname[-1] | int + 1) | string }}/24 +DNS=1.1.1.1 diff --git a/networking/templates/kea-dhcp4.conf.j2 b/networking/templates/kea-dhcp4.conf.j2 new file mode 100644 index 0000000..cef03b8 --- /dev/null +++ b/networking/templates/kea-dhcp4.conf.j2 @@ -0,0 +1,36 @@ +{ + "Dhcp4": { + "interfaces-config": { + "interfaces": [ "{{ router_interface }}" ], + "dhcp-socket-type": "raw" + }, + "valid-lifetime": 4000, + "max-valid-lifetime": 7200, + "subnet4": [{ + "pools": [ { "pool": "10.10.0.200-10.10.0.249" } ], + "subnet": "10.10.0.0/24", + "option-data": [ + { + "name": "routers", + "data": "10.10.0.254" + }, + { + "name": "domain-name-servers", + "data": "1.1.1.1, 1.0.0.1" + } + ], + "reservations": + [ + {%for mac_address in mac_addresses %} + { + "hw-address": "{{mac_address}}", + "ip-address": "10.10.0.{{ (groups["routers"] | length) + loop.index + 1}}" + } + {% if loop.index != loop.length%} + , + {% endif %} + {% endfor %} + ] + }] + } +} \ No newline at end of file diff --git a/networking/templates/nftables.conf.j2 b/networking/templates/nftables.conf.j2 new file mode 100644 index 0000000..6d26d15 --- /dev/null +++ b/networking/templates/nftables.conf.j2 @@ -0,0 +1,11 @@ +#!/usr/sbin nft -f + +flush ruleset + + +table ip nat { + chain postrouting { + type nat hook postrouting priority srcnat; policy accept; + ip saddr 10.10.0.0/24 oif "{{ 'enp0s1f0' if dev_cluster == 'false' else 'eth1' }}" masquerade + } +} \ No newline at end of file diff --git a/networking/templates/router-haproxy.cfg.j2 b/networking/templates/router-haproxy.cfg.j2 index ba7d5c5..d18efe4 100644 --- a/networking/templates/router-haproxy.cfg.j2 +++ b/networking/templates/router-haproxy.cfg.j2 @@ -49,4 +49,5 @@ backend apiserver server {{nodename}} {{ hostvars[nodename]["ansible_"~ hostvars[nodename]["ansible_facts"]["target_interface"]]['ipv4']['address'] }}:6443 check - {% endfor %} \ No newline at end of file + {% endfor %} +#--------------------------------------------------------------------- diff --git a/node/Dockerfile b/node/Dockerfile index 6b8a6f7..bdc7bbd 100644 --- a/node/Dockerfile +++ b/node/Dockerfile @@ -1,3 +1,10 @@ FROM rockylinux:9 +RUN dnf install epel-release -y +RUN dnf install lorax createrepo yum-utils -y -RUN dnf install lorax -y \ No newline at end of file +RUN mkdir /NInux +WORKDIR /NInux +RUN yumdownloader --resolve dhcpcd avahi avahi-tools +RUN createrepo -v /NInux + +WORKDIR / \ No newline at end of file diff --git a/node/add-epel-repos-playbook.yaml b/node/add-epel-repos-playbook.yaml new file mode 100644 index 0000000..85bfc72 --- /dev/null +++ b/node/add-epel-repos-playbook.yaml @@ -0,0 +1,12 @@ +- name: Add EPEL repos to all nodes + hosts: nodes + tasks: + - name: Enable CRB + become: true + ansible.builtin.command: /usr/bin/dnf config-manager --set-enabled crb + changed_when: true # AFAIK there's no way to check if this is enabled or not + - name: Install EPEL repo + become: true + ansible.builtin.dnf: + name: "epel-release" + state: present diff --git a/node/create-iso.sh b/node/create-iso.sh index b66092f..9f6bea2 100755 --- a/node/create-iso.sh +++ b/node/create-iso.sh @@ -14,4 +14,4 @@ rm -rf ninux.iso docker build . -t ninux-make-iso -docker run -v .:/vol ninux-make-iso mkksiso /vol/ks.cfg /vol/rocky.iso /vol/ninux.iso \ No newline at end of file +docker run --privileged=true -v .:/vol ninux-make-iso mkksiso --add /NInux --ks /vol/ks.cfg /vol/rocky.iso /vol/ninux.iso \ No newline at end of file diff --git a/node/ks.cfg b/node/ks.cfg index c428818..7445d8c 100644 --- a/node/ks.cfg +++ b/node/ks.cfg @@ -5,6 +5,7 @@ text # Use CDROM installation media cdrom +repo --name=NInux --baseurl=file:///run/install/repo/NInux %addon com_redhat_kdump --enable --reserve-mb='auto' @@ -18,6 +19,10 @@ lang en_US.UTF-8 %packages @^minimal-environment @standard +# TODO(luisd): Test if upgrades work +avahi +avahi-tools +dhcpcd %end @@ -31,7 +36,7 @@ firstboot --enable clearpart --all --drives=sda --initlabel autopart --nohome -bootloader +bootloader --append="intel_iommu=on" timesource --ntp-disable # System timezone @@ -67,4 +72,21 @@ install \ # TODO: fix this in the ansible side later chmod +x /etc +# Avahi related configs, for cluster bootstrapping in order to to use IPV4LL + +sed -i 's/publish-workstation=no/publish-workstation=yes/g' /etc/avahi/avahi-daemon.conf + +echo "[main] +dhcp=dhcpcd" > /etc/NetworkManager/conf.d/dhcp-client.conf + +# Disable NetworkManager and use dhcpcd instead to use IPv4LL (NetworkManager configuration is buggy, at best) +# we enable NetworkManager again when the full network is configured. + +# TODO(luisd): enable NetworkManager on ansible side after network configuration +systemctl disable NetworkManager +systemctl enable dhcpcd + +# Firewalld causes more problems than it help, on cluster bootstrapping. It interferes with RKE2 and mDNS, +# making the cluster more difficult to deploy. In the future we can reenable-it and whitelist what's needed. +systemctl disable firewalld %end \ No newline at end of file diff --git a/node/packer.pkr.hcl b/node/packer.pkr.hcl index 6720bc3..8d4ddb1 100644 --- a/node/packer.pkr.hcl +++ b/node/packer.pkr.hcl @@ -8,6 +8,10 @@ packer { version = "~> 1" source = "github.com/hashicorp/virtualbox" } + qemu = { + version = ">= 1.0.10" + source = "github.com/hashicorp/qemu" + } } } diff --git a/node/reenable-networkmanager-playbook.yaml b/node/reenable-networkmanager-playbook.yaml new file mode 100644 index 0000000..1e46b5f --- /dev/null +++ b/node/reenable-networkmanager-playbook.yaml @@ -0,0 +1,80 @@ +--- + +- name: Stop dhcp kea server + hosts: routers + become: true + tasks: + - name: Delete old leases + become: true + ansible.builtin.command: + cmd: rm -f /var/lib/kea/kea-leases4.csv + changed_when: true + + - name: Stop kea + ansible.builtin.systemd: + service: kea-dhcp4-server + state: stopped + +- name: Re-enable NetworkManager on nodes + hosts: nodes + become: true + tasks: + - name: Get service facts + ansible.builtin.service_facts: + + - name: Remove dhcpcd if it's enabled and add NetworkManager + when: > + 'dhcpcd.service' in ansible_facts.services and ansible_facts.services['dhcpcd.service']['status'] == 'enabled' + block: + - name: Disable dhcpcd + ansible.builtin.systemd_service: + name: dhcpcd + enabled: false + state: stopped + - name: Enable NetworkManager + ansible.builtin.systemd_service: + name: NetworkManager + enabled: true + - name: Delete system-connections folder + ansible.builtin.file: + path: /etc/NetworkManager/system-connections + state: absent + - name: Create system-connections folder + ansible.builtin.file: + path: /etc/NetworkManager/system-connections + state: directory + mode: "744" + - name: Copy internal network template + ansible.builtin.template: + src: templates/internal-network.nmconnection.j2 + dest: /etc/NetworkManager/system-connections/internal-network.nmconnection + mode: "600" + - name: Set flag for reboot + ansible.builtin.set_fact: + needs_reboot_dhcp: true + +- name: Start dhcp kea server + hosts: routers + become: true + tasks: + - name: Start kea + ansible.builtin.systemd: + service: kea-dhcp4-server + state: started + + +- name: Restart node if necessary + hosts: nodes + tasks: + - name: Check if restart is necessary + when: needs_reboot_dhcp is defined + block: + - name: Get current kernel version for reboot + ansible.builtin.command: + cmd: uname -r + register: kernel_version + changed_when: false + - name: Reboot using kexec to apply the networkmanager changes + become: true + ansible.builtin.reboot: + reboot_command: "kexec /boot/vmlinuz-{{ kernel_version.stdout }} --initrd=/boot/initramfs-{{ kernel_version.stdout }}.img --reuse-cmdline" diff --git a/node/templates/internal-network.nmconnection.j2 b/node/templates/internal-network.nmconnection.j2 new file mode 100644 index 0000000..646b5aa --- /dev/null +++ b/node/templates/internal-network.nmconnection.j2 @@ -0,0 +1,15 @@ +[connection] +id={{ target_interface }} +type=ethernet +interface-name={{ target_interface }} + +[ethernet] + +[ipv4] +method=auto + +[ipv6] +addr-gen-mode=eui64 +method=auto + +[proxy] diff --git a/requirements.yml b/requirements.yml index d23b947..6ebe7ad 100644 --- a/requirements.yml +++ b/requirements.yml @@ -1,3 +1,6 @@ collections: - ansible.posix + - community.libvirt + - ansible.utils + - community.general - community.crypto diff --git a/router/build-router-vm-playbook.yaml b/router/build-router-vm-playbook.yaml new file mode 100644 index 0000000..e60870f --- /dev/null +++ b/router/build-router-vm-playbook.yaml @@ -0,0 +1,161 @@ +- name: Build router VM in hosts that have the appropriate NICs + hosts: controlplane + tasks: + - name: Skip play if running in the development cluster + when: dev_cluster == "true" + ansible.builtin.meta: end_play + - name: Skip host if it doesnt have external_interface defined + when: external_interface is undefined + ansible.builtin.meta: end_host + - name: Import router vars + ansible.builtin.include_vars: + file: "router-vars.json" + - name: Check if libvirt is installed + ansible.builtin.service_facts: + - name: Check if router VM exists + become: true + community.libvirt.virt: + command: list_vms + register: routervm_stat + when: "'libvirtd.service' in ansible_facts.services" + # we only want to remove vfio if there isn't a router anymore in order to make this reproducible + - name: Remove vfio pci devices modprobe + become: true + when: '"libvirtd.service" in ansible_facts.services and "routerVM" not in routervm_stat.list_vms' + ansible.builtin.file: + path: /etc/modprobe.d/vfio.conf + state: absent + notify: + - Update initramfs and reboot + - name: Flush handlers + ansible.builtin.meta: flush_handlers + - name: Get PCI devices to find appropriate NICs + ansible.utils.cli_parse: + command: lspci -Dn + parser: + name: ansible.utils.textfsm + template_path: router/templates/lspci-parser.textfsm + set_fact: pci_devices + - name: Remove dhcpcd persistance + become: true + ansible.builtin.lineinfile: + path: /etc/dhcpcd.conf + regexp: "^persistent" + line: "#persistent" + notify: + - Restart dhcpcd + - name: Remove external_interface from dhcpcd + become: true + ansible.builtin.lineinfile: + path: /etc/dhcpcd.conf + regexp: "^denyinterfaces.*" + line: "denyinterfaces {{ external_interface }}" + notify: + - Restart dhcpcd + - name: Flush handlers + ansible.builtin.meta: flush_handlers + + - name: Only include nodes that have suitable devices + when: > + (pci_devices | map(attribute="pciVendorDevice") | intersect(valid_nic_ids) | length) != 0 + block: + - name: Assign one public IP per router + ansible.builtin.set_fact: + assigned_network: "{{ external_eth_config[play_hosts.index(inventory_hostname)] }}" + delegate_to: "{{ item }}" + with_items: "{{ ansible_play_hosts }}" + + - name: Build new router VM + when: '"libvirtd.service" not in ansible_facts.services or "routerVM" not in routervm_stat.list_vms' + block: + - name: Get PCI ID per interface + ansible.builtin.set_fact: + device_by_pci_address: "{{ ansible_facts | json_query('@.* | [?pciid].{key: device, value: pciid}') | items2dict }}" + - name: Parse PCI ID of external interface + ansible.utils.cli_parse: + text: "{{ device_by_pci_address[external_interface] }}" + parser: + name: ansible.utils.textfsm + template_path: router/templates/pci-id-parser.textfsm + set_fact: external_interface_pci_device + - name: Filter pci_devices to include the same device as the external interface + ansible.builtin.set_fact: + pci_devices: "{{ pci_devices | selectattr('bus', 'equalto', external_interface_pci_device[0]['bus']) + | selectattr('device', 'equalto', external_interface_pci_device[0]['device']) + | selectattr('bus', 'equalto', external_interface_pci_device[0]['bus']) + }}" + - name: Set external interface up + become: true + ansible.builtin.command: + cmd: "ip link set up {{ external_interface }}" + changed_when: true + # This might fail a few times until the interface comes up + - name: Configure external ip to configure VM temporarily + become: true + retries: 3 + delay: 5 + ansible.builtin.command: + cmd: "ip addr add {{ assigned_network.ip + '/' + assigned_network.prefix }} dev {{ external_interface }}" + changed_when: true + - name: Configure default route + become: true + ansible.builtin.command: + cmd: "ip route add default dev {{ external_interface }} via {{ assigned_network.gateway }}" + changed_when: true + - name: Configure DNS + become: true + ansible.builtin.lineinfile: + dest: /etc/resolv.conf + line: "nameserver {{ assigned_network.nameservers[0] }}" + - name: Build VM disk image + ansible.builtin.include_tasks: + file: "vm-image-task.ansible.yaml" + - name: Enable vfio modules + become: true + community.general.modprobe: + state: present + persistent: present + name: "{{ item }}" + with_items: + - "vfio" + - "vfio_pci" + - "vfio_iommu_type1" + - name: Set device ids to load the vfio driver instead + become: true + ansible.builtin.template: + src: templates/vfio.conf.j2 + dest: /etc/modprobe.d/vfio.conf + mode: "644" + notify: + - Update initramfs and reboot + - name: Flush handlers + ansible.builtin.meta: flush_handlers + - name: Create VM for router + become: true + community.libvirt.virt: + autostart: true + name: routerVM + command: define + xml: "{{ lookup('template', 'templates/libvirt-xml.j2') }}" + - name: Start VM + become: true + community.libvirt.virt: + name: routerVM + state: running + - name: Pause until user inputs router inventory settings + when: dev_cluster == "false" + ansible.builtin.pause: + prompt: "Please verify that the routers are properly up and add them to the corresponding inventory file" + - name: Refresh inventory + ansible.builtin.meta: refresh_inventory + + + handlers: + - name: Update initramfs and reboot + ansible.builtin.include_tasks: + file: "update-initramfs-tasks.ansible.yaml" + - name: Restart dhcpcd + become: true + ansible.builtin.systemd: + name: dhcpcd + state: restarted diff --git a/router/router-vars.json b/router/router-vars.json new file mode 100644 index 0000000..6fde611 --- /dev/null +++ b/router/router-vars.json @@ -0,0 +1,22 @@ +{ + "valid_nic_ids": [ + "8086:1528" + ], + "guest_nic_identifier": { + "domain": 0, + "bus": 0, + "device": 1, + "function": 0 + }, + "external_eth_config": [ + { + "ip": "192.168.1.5", + "prefix": "24", + "gateway": "192.168.1.1", + "hw_addr": "c4:34:6b:5e:c6:45", + "nameservers": [ + "1.1.1.1" + ] + } + ] +} \ No newline at end of file diff --git a/router/templates/01-external.network.j2 b/router/templates/01-external.network.j2 new file mode 100644 index 0000000..718b805 --- /dev/null +++ b/router/templates/01-external.network.j2 @@ -0,0 +1,12 @@ +[Match] +Name=enp0s1f0 + +[Link] +MACAddress={{assigned_network.hw_addr}} + +[Network] +Address={{assigned_network.ip}}/{{assigned_network.prefix}} +Gateway={{assigned_network.gateway}} +{% for dns in assigned_network.nameservers %} +DNS={{ dns }} +{% endfor %} diff --git a/router/templates/99-ipv4ll.network b/router/templates/99-ipv4ll.network new file mode 100644 index 0000000..2564fff --- /dev/null +++ b/router/templates/99-ipv4ll.network @@ -0,0 +1,7 @@ +[Match] +Name=* +Type=ether + +[Network] +LinkLocalAddressing=yes +IPv4LLRoute=true \ No newline at end of file diff --git a/router/templates/libvirt-xml.j2 b/router/templates/libvirt-xml.j2 new file mode 100644 index 0000000..9de1151 --- /dev/null +++ b/router/templates/libvirt-xml.j2 @@ -0,0 +1,183 @@ + + routerVM + 512 + 512 + 2 + + hvm + + + + + + + + + + + + + destroy + restart + restart + + + + + + /usr/libexec/qemu-kvm + + + + + + +
+ + + +
+ + + + + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + /dev/urandom + +
+ +{# FIXME(luisd): load accepted pci_vendor_devices from a variable #} +{% for device in (pci_devices | selectattr("pciVendorDevice", "in",valid_nic_ids)) %} + + + +
+ +
+ + +{% endfor %} + + \ No newline at end of file diff --git a/router/templates/lspci-parser.textfsm b/router/templates/lspci-parser.textfsm new file mode 100644 index 0000000..4f47653 --- /dev/null +++ b/router/templates/lspci-parser.textfsm @@ -0,0 +1,8 @@ +Value domain ([0-9a-fA-F]{4}) +Value bus ([0-9a-fA-F]{2}) +Value device ([0-9a-fA-F]{2}) +Value function ([0-7]) +Value pciVendorDevice ([0-9a-fA-F]{4}:[0-9a-fA-F]{4}) + +Start + ^${domain}:${bus}:${device}\.${function}.*: *${pciVendorDevice} -> Next.Record \ No newline at end of file diff --git a/router/templates/lspci-parser.yaml b/router/templates/lspci-parser.yaml new file mode 100644 index 0000000..183a0a3 --- /dev/null +++ b/router/templates/lspci-parser.yaml @@ -0,0 +1,10 @@ +--- +- example: "0000:01:00.1 0200: 8086:1528 (rev 01)" + getval: '(?P[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]):(?P[0-9a-fA-F][0-9a-fA-F]):(?P[0-9a-fA-F][0-9a-fA-F])\.(?P[0-7])\s*([0-9]*)\s*:\s* (?P[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]).*' + result: + "{{ bus + ':' + device + '.' + function }}": + domain: "{{ domain }}" + bus: "{{ bus }}" + device: '{{ device }}' + function: "{{ function }}" + pci_vendor_device: "{{ pci_vendor_device }}" \ No newline at end of file diff --git a/router/templates/pci-id-parser.textfsm b/router/templates/pci-id-parser.textfsm new file mode 100644 index 0000000..014efa6 --- /dev/null +++ b/router/templates/pci-id-parser.textfsm @@ -0,0 +1,8 @@ +Value domain ([0-9a-fA-F]{4}) +Value bus ([0-9a-fA-F]{2}) +Value device ([0-9a-fA-F]{2}) +Value function ([0-7]) + + +Start + ^${domain}:${bus}:${device}\.${function} -> Record \ No newline at end of file diff --git a/router/templates/vfio.conf.j2 b/router/templates/vfio.conf.j2 new file mode 100644 index 0000000..7f0031c --- /dev/null +++ b/router/templates/vfio.conf.j2 @@ -0,0 +1 @@ +options vfio-pci ids={{valid_nic_ids | join(',')}} \ No newline at end of file diff --git a/router/update-initramfs-tasks.ansible.yaml b/router/update-initramfs-tasks.ansible.yaml new file mode 100644 index 0000000..16acd73 --- /dev/null +++ b/router/update-initramfs-tasks.ansible.yaml @@ -0,0 +1,16 @@ +- name: Update initramfs + become: true + ansible.builtin.command: + cmd: dracut -f -v + changed_when: true +- name: Get current kernel version for reboot + ansible.builtin.command: + cmd: uname -r + register: kernel_version + changed_when: false +- name: Reboot using kexec to apply the driver changes + become: true + ansible.builtin.reboot: + reboot_command: "kexec /boot/vmlinuz-{{ kernel_version.stdout }} --initrd=/boot/initramfs-{{ kernel_version.stdout }}.img --reuse-cmdline" +- name: Re-gather facts because pci information might be outdated + ansible.builtin.gather_facts: diff --git a/router/vm-image-task.ansible.yaml b/router/vm-image-task.ansible.yaml new file mode 100644 index 0000000..e05cdb2 --- /dev/null +++ b/router/vm-image-task.ansible.yaml @@ -0,0 +1,162 @@ +- name: "Install virtualization utils" + become: true + ansible.builtin.dnf: + name: + - qemu-kvm + - libvirt + - virt-install + - guestfs-tools + state: present +- name: "Enable libvirt service" + become: true + ansible.builtin.systemd: + name: libvirtd + state: "started" + enabled: true +- name: "Create VM disks folder" + become: true + ansible.builtin.file: + recurse: true + state: directory + path: "/srv/vm/disks" + mode: "770" + group: qemu +- name: "Create router disk folder" + become: true + ansible.builtin.file: + recurse: true + state: directory + path: "/srv/vm/disks/router" + mode: "770" + group: qemu +- name: "Download router QCOW2 file" + retries: 3 + become: true + ansible.builtin.get_url: + url: https://cloud.debian.org/images/cloud/bookworm/20240211-1654/debian-12-generic-amd64-20240211-1654.qcow2 + dest: /srv/vm/disks/router/router.qcow2 + checksum: sha512:b679398972ba45a60574d9202c4f97ea647dd3577e857407138b73b71a3c3c039804e40aac2f877f3969676b6c8a1ebdb4f2d67a4efa6301c21e349e37d43ef5 + mode: "770" +- name: "Resize router QCOW2 file" + become: true + ansible.builtin.command: + cmd: qemu-img resize /srv/vm/disks/router/router.qcow2 5G + changed_when: true + +- name: "Resize router QCOW2 partition" + block: + - name: "Change old file router name" + become: true + ansible.builtin.command: + cmd: cp /srv/vm/disks/router/router.qcow2 /srv/vm/disks/router/router.qcow2.old + creates: /srv/vm/disks/router/router.qcow2.old + changed_when: true + - name: "Resize filesystem partition for router" + become: true + ansible.builtin.command: + cmd: virt-resize --expand /dev/sda1 /srv/vm/disks/router/router.qcow2.old /srv/vm/disks/router/router.qcow2 + changed_when: true + - name: "Delete old router qcow file" + become: true + ansible.builtin.file: + path: /srv/vm/disks/router/router.qcow2.old + state: absent + +- name: Create user on vm + become: true + ansible.builtin.command: + cmd: virt-customize -a /srv/vm/disks/router/router.qcow2 --run-command 'useradd -m -s /bin/bash -g sudo ni' + changed_when: true + +- name: Create password on user + become: true + ansible.builtin.command: + cmd: virt-customize -a /srv/vm/disks/router/router.qcow2 --run-command 'echo "ni:n1bl04t:)" | chpasswd' + changed_when: true + +- name: "Create mount directory" + become: true + ansible.builtin.file: + state: directory + path: "/mnt/router" + mode: "644" + +- name: Update package repos + become: true + ansible.builtin.command: + cmd: virt-customize -a /srv/vm/disks/router/router.qcow2 --run-command 'apt-get update' + changed_when: true + +- name: Install Avahi on VM + become: true + ansible.builtin.command: + cmd: virt-customize -a /srv/vm/disks/router/router.qcow2 --run-command 'apt-get install -y avahi-daemon avahi-utils' + changed_when: true + + +- name: Add host private key files + become: true + ansible.builtin.command: + cmd: virt-customize -a /srv/vm/disks/router/router.qcow2 --run-command 'ssh-keygen -A' + changed_when: true + +- name: "VM file provisioning" + block: + - name: "Mount router image" + become: true + ansible.builtin.command: guestmount -a /srv/vm/disks/router/router.qcow2 -m /dev/sda3 /mnt/router + changed_when: true + register: image_create_dev + failed_when: image_create_dev.rc != 0 + - name: Create .ssh folder on vm + become: true + ansible.builtin.file: + path: /mnt/router/home/{{ ansible_ssh_user }}/.ssh + state: directory + owner: "{{ ansible_ssh_user }}" + group: "{{ ansible_ssh_user }}" + mode: "755" + - name: Copy authorized_keys to vm + become: true + ansible.builtin.copy: + src: /home/ni/.ssh/authorized_keys + dest: /mnt/router/home/ni/.ssh/authorized_keys + remote_src: true + mode: "644" + - name: Add ni user to sudoers password + become: true + ansible.builtin.copy: + dest: /mnt/router/etc/sudoers.d/dont-prompt-ni_user-for-sudo-password + content: 'ni ALL=(ALL) NOPASSWD:ALL' + mode: "644" + - name: Copy avahi dhcp fallback to vm + become: true + ansible.builtin.copy: + src: templates/99-ipv4ll.network + dest: /mnt/router/etc/systemd/network/99-ipv4ll.network + mode: "644" + - name: Copy interface network config to vm + become: true + ansible.builtin.template: + src: templates/01-external.network.j2 + dest: /mnt/router/etc/systemd/network/01-external.network + mode: "644" + - name: Enable avahi discoverability + become: true + ansible.builtin.lineinfile: + path: /mnt/router/etc/avahi/avahi-daemon.conf + regexp: '^publish-workstation=' + line: publish-workstation=yes + always: + - name: "Umount router partition" + become: true + ansible.builtin.command: + cmd: guestunmount /mnt/router + failed_when: false + changed_when: true + +- name: Fix grub on VM + become: true + ansible.builtin.command: + cmd: virt-customize -a /srv/vm/disks/router/router.qcow2 --run-command 'grub-install /dev/sda' + changed_when: true