Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions infrastructure/network/vyos/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Rendered config contains secrets - never commit
.rendered-gateway.conf

# Backups are local artifacts
backups/

# Test artifacts
tests/.vyos-test-key
tests/.vyos-test-key.pub
tests/config.boot
175 changes: 114 additions & 61 deletions infrastructure/network/vyos/ansible/playbooks/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,41 @@
# VyOS Configuration Deployment Playbook
# Applies gateway.conf to VyOS with rollback protection
# Applies gateway.conf to VyOS router using native VyOS `load` command
#
# Usage:
# ansible-playbook deploy.yml -i ../inventory/hosts.yml
# ansible-playbook deploy.yml -i ../inventory/hosts.yml -e "ssh_public_key_file=~/.ssh/id_rsa.pub"
#
# Environment Variables:
# VYOS_HOST - VyOS hostname or IP (default: gateway.lab.gilman.io)
# VYOS_SSH_KEY - Path to SSH private key for connection (default: ~/.ssh/id_rsa)
# VYOS_HOST - VyOS hostname or IP (default: 10.0.0.2)
# VYOS_SSH_KEY - Path to SSH private key for connection (default: ~/.ssh/vyos-gateway)
#
# Extra Variables:
# ssh_public_key_file - Path to SSH public key to configure on VyOS (optional)
# This playbook:
# 1. Backs up current router configuration
# 2. Decrypts SSH credentials from SOPS (ssh.sops.yaml)
# 3. Injects credentials into gateway.conf
# 4. Copies config to router and applies via `load` command
# 5. Verifies connectivity before saving
#
# Note: Uses VyOS native `load` command instead of vyos_config module because
# the Ansible module doesn't properly parse bracket format with comments.
#
# Safety: Config is committed but not saved until connectivity is verified.
# If connectivity fails, reboot the router to restore the previous saved config.
---
- name: Deploy VyOS Configuration
hosts: vyos_gateway
gather_facts: false

vars:
config_file: "{{ playbook_dir }}/../../configs/gateway.conf"
sops_file: "{{ playbook_dir }}/../../ssh.sops.yaml"
backup_dir: "{{ playbook_dir }}/../../backups"
commit_confirm_timeout: 5 # Minutes before auto-rollback
ssh_public_key_file: "" # Set via -e to configure SSH key
rendered_config_file: "{{ playbook_dir }}/../../.rendered-gateway.conf"
remote_config_path: /tmp/gateway.conf

tasks:
# =========================================================================
# Backup Current Configuration
# =========================================================================
- name: Ensure backup directory exists
delegate_to: localhost
ansible.builtin.file:
Expand All @@ -40,75 +53,115 @@
delegate_to: localhost
ansible.builtin.copy:
content: "{{ current_config.stdout[0] }}"
dest: "{{ backup_dir }}/gateway-{{ ansible_date_time.iso8601_basic_short }}.conf"
dest: "{{ backup_dir }}/gateway-{{ lookup('pipe', 'date +%Y%m%dT%H%M%S') }}.conf"
mode: "0644"
vars:
ansible_date_time: "{{ lookup('pipe', 'date +%Y%m%dT%H%M%S') }}"

- name: Read new configuration
# =========================================================================
# Decrypt SOPS Credentials
# =========================================================================
- name: Decrypt SSH credentials from SOPS
delegate_to: localhost
ansible.builtin.command:
cmd: sops -d "{{ sops_file }}"
register: sops_output
changed_when: false
no_log: true

- name: Parse SSH credentials
delegate_to: localhost
ansible.builtin.set_fact:
ssh_credentials: "{{ sops_output.stdout | from_yaml }}"
no_log: true

- name: Extract SSH key components
delegate_to: localhost
ansible.builtin.set_fact:
ssh_key_type: "{{ ssh_credentials.public_key.split()[0] }}"
ssh_key_data: "{{ ssh_credentials.public_key.split()[1] }}"
ssh_password: "{{ ssh_credentials.password }}"
no_log: true

# =========================================================================
# Render Configuration with Credentials
# =========================================================================
- name: Read gateway configuration
delegate_to: localhost
ansible.builtin.slurp:
src: "{{ config_file }}"
register: new_config_raw
register: config_content

- name: Load configuration with commit-confirm
vyos.vyos.vyos_config:
src: "{{ config_file }}"
save: false # Don't save yet - wait for confirm
backup: true
register: config_result
- name: Render configuration with injected credentials
delegate_to: localhost
vars:
# NOTE: Indentation must exactly match gateway.conf (8/12/16/20 spaces)
# YAML strips leading indent based on first line, so we start at column 0
# and include literal spaces for each line
auth_placeholder: " user vyos {\n authentication {\n /* SSH public keys added by Ansible deploy.yml */\n /* Password set manually for console access */\n }\n }"
auth_with_credentials: " user vyos {\n authentication {\n plaintext-password \"{{ ssh_password }}\"\n public-keys vyos-gateway {\n key {{ ssh_key_data }}\n type {{ ssh_key_type }}\n }\n }\n }"
ansible.builtin.copy:
content: "{{ (config_content.content | b64decode) | replace(auth_placeholder, auth_with_credentials) }}"
dest: "{{ rendered_config_file }}"
mode: "0600"
no_log: true

