diff --git a/ansible.cfg b/ansible.cfg index 9b89217..762fc60 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,6 +1,7 @@ [defaults] # Explicitely redefined some defaults to make play execution work roles_path = ./roles +lookup_plugins = ./lookup_plugins vars_plugins = ./vars_plugins inventory = ./hosts diff --git a/group_vars/all/network_interfaces.yml b/group_vars/all/network_interfaces.yml new file mode 100644 index 0000000..238dedc --- /dev/null +++ b/group_vars/all/network_interfaces.yml @@ -0,0 +1,20 @@ +glob_network_interfaces: + vlan: + - name: srv + id: 1 + gateway: "185.230.76.62" + dns: "{{ query('ldap', 'ip', 'routeur-templier', 'srv') | ipv4 | first }}" + gateway_v6: "2a0c:700:3002::ff:fe02:102" + - name: adm + id: 42 + dns: "{{ query('ldap', 'ip', 'routeur-templier', 'adm') | ipv4 | first }}" + - name: srv_nat + id: 43 + gateway: "{{ query('ldap', 'ip', 'routeur-templier', 'srv-nat') | ipv4 | first }}" + dns: "{{ query('ldap', 'ip', 'routeur-templier', 'srv-nat') | ipv4 | first }}" + gateway_v6: "{{ query('ldap', 'ip', 'routeur-templier', 'srv-nat') | ipv6 | first }}" + + +# Deploy only adm by default +interfaces: + adm: eth0 diff --git a/host_vars/dns.adm.ynerant.fr.yml b/host_vars/dns.adm.ynerant.fr.yml new file mode 100644 index 0000000..2eb6f99 --- /dev/null +++ b/host_vars/dns.adm.ynerant.fr.yml @@ -0,0 +1,4 @@ +--- +interfaces: + adm: eth0 + srv_nat: eth1 diff --git a/host_vars/docker.adm.ynerant.fr.yml b/host_vars/docker.adm.ynerant.fr.yml new file mode 100644 index 0000000..2eb6f99 --- /dev/null +++ b/host_vars/docker.adm.ynerant.fr.yml @@ -0,0 +1,4 @@ +--- +interfaces: + adm: eth0 + srv_nat: eth1 diff --git a/host_vars/gitea.adm.ynerant.fr.yml b/host_vars/gitea.adm.ynerant.fr.yml new file mode 100644 index 0000000..2eb6f99 --- /dev/null +++ b/host_vars/gitea.adm.ynerant.fr.yml @@ -0,0 +1,4 @@ +--- +interfaces: + adm: eth0 + srv_nat: eth1 diff --git a/host_vars/mailu.adm.ynerant.fr.yml b/host_vars/mailu.adm.ynerant.fr.yml new file mode 100644 index 0000000..2eb6f99 --- /dev/null +++ b/host_vars/mailu.adm.ynerant.fr.yml @@ -0,0 +1,4 @@ +--- +interfaces: + adm: eth0 + srv_nat: eth1 diff --git a/host_vars/nextcloud.adm.ynerant.fr.yml b/host_vars/nextcloud.adm.ynerant.fr.yml new file mode 100644 index 0000000..2eb6f99 --- /dev/null +++ b/host_vars/nextcloud.adm.ynerant.fr.yml @@ -0,0 +1,4 @@ +--- +interfaces: + adm: eth0 + srv_nat: eth1 diff --git a/host_vars/proxy.adm.ynerant.fr.yml b/host_vars/proxy.adm.ynerant.fr.yml new file mode 100644 index 0000000..2eb6f99 --- /dev/null +++ b/host_vars/proxy.adm.ynerant.fr.yml @@ -0,0 +1,4 @@ +--- +interfaces: + adm: eth0 + srv_nat: eth1 diff --git a/host_vars/psql.adm.ynerant.fr.yml b/host_vars/psql.adm.ynerant.fr.yml new file mode 100644 index 0000000..5cde204 --- /dev/null +++ b/host_vars/psql.adm.ynerant.fr.yml @@ -0,0 +1,3 @@ +--- +interfaces: + adm: eth0 diff --git a/hosts b/hosts index 8c78149..4f1dee3 100644 --- a/hosts +++ b/hosts @@ -33,6 +33,9 @@ mailu.adm.ynerant.fr [reverseproxy] proxy.adm.ynerant.fr +[routeur] +routeur-templier.adm.ynerant.fr + [server:children] virtu vm diff --git a/lookup_plugins/__pycache__/ldap.cpython-39.pyc b/lookup_plugins/__pycache__/ldap.cpython-39.pyc new file mode 100644 index 0000000..4055b1e Binary files /dev/null and b/lookup_plugins/__pycache__/ldap.cpython-39.pyc differ diff --git a/lookup_plugins/ldap.py b/lookup_plugins/ldap.py new file mode 100644 index 0000000..b085b89 --- /dev/null +++ b/lookup_plugins/ldap.py @@ -0,0 +1,206 @@ +""" +To use this lookup plugin, you need to pass ldap: +ssh -L 1636:172.16.10.1:636 172.16.10.1 +""" + +import ipaddress + +from ansible.errors import AnsibleError, AnsibleParserError +from ansible.plugins.lookup import LookupBase +from ansible.utils.display import Display + +try: + import ldap +except ImportError: + raise AnsibleError("You need to install python3-ldap") + +display = Display() + +def decode_object(object): + return {attribute: [value.decode('utf-8') for value in object[attribute]] for attribute in object} + +class LookupModule(LookupBase): + + def __init__(self, **kwargs): + self.base = ldap.initialize('ldaps://localhost:1636/') + self.base.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW) + self.base.set_option(ldap.OPT_X_TLS_NEWCTX, 0) + self.base_dn = 'dc=ynerant,dc=fr' + + def query(self, base, scope, filter='(objectClass=*)', attr=None): + """ + Make a LDAP query + query('ldap', 'query', BASE, SCOPE[, FILTER[, ATTR]]) + BASE: base dn + SCOPE: 'base', 'one' or 'sub' + FILTER: ldap filter (optional) + ATTR: list of attributes (optional) + """ + scope = { 'base': ldap.SCOPE_BASE, 'one': ldap.SCOPE_ONELEVEL, 'sub': ldap.SCOPE_SUBTREE }[scope] + query_id = self.base.search(f"{base}", scope, filter, attr) + result = self.base.result(query_id)[1] + result = { dn: decode_object(entry) for dn, entry in result } + return result + + def ip(self, host, vlan): + """ + Retrieve IP addresses of an interface of a device + query('ldap', 'ip', HOST, VLAN) + """ + if isinstance(vlan, int): + network_query_id = self.base.search(f"ou=networks,{self.base_dn}", ldap.SCOPE_ONELEVEL, f"description={vlan}") + network_result = self.base.result(network_query_id) + vlan = network_result[1][0][1]['cn'][0].decode('utf-8') + if vlan == 'srv': + query_id = self.base.search(f"cn={host}.ynerant.fr,cn={host},ou=hosts,{self.base_dn}", ldap.SCOPE_BASE) + else: + query_id = self.base.search(f"cn={host}.{vlan}.ynerant.fr,cn={host},ou=hosts,{self.base_dn}", ldap.SCOPE_BASE) + result = self.base.result(query_id) + result = result[1][0][1] + result = [res.decode('utf-8') for res in result['ipHostNumber']] + return result + + def all_ip(self, host): + """ + Retrieve all IP addresses of a device + query('ldap', 'all_ip', HOST) + """ + interfaces_query_id = self.base.search(f"cn={host},ou=hosts,{self.base_dn}", ldap.SCOPE_ONELEVEL) + interfaces_result = self.base.result(interfaces_query_id) + result = [] + for dn, interface in interfaces_result[1]: + for ip in interface['ipHostNumber']: + result.append(ip.decode('utf-8')) + return result + + def cn(self, host, vlan): + """ + Retrieve aliases of an interface of a device + query('ldap', 'cn', HOST, VLAN) + """ + if isinstance(vlan, int): + network_query_id = self.base.search(f"ou=networks,{self.base_dn}", ldap.SCOPE_ONELEVEL, f"description={vlan}") + network_result = self.base.result(network_query_id) + vlan = network_result[1][0][1]['cn'][0].decode('utf-8') + if vlan == 'srv': + query_id = self.base.search(f"cn={host}.ynerant.fr,cn={host},ou=hosts,{self.base_dn}", ldap.SCOPE_BASE) + else: + query_id = self.base.search(f"cn={host}.{vlan}.ynerant.fr,cn={host},ou=hosts,{self.base_dn}", ldap.SCOPE_BASE) + result = self.base.result(query_id) + result = result[1][0][1] + result = [res.decode('utf-8') for res in result['cn']] + return result + + def all_cn(self, host): + """ + Retrieve all aliases addresses of a device + query('ldap', 'all_cn', HOST) + """ + interfaces_query_id = self.base.search(f"cn={host},ou=hosts,{self.base_dn}", ldap.SCOPE_ONELEVEL) + interfaces_result = self.base.result(interfaces_query_id) + result = [] + for dn, interface in interfaces_result[1]: + for cn in interface['cn']: + result.append(cn.decode('utf-8')) + return result + + def ssh_keys(self, host): + """ + Retrieve SSH keys of a host + query('ldap', 'ssh_keys', HOST) + """ + host_query_id = self.base.search(f"cn={host},ou=hosts,{self.base_dn}", ldap.SCOPE_BASE) + host_result = self.base.result(host_query_id)[1][0][1] + result = [] + if 'description' not in host_result: + return result + for description in host_result['description']: + description = description.decode('utf-8') + key, value = description.split(':', 1) + if key in {'ecdsa-sha2-nistp256', 'ssh-ed25519', 'ssh-dss', 'ssh-rsa'}: + result.append(f'{key} {value}') + return result + + def subnet_ipv4(self, subnet): + """ + Retrieve used IP addresses on a subnet + query('ldap', 'subnet_ipv4', SUBNET) + """ + network_query_id = self.base.search(f"cn={subnet},ou=networks,{self.base_dn}", ldap.SCOPE_BASE) + network_result = self.base.result(network_query_id) + network = network_result[1][0][1] + network, hostmask = network['ipNetworkNumber'][0].decode('utf-8'), network['ipNetmaskNumber'][0].decode('utf-8') + subnet = ipaddress.IPv4Network(f"{network}/{hostmask}") + query_id = self.base.search(f"ou=hosts,{self.base_dn}", ldap.SCOPE_SUBTREE, "objectClass=ipHost") + result = self.base.result(query_id) + result = [ip.decode('utf-8') for dn, entry in result[1] for ip in entry['ipHostNumber'] if ipaddress.ip_address(ip.decode('utf-8')) in subnet] + return result + + def run(self, terms, variables=None, **kwargs): + if terms[0] == 'query': + result = self.query(*terms[1:]) + elif terms[0] == 'ip': + result = self.ip(*terms[1:]) + elif terms[0] == 'all_ip': + result = self.all_ip(*terms[1:]) + elif terms[0] == 'cn': + result = self.cn(*terms[1:]) + elif terms[0] == 'all_cn': + result = self.all_cn(*terms[1:]) + elif terms[0] == 'subnet_ipv4': + result = self.subnet_ipv4(*terms[1:]) + elif terms[0] == 'ssh_keys': + result = self.ssh_keys(*terms[1:]) + elif terms[0] == 'group': + query_id = self.base.search(f"ou=group,{self.base_dn}", ldap.SCOPE_SUBTREE, "objectClass=posixGroup") + result = self.base.result(query_id) + result = result[1] + # query interface attribute + # query('ldap', 'hosts', HOST, VLAN, ATTR) + # HOST: device name + # VLAN: vlan name + # ATTR: attribute + elif terms[0] == 'hosts': + host = terms[1] + vlan = terms[2] + attr = terms[3] + if isinstance(vlan, int): + network_query_id = self.base.search(f"ou=networks,{self.base_dn}", ldap.SCOPE_ONELEVEL, f"description={vlan}") + network_result = self.base.result(network_query_id) + vlan = network_result[1][0][1]['cn'][0].decode('utf-8') + if vlan == 'srv': + query_id = self.base.search(f"cn={host}.ynerant.fr,cn={host},ou=hosts,{self.base_dn}", ldap.SCOPE_BASE) + else: + query_id = self.base.search(f"cn={host}.{vlan}.ynerant.fr,cn={host},ou=hosts,{self.base_dn}", ldap.SCOPE_BASE) + result = self.base.result(query_id) + result = result[1][0][1] + result = [res.decode('utf-8') for res in result[attr]] + elif terms[0] == 'network': + network = terms[1] + query_id = self.base.search(f"cn={network},ou=networks,{self.base_dn}", ldap.SCOPE_BASE, "objectClass=ipNetwork") + result = self.base.result(query_id) + result = result[1][0][1] + return str(ipaddress.ip_network('{}/{}'.format(result['ipNetworkNumber'][0].decode('utf-8'), result['ipNetmaskNumber'][0].decode('utf-8')))) + elif terms[0] == 'zones': + query_id = self.base.search(f"ou=networks,{self.base_dn}", ldap.SCOPE_ONELEVEL, "objectClass=ipNetwork") + result = self.base.result(query_id) + res = [] + for _, network in result[1]: + network = network['cn'][0].decode('utf-8') + if network == 'srv': + res.append('ynerant.fr') + else: + res.append(f"{network}.ynerant.fr") + result = res + elif terms[0] == 'vlanid': + network = terms[1] + query_id = self.base.search(f"cn={network},ou=networks,{self.base_dn}", ldap.SCOPE_BASE, "objectClass=ipNetwork") + result = self.base.result(query_id) + result = result[1][0][1] + return int(result['description'][0]) + elif terms[0] == 'role': + role = terms[1] + query_id = self.base.search(f"ou=hosts,{self.base_dn}", ldap.SCOPE_ONELEVEL, f"description=role:{role}") + result = self.base.result(query_id) + result = [cn.decode('utf-8') for res in result[1] for cn in res[1]['cn']] + return result diff --git a/plays/base.yml b/plays/base.yml index 414739d..a688a9d 100755 --- a/plays/base.yml +++ b/plays/base.yml @@ -2,6 +2,7 @@ --- - import_playbook: root.yml +- import_playbook: network_interfaces.yml - import_playbook: apt.yml - import_playbook: ntp.yml - import_playbook: ldap-client.yml diff --git a/plays/network_interfaces.yml b/plays/network_interfaces.yml new file mode 100755 index 0000000..c6d7fe0 --- /dev/null +++ b/plays/network_interfaces.yml @@ -0,0 +1,7 @@ +#!/usr/bin/env ansible-playbook +--- +- hosts: vm,!routeur + vars: + network_interfaces: "{{ glob_network_interfaces | default({}) | combine(loc_network_interfaces | default({})) }}" + roles: + - network-interfaces diff --git a/roles/network-interfaces/tasks/main.yml b/roles/network-interfaces/tasks/main.yml new file mode 100644 index 0000000..ec28213 --- /dev/null +++ b/roles/network-interfaces/tasks/main.yml @@ -0,0 +1,28 @@ +--- +- name: Install vlan support + apt: + update_cache: true + name: vlan + state: present + register: apt_result + retries: 3 + until: apt_result is succeeded + +- name: Deploy default interfaces config + template: + src: network/interfaces.j2 + dest: /etc/network/interfaces + mode: 0644 + +- name: Remove cloud-init interface configuration + file: + path: /etc/network/interfaces.d/50-cloud-init + state: absent + +- name: Deploy interfaces config + template: + src: "network/interfaces.d/ifalias.j2" + dest: "/etc/network/interfaces.d/{{ '%02d' | format(item.id) }}-{{ item.name | replace('_', '-') }}" + mode: 0644 + when: item.name in interfaces + loop: "{{ network_interfaces.vlan }}" diff --git a/roles/network-interfaces/templates/network/interfaces.d/ifalias.j2 b/roles/network-interfaces/templates/network/interfaces.d/ifalias.j2 new file mode 100644 index 0000000..01ef107 --- /dev/null +++ b/roles/network-interfaces/templates/network/interfaces.d/ifalias.j2 @@ -0,0 +1,55 @@ +{{ ansible_header | comment }} + +{% set vlan_name = (item.name | replace('_', '-')) %} +{% set subnet_network = (query('ldap', 'network', vlan_name) | ipaddr('network')) %} +{% set subnet_netmask = (query('ldap', 'network', vlan_name) | ipaddr('netmask')) %} +{% set ips = query('ldap', 'ip', ansible_hostname, vlan_name) %} +{% if (ips | ipv4 | length) > 0 %} +auto {{ interfaces[item.name] }} +iface {{ interfaces[item.name] }} inet static +{% for ip in (ips | ipv4) %} + address {{ ip }} +{% endfor %} + network {{ subnet_network }} + netmask {{ subnet_netmask }} +{% if item.gateway is defined %} + gateway {{ item.gateway }} +{% endif %} +{% if item.metric is defined %} + metric {{ item.metric }} +{% endif %} +{% if item.dns is defined %} + dns-nameservers {{ item.dns }} +{% endif %} +{% if vlan_name == 'srv' %} + dns-search ynerant.fr +{% else %} + dns-search {{ vlan_nameĀ }}.ynerant.fr +{% endif %} + up /sbin/ip link set $IFACE alias {{ vlan_name }} +{% if ansible_local.interfaces.sup_if_4 is defined %} +{% if interfaces[item.name] in ansible_local.interfaces.sup_if_4 %} +{% for line in ansible_local.interfaces.sup_if_4[interfaces[item.name]] %} + {{ line }} +{% endfor %} +{% endif %} +{% endif %} +{% endif %} + +{% if (ips | ipv6 | length) > 0 %} +iface {{ interfaces[item.name] }} inet6 static +{% for ip in (ips | ipv6) %} + address {{ ip }}/64 +{% endfor %} +{% if item.gateway_v6 is defined %} + gateway {{ item.gateway_v6 }} +{% endif %} + accept_ra 0 +{% if ansible_local.interfaces.sup_if_6 is defined %} +{% if interfaces[item.name] in ansible_local.interfaces.sup_if_6 %} +{% for line in ansible_local.interfaces.sup_if_6[interfaces[item.name]] %} + {{ line }} +{% endfor %} +{% endif %} +{% endif %} +{% endif %} diff --git a/roles/network-interfaces/templates/network/interfaces.j2 b/roles/network-interfaces/templates/network/interfaces.j2 new file mode 100644 index 0000000..0c33996 --- /dev/null +++ b/roles/network-interfaces/templates/network/interfaces.j2 @@ -0,0 +1,10 @@ +{{ ansible_header | comment }} + +# 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 diff --git a/roles/qemu-guest-agent/tasks/main.yml b/roles/qemu-guest-agent/tasks/main.yml index 72a322a..3c8f358 100644 --- a/roles/qemu-guest-agent/tasks/main.yml +++ b/roles/qemu-guest-agent/tasks/main.yml @@ -8,3 +8,12 @@ register: apt_result retries: 3 until: apt_result is succeeded + +- name: Remove cloud-init + apt: + name: cloud-init + state: absent + purge: true + register: apt_result + retries: 3 + until: apt_result is succeeded