ansible-collections / hetzner.hcloud

A collection to manage resources on Hetzner Cloud
https://galaxy.ansible.com/ui/repo/published/hetzner/hcloud
GNU General Public License v3.0
100 stars 35 forks source link

Add Support for Assigning Servers to Specific Subnets on Creation #526

Open germebl opened 1 week ago

germebl commented 1 week ago
SUMMARY

It would be beneficial to have the ability to assign a server to a specific subnet during its creation using the Ansible hcloud collection. Currently, servers can be added to a private network, but the subnet is automatically chosen. This feature would allow for more precise network configurations.

ISSUE TYPE
COMPONENT NAME

hcloud_server module

ADDITIONAL INFORMATION

The feature would allow users to specify a subnet within a private network when creating a server. This capability is needed to better manage network resources and ensure that servers are placed in the correct subnet according to predefined network architecture.

Example usage in a playbook:

    - name: Create a new server
      hcloud_server:
        api_token: "{{ hetzner_api_token }}"
        name: "{{ hostname }}"
        server_type: "{{ server_type }}"
        image: "debian-12"
        location: "{{ location }}"
        enable_ipv4: false
        enable_ipv6: false
        private_networks:
          - nat-network
            subnet:
              - 10.1.200.0/24

If there is already a way to achieve this, please hint me and close the request.

jooola commented 1 week ago

The API does not yet support this, in the meantime here is a workaround: https://github.com/ansible-collections/hetzner.hcloud/blob/ecaeac117563cf0c070e442938feb68976374381/examples/server-assign-to-subnetwork.yml

germebl commented 1 week ago

I wrote myself another workaround, so i just get the real next free ip to simulate the DHCP assignment:

following need to be passed to it: {{ network_name }} e.g. nat-network {{ hetzner_api_token }} {{ subnet }} e.g. 10.1.100.0/24

get_ip.yaml

---
- name: Retrieve the Hetzner network information
  hosts: localhost
  tasks:
    - name: Get all networks from Hetzner Cloud
      uri:
        url: https://api.hetzner.cloud/v1/networks
        method: GET
        headers:
          Authorization: "Bearer {{ hetzner_api_token }}"
      register: networks_response

    - name: Find the network with the specified name
      set_fact:
        target_network: "{{ networks_response.json.networks | selectattr('name', 'equalto', network_name) | list | first }}"

    - name: Find the subnet in the specified network
      set_fact:
        target_subnet: "{{ target_network.subnets | selectattr('ip_range', 'equalto', subnet) | list | first }}"

    - name: Get all servers from Hetzner Cloud
      uri:
        url: https://api.hetzner.cloud/v1/servers
        method: GET
        headers:
          Authorization: "Bearer {{ hetzner_api_token }}"
      register: servers_response

    - name: Extract server IPs in the target subnet
      set_fact:
        server_ips_in_subnet: >-
          {{
            servers_response.json.servers |
            map(attribute='private_net') |
            sum(start=[]) |
            selectattr('network', 'equalto', target_network.id) |
            map(attribute='ip') |
            select('match', '^' + target_subnet.ip_range.split('/')[0] | regex_replace('\.[0-9]+$', '')) |
            list
          }}

    - name: Extract server alias IPs in the target subnet
      set_fact:
        main_ips_in_subnet: >-
          {{
            servers_response.json.servers |
            map(attribute='private_net') |
            sum(start=[]) |
            selectattr('network', 'equalto', target_network.id) |
            map(attribute='ip') |
            select('match', '^' + target_subnet.ip_range.split('/')[0] | regex_replace('\.[0-9]+$', '')) |
            list
          }}

    - name: Extract alias IPs in the target subnet
      set_fact:
        alias_ips_in_subnet: >-
          {{
            servers_response.json.servers |
            map(attribute='private_net') |
            sum(start=[]) |
            selectattr('network', 'equalto', target_network.id) |
            map(attribute='alias_ips') |
            sum(start=[]) |
            select('match', '^' + target_subnet.ip_range.split('/')[0] | regex_replace('\.[0-9]+$', '')) |
            list
          }}

    - name: Combine main and alias IPs in the target subnet
      set_fact:
        server_ips_in_subnet: "{{ main_ips_in_subnet + alias_ips_in_subnet }}"

    - name: Calculate all IPs in the subnet range using Python script
      command: "python3 ip_range.py {{ target_subnet.ip_range }}"
      register: all_ips_in_subnet_output

    - name: Set fact for all IPs in subnet range
      set_fact:
        all_ips_in_subnet: "{{ all_ips_in_subnet_output.stdout.split(',') }}"

    - name: Remove used IPs from the list of all IPs in the subnet
      set_fact:
        available_ips_in_subnet: >-
          {{
            (all_ips_in_subnet | difference(server_ips_in_subnet))
            | map('split', '.') | map('map', 'int') | sort | map('join', '.') | list
          }}

    - name: Find the smallest available IP in the subnet
      set_fact:
        smallest_available_ip: >-
          {{
            (available_ips_in_subnet | first)
          }}

ip_range.py

# ip_range.py
import sys
from ipaddress import ip_network

network = ip_network(sys.argv[1])
ips = [str(ip) for ip in network.hosts()]
print(",".join(ips))

Really dirty, but works.