# =========================================================================
# Copy Configuration to Router
# =========================================================================
- name: Copy configuration to router
delegate_to: localhost
ansible.builtin.shell:
cmd: >
scp -i {{ ansible_ssh_private_key_file }}
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
"{{ rendered_config_file }}"
{{ ansible_user }}@{{ ansible_host }}:{{ remote_config_path }}
no_log: true

# =========================================================================
# Apply Configuration via VyOS load command
# =========================================================================
- name: Load, commit, and save configuration
vyos.vyos.vyos_command:
commands:
- configure
- "load {{ remote_config_path }}"
- commit
- save
- exit
register: load_result
vars:
ansible_command_timeout: 120

- name: Display configuration changes
- name: Display load result
ansible.builtin.debug:
msg: "{{ config_result }}"
when: config_result.changed
msg: "Configuration loaded, committed, and saved"

- name: Wait for connectivity test
- name: Verify connectivity after config change
ansible.builtin.wait_for_connection:
delay: 5
timeout: 60
when: config_result.changed

- name: Confirm commit (prevents auto-rollback)
vyos.vyos.vyos_command:
commands:
- confirm
when: config_result.changed

- name: Save configuration
vyos.vyos.vyos_command:
commands:
- save
when: config_result.changed
# =========================================================================
# Cleanup
# =========================================================================
- name: Remove configuration file from router
delegate_to: localhost
ansible.builtin.shell:
cmd: >
ssh -i {{ ansible_ssh_private_key_file }}
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
{{ ansible_user }}@{{ ansible_host }}
"rm -f {{ remote_config_path }}"
ignore_errors: true

# SSH Key Management (separate from main config)
- name: Read SSH public key
- name: Remove rendered configuration file locally
delegate_to: localhost
ansible.builtin.slurp:
src: "{{ ssh_public_key_file }}"
register: ssh_key_content
when: ssh_public_key_file | length > 0
ansible.builtin.file:
path: "{{ rendered_config_file }}"
state: absent

- name: Parse SSH key components
ansible.builtin.set_fact:
ssh_key_type: "{{ (ssh_key_content.content | b64decode).split()[0] }}"
ssh_key_data: "{{ (ssh_key_content.content | b64decode).split()[1] }}"
when: ssh_public_key_file | length > 0

- name: Configure SSH public key
vyos.vyos.vyos_config:
lines:
- "set system login user vyos authentication public-keys admin type {{ ssh_key_type }}"
- "set system login user vyos authentication public-keys admin key {{ ssh_key_data }}"
save: true
when: ssh_public_key_file | length > 0
register: ssh_key_result

- name: Configuration deployment complete
- name: Deployment complete
ansible.builtin.debug:
msg: >
VyOS configuration deployed successfully.
Config changes: {{ 'Yes' if config_result.changed else 'No' }}
SSH key configured: {{ 'Yes' if (ssh_key_result is defined and ssh_key_result.changed) else 'No' }}
msg: "VyOS configuration deployed successfully via native load command"
3 changes: 3 additions & 0 deletions infrastructure/network/vyos/configs/gateway.conf
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ interfaces {
description "LAB_STORAGE - Storage Replication"
}
}
ethernet eth2 {
description "LAN - UM760 Platform Anchor Node"
}
}
nat {
source {
Expand Down
9 changes: 5 additions & 4 deletions infrastructure/network/vyos/tests/render-config-boot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ fi
# Start with the base gateway.conf
cp "${CONFIG_FILE}" "${OUTPUT_FILE}"

# Remap interfaces for test environment (Containerlab reserves eth0 for management)
# Production: eth0 (WAN), eth1 (Trunk)
# Test: eth2 (WAN), eth3 (Trunk)
sed -i.bak -e 's/eth0/eth2/g' -e 's/eth1/eth3/g' "${OUTPUT_FILE}"
# Remap interfaces for test environment (Containerlab reserves eth0/eth1 for management)
# Production: eth0 (WAN), eth1 (Trunk), eth2+ (LAN)
# Test: eth2 (WAN), eth3 (Trunk), eth4+ (LAN)
# Order matters: remap higher interfaces first to avoid double-replacement
sed -i.bak -e 's/eth2/eth4/g' -e 's/eth0/eth2/g' -e 's/eth1/eth3/g' "${OUTPUT_FILE}"
rm -f "${OUTPUT_FILE}.bak"

# Adjust WAN IP for test environment (192.168.0.0/24 instead of 10.0.0.0/30)
Expand Down