NodeRings Docs

Cloud-Init Templates

User, vendor, and network data synced to the agent for VM provisioning

Cloud-init templates define the user-data, vendor-data, and network-data your operator injects when cloning a VM. Templates support Jinja2 syntax. When a VM is created, NodeRings compiles each field and substitutes values from the order, IPAM static assignment, and the customer's SSH keys or password.

NodeRings assigns static IPs only through IPAM. Configure fixed addresses, gateways, and DNS in your templates. Not DHCP.

When you save a template, NodeRings syncs the compiled files to your linked agents so the operator can use them at provision time.

Manage templates at /infrastructure/cloud-init.


Seed templates

NodeRings ships read-only seed templates for common guest OS families (for example Ubuntu/Debian, Fedora, Rocky Linux). They are visible to every organization but cannot be edited, deleted, or linked directly to a VM template.

To use one:

Cloud-Init Templates page

Step 1 of 2

  1. Open Cloud-Init TemplatesNew and pick the seed that matches your guest OS.
  2. NodeRings loads a draft with user_data, vendor_data, and network_data.
  3. Adjust storage or network matching for your environment, then save each file to create your own template.

Your copy is a normal organization template you link when registering VM templates.


Template fields

FieldTypical content
user-dataUsers, SSH keys, packages, hostname
vendor-dataVendor-specific metadata
network-dataInterfaces, static IPs, routes, DNS

Provided variables

NodeRings fills these Jinja variables when your operator compiles a template at VM create. Use {% if … is defined %} guards only where a field may be absent from the order. Every provisioned VM receives both IPv4 and IPv6 from IPAM.

vm: identity and access

VariableTypeDescription
{{ vm.name }}stringProxmox VM name on the hypervisor (for example NR-{uuid}). Always set.
{{ vm.hostname }}stringHostname from the VM order. Set when the customer supplies a hostname.
{{ vm.ssh_keys }}listSSH public keys from the order. Set when the customer supplies SSH keys.
{{ vm.hashed_passwd }}stringSHA-512 crypt hash ($6$…) of the root password from the order. Set when the customer chooses password authentication. Use with cloud-init hashed_passwd — never plaintext.

Password auth: NodeRings hashes the create-form password in the backend before cloud-init compile. Templates must use vm.hashed_passwd with cloud-init hashed_passwd only — do not use plaintext password fields.

vm_network: IPAM static assignment

Populated from NodeRings IPAM when the VM is provisioned. Every customer VM receives a managed static address. Use these variables in network-data.

VariableTypeDescription
{{ vm_network.ipv4_address }}stringAssigned IPv4 address.
{{ vm_network.ipv4_prefix }}stringIPv4 prefix length (for example 24).
{{ vm_network.ipv6_address }}stringAssigned IPv6 address.
{{ vm_network.ipv6_prefix }}stringIPv6 prefix length.
{{ vm_network.gateway }}stringDefault gateway.
{{ vm_network.dns_servers }}listDNS resolvers. Iterate with {% for dns in vm_network.dns_servers %}.
{{ vm_network.mac_address }}stringMAC address NodeRings allocated for the VM's primary NIC (lowercase). Use it to match: macaddress: in network-data so the interface is found regardless of its name inside the guest.

Organisation variables

Provider templates can reference organisation-scoped configuration stored under Configuration → Environment Variables and Configuration → Secrets. NodeRings compiles these references at VM create time in the backend and writes the final YAML to a per-VM secret: the operator receives rendered cloud-config, not raw Jinja with env.* / secret.*.

VariableSourceExample
{{ env.<key> }}Provider org environment variable{{ env.app_env }}
{{ secret.<key> }}Provider org secret (Vault){{ secret.api_token }}

Bracket syntax is also supported: {{ env['MY_KEY'] }}, {{ secret["API_TOKEN"] }}.

Reserved key names: do not create env vars or secrets whose keys collide with built-in namespaces (vm, vm_network, hashed_passwd, password, env, secret, and so on). See Environment variables and Secrets.

Create-time only: changing an env var or rotating a secret does not re-provision running VMs. New values apply to VMs created after the change. See the operations runbook.

Example user-data snippet:

#cloud-config
write_files:
  - path: /etc/nrings/app.env
    content: |
      APP_ENV={{ env.app_env | tojson }}
      API_TOKEN={{ secret.api_token | tojson }}

When a template's user-data contains env.* or secret.* references, NodeRings pre-renders it per VM. Templates with built-ins only (vm.*, vm_network.*) continue to compile in the operator unchanged.

Manage org configuration at /configuration/env-vars and /configuration/secrets.


Example template

user-data and network-data for static IP provisioning. Seed templates follow the same structure; network-data may differ by guest OS (netplan, NetworkManager, and so on).

#cloud-config
hostname: {{ vm.hostname | default(vm.name) }}

ssh_deletekeys: false
ssh_genkeytypes: [rsa, ecdsa, ed25519]
disable_root: false
{% if vm.hashed_passwd is defined and vm.hashed_passwd %}
ssh_pwauth: true
write_files:
  - path: /etc/ssh/sshd_config.d/99-nrings-root-password.conf
    permissions: "0644"
    content: |
      PermitRootLogin yes
      PasswordAuthentication yes
runcmd:
  - rm -f /etc/ssh/sshd_config.d/50-cloud-init.conf /etc/ssh/sshd_config.d/60-cloudimg-settings.conf
  - sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
  - systemctl try-reload-or-restart sshd 2>/dev/null || systemctl try-reload-or-restart ssh 2>/dev/null || true
{% endif %}

{% if vm.hashed_passwd is defined and vm.hashed_passwd or (vm.ssh_keys is defined and vm.ssh_keys) %}
users:
  - name: root
{% if vm.hashed_passwd is defined and vm.hashed_passwd %}
    lock_passwd: false
    hashed_passwd: {{ vm.hashed_passwd | tojson }}
{% else %}
    lock_passwd: true
{% endif %}
{% if vm.ssh_keys is defined and vm.ssh_keys %}
    ssh_authorized_keys:
{% for key in vm.ssh_keys %}
      - {{ key }}
{% endfor %}
{% endif %}
{% endif %}
#network-config
version: 2
ethernets:
  primary:
{% if vm_network.mac_address is defined and vm_network.mac_address %}
    match:
      macaddress: {{ vm_network.mac_address }}
{% else %}
    match:
      name: "en*"
{% endif %}
{% if vm_network.ipv4_address is defined and vm_network.ipv4_address or (vm_network.ipv6_address is defined and vm_network.ipv6_address) %}
    dhcp4: false
    dhcp6: false
    addresses:
{% if vm_network.ipv4_address is defined and vm_network.ipv4_address and vm_network.ipv4_prefix is defined and vm_network.ipv4_prefix %}
      - {{ vm_network.ipv4_address }}/{{ vm_network.ipv4_prefix }}
{% endif %}
{% if vm_network.ipv6_address is defined and vm_network.ipv6_address and vm_network.ipv6_prefix is defined and vm_network.ipv6_prefix %}
      - {{ vm_network.ipv6_address }}/{{ vm_network.ipv6_prefix }}
{% endif %}
{% if vm_network.dns_servers is defined and vm_network.dns_servers %}
    nameservers:
      addresses:
{% for dns in vm_network.dns_servers %}
        - {{ dns }}
{% endfor %}
{% endif %}
{% if (vm_network.ipv4_gateway is defined and vm_network.ipv4_gateway) or (vm_network.gateway is defined and vm_network.gateway) or (vm_network.ipv6_gateway is defined and vm_network.ipv6_gateway) %}
    routes:
{% if (vm_network.ipv4_gateway is defined and vm_network.ipv4_gateway) or (vm_network.gateway is defined and vm_network.gateway) %}
      - to: default
        via: {{ vm_network.ipv4_gateway | default(vm_network.gateway) }}
{% endif %}
{% if vm_network.ipv6_gateway is defined and vm_network.ipv6_gateway %}
      - to: default
        via: {{ vm_network.ipv6_gateway }}
{% endif %}
{% endif %}
{% else %}
    dhcp4: true
    dhcp6: true
{% endif %}