diff --git a/ansible/host_vars/netbox.vntx.net.yml b/ansible/host_vars/netbox.vntx.net.yml index 72f4361..ed8c98f 100644 --- a/ansible/host_vars/netbox.vntx.net.yml +++ b/ansible/host_vars/netbox.vntx.net.yml @@ -1,13 +1,18 @@ --- -# Host-specific configuration for netbox.vntx.net - -# Ansible connection settings +# Host-specific configuration for netbox.vntx.net. +# Connection ansible_user: graham ansible_host: 204.110.191.243 ansible_python_interpreter: /usr/bin/python3 ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519 ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new' +# This host's network is configured out of band; don't let the general role +# clobber it. +network: + skip: true + +# Public-facing reverse proxy + SSH-from-LAN — mirrors the live ufw rules. firewall_allow_rules: - port: 80 proto: tcp @@ -16,25 +21,19 @@ firewall_allow_rules: proto: tcp comment: HTTPS -# Override defaults for NetBox installation -netbox_port: 8001 +# ---- netbox role overrides ---- +netbox_admins: + - name: "Graham McIntire" + email: "graham@vntx.net" + +netbox_allowed_hosts: + - netbox.vntx.net + +netbox_caddy_domain: netbox.vntx.net +netbox_login_required: true + +# Secrets — pull from a vault file alongside this one (host_vars/netbox.vntx.net/vault.yml, +# encrypted with ansible-vault). The role asserts these are set before rendering config. netbox_secret_key: "{{ vault_netbox_secret_key }}" netbox_db_password: "{{ vault_netbox_db_password }}" -netbox_admin_password: "{{ vault_netbox_admin_password }}" -netbox_admin_email: "admin@vntx.net" - -# Set allowed hosts for this installation (space-separated string) -netbox_allowed_hosts: "netbox.vntx.net localhost 127.0.0.1" - -# Email configuration for VNTX environment -netbox_email_from: "netbox@vntx.net" - -# Time zone -netbox_timezone: "America/Chicago" - -# Enable metrics for monitoring -netbox_metrics_enabled: true - -# Custom branding if desired -# netbox_branding_title: "VNTX NetBox" -# netbox_branding_url: "https://vntx.net" \ No newline at end of file +netbox_api_token_peppers: "{{ vault_netbox_api_token_peppers | default({}) }}" diff --git a/ansible/hosts b/ansible/hosts index 1d5007c..b2e3e4e 100755 --- a/ansible/hosts +++ b/ansible/hosts @@ -41,6 +41,10 @@ aprs.w5isp.com [caddy_servers] skippy.w5isp.com +netbox.vntx.net + +[netbox_servers] +netbox.vntx.net [bind9_servers] ns1.as393837.net ansible_host=204.110.191.222 diff --git a/ansible/playbook.yml b/ansible/playbook.yml index c813feb..a2edcb9 100644 --- a/ansible/playbook.yml +++ b/ansible/playbook.yml @@ -33,6 +33,15 @@ roles: - resolvers +- name: Install and configure NetBox + hosts: netbox_servers + become: true + gather_facts: true + tags: + - netbox + roles: + - netbox + - name: Install and configure Caddy hosts: caddy_servers become: true diff --git a/ansible/roles/caddy/templates/Caddyfile-netbox.vntx.net.j2 b/ansible/roles/caddy/templates/Caddyfile-netbox.vntx.net.j2 new file mode 100644 index 0000000..3c6460d --- /dev/null +++ b/ansible/roles/caddy/templates/Caddyfile-netbox.vntx.net.j2 @@ -0,0 +1,21 @@ +{{ netbox_caddy_domain | default(inventory_hostname) }} { + # Set max request body size + request_body { + max_size {{ netbox_caddy_max_body_size | default('25MB') }} + } + + # Serve static files + handle /static/* { + root * {{ netbox_install_dir | default('/opt/netbox') }}/netbox + file_server + } + + # Proxy to Gunicorn backend + handle { + reverse_proxy {{ netbox_listen_addr | default('127.0.0.1:8001') }} { + header_up X-Forwarded-Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-Proto {scheme} + } + } +} diff --git a/ansible/roles/netbox/README.md b/ansible/roles/netbox/README.md new file mode 100644 index 0000000..ba54283 --- /dev/null +++ b/ansible/roles/netbox/README.md @@ -0,0 +1,42 @@ +# netbox role + +Bare-metal NetBox install matching the live netbox.vntx.net layout: + +- Debian 13 (trixie) host +- System PostgreSQL 17 + Redis 8 +- NetBox release tarball extracted under `/opt/netbox-`, symlinked to `/opt/netbox` +- Python venv managed by NetBox's bundled `upgrade.sh` +- gunicorn bound to `127.0.0.1:8001` under systemd (`netbox.service`) +- Background worker under `netbox-rq.service` +- Reverse proxy via the repo's existing `caddy` role using the per-host + template `roles/caddy/templates/Caddyfile-netbox.vntx.net.j2` + +## Required vault variables + +The role refuses to render config without these. Put them in +`host_vars/netbox.vntx.net/vault.yml` (encrypted with ansible-vault) or any +equivalent secrets store: + +```yaml +netbox_secret_key: "<>= 50 random chars>" # generate with /opt/netbox/netbox/generate_secret_key.py +netbox_db_password: "" +netbox_api_token_peppers: # optional, only if rotating peppers + 1: "<32+ random hex chars>" +``` + +## Idempotency + +Re-running the playbook against a host already at the target version is a +no-op except for config rendering. To upgrade: + +1. Bump `netbox_version` in defaults or host_vars. +2. Run the play. The role downloads the new tarball, re-points the + `/opt/netbox` symlink, runs `upgrade.sh` (which handles venv + migrations + + collectstatic), and bounces the systemd units. + +## What the role does NOT manage + +- The data inside NetBox (sites, devices, IPs, etc.). Use the + `netbox.netbox` Ansible collection or NetBox's import APIs for that. +- Postgres performance tuning beyond defaults (shared_buffers, etc.). +- Off-host backups of the Postgres DB. Add a separate cron / restic job. diff --git a/ansible/roles/netbox/defaults/main.yml b/ansible/roles/netbox/defaults/main.yml index ada185f..c4c326d 100644 --- a/ansible/roles/netbox/defaults/main.yml +++ b/ansible/roles/netbox/defaults/main.yml @@ -1,106 +1,134 @@ --- -# Directory configuration -netbox_home: /opt/netbox +# Bare-metal NetBox install (Debian 13 + tarball + system Postgres/Redis + +# system Caddy). Mirrors the live install on netbox.vntx.net. +# Override per-host as needed; never commit secrets — keep them in +# ansible-vault or host_vars marked vault-only. -# Version configuration -netbox_version: release -netbox_docker_tag: latest -netbox_postgres_version: 15-alpine -netbox_redis_version: 7-alpine +netbox_version: "4.6.0" +netbox_release_url: "https://github.com/netbox-community/netbox/archive/v{{ netbox_version }}.tar.gz" -# Network configuration -netbox_port: 8080 -netbox_listen_address: 0.0.0.0 -netbox_domain: netbox.vntx.net +# Install layout — matches NetBox upstream docs. /opt/netbox is the extracted +# release root; /opt/netbox/netbox is the Django project; /opt/netbox/venv is +# the Python virtualenv that upgrade.sh manages. +netbox_install_dir: /opt/netbox +netbox_user: netbox +netbox_group: netbox -# Database configuration +# Listen address for gunicorn (matches the live gunicorn.py). Caddy proxies here. +netbox_listen_addr: "127.0.0.1:8001" +netbox_gunicorn_workers: 5 +netbox_gunicorn_threads: 3 +netbox_gunicorn_timeout: 120 +netbox_gunicorn_max_requests: 5000 +netbox_gunicorn_max_requests_jitter: 500 + +# PostgreSQL netbox_db_name: netbox netbox_db_user: netbox -netbox_db_password: "{{ vault_netbox_db_password | default('changeme') }}" -netbox_db_host: postgres -netbox_db_port: 5432 +netbox_db_host: localhost +netbox_db_port: "" +netbox_db_conn_max_age: 300 +# netbox_db_password: provided via host_vars/vault — no default -# Redis configuration -netbox_redis_host: redis +# Redis (matches the live install — both queues on the same local instance) +netbox_redis_host: localhost netbox_redis_port: 6379 -netbox_redis_password: "{{ vault_netbox_redis_password | default('') }}" -netbox_redis_cache_host: redis-cache -netbox_redis_cache_port: 6379 -netbox_redis_cache_password: "{{ vault_netbox_redis_cache_password | default('') }}" +netbox_redis_username: "" +netbox_redis_password: "" +netbox_redis_tasks_db: 0 +netbox_redis_caching_db: 1 +netbox_redis_ssl: false -# NetBox configuration -netbox_secret_key: "{{ vault_netbox_secret_key | default('changeme_to_a_very_long_random_string') }}" -netbox_superuser_api_token: "{{ vault_netbox_superuser_api_token | default('') }}" -netbox_allowed_hosts: "*" -netbox_cors_origin_allow_all: true +# Application config +netbox_allowed_hosts: + - "{{ inventory_hostname }}" +netbox_admins: [] # e.g. [{name: "Graham McIntire", email: "graham@vntx.net"}] netbox_debug: false -netbox_email_server: localhost -netbox_email_port: 25 -netbox_email_username: "" -netbox_email_password: "" -netbox_email_use_ssl: false -netbox_email_use_tls: false -netbox_email_from: "netbox@{{ ansible_fqdn }}" -netbox_email_timeout: 10 - -# Login configuration -netbox_login_required: false -netbox_login_timeout: 1209600 # 14 days in seconds - -# Media and upload configuration -netbox_media_root: /opt/netbox/netbox/media -netbox_reports_root: /opt/netbox/netbox/reports -netbox_scripts_root: /opt/netbox/netbox/scripts -netbox_max_upload_size: 26214400 # 25MB - -# Housekeeping -netbox_housekeeping_interval: 86400 # 24 hours - -# Metrics +netbox_login_required: true +netbox_login_persistence: false +netbox_login_form_hidden: false +netbox_logout_redirect_url: home netbox_metrics_enabled: false +netbox_time_zone: UTC +netbox_default_language: en-us +netbox_base_path: "" +netbox_session_cookie_name: sessionid +netbox_csrf_cookie_name: csrftoken +netbox_session_file_path: "" # empty -> falls back to db +netbox_release_check_url: "" # empty -> disabled +netbox_rq_default_timeout: 300 +netbox_login_timeout: "" # empty -> Django default +netbox_internal_ips: + - "127.0.0.1" + - "::1" -# Plugins +# Auth / API +netbox_allow_token_retrieval: false +netbox_remote_auth_enabled: false +netbox_remote_auth_backend: "netbox.authentication.RemoteUserBackend" +netbox_remote_auth_header: "HTTP_REMOTE_USER" +netbox_remote_auth_user_first_name: "HTTP_REMOTE_USER_FIRST_NAME" +netbox_remote_auth_user_last_name: "HTTP_REMOTE_USER_LAST_NAME" +netbox_remote_auth_user_email: "HTTP_REMOTE_USER_EMAIL" +netbox_remote_auth_auto_create_user: true +netbox_remote_auth_default_groups: [] +netbox_remote_auth_default_permissions: {} + +# CORS +netbox_cors_origin_allow_all: false +netbox_cors_origin_whitelist: [] +netbox_cors_origin_regex_whitelist: [] +netbox_exempt_view_permissions: [] +netbox_auth_password_validators: [] +netbox_logging: {} + +# Plugins (must match what's in venv via local_requirements.txt) netbox_plugins: [] netbox_plugins_config: {} +netbox_local_requirements: [] -# Admin user configuration -netbox_create_superuser: true -netbox_admin_user: admin -netbox_admin_email: "admin@{{ ansible_fqdn }}" -netbox_admin_password: "{{ vault_netbox_admin_password | default('changeme') }}" +# Email +netbox_email: + SERVER: localhost + PORT: 25 + USERNAME: "" + PASSWORD: "" + USE_SSL: false + USE_TLS: false + TIMEOUT: 10 + FROM_EMAIL: "" -# NAPALM configuration (for network device integration) -netbox_napalm_username: "" -netbox_napalm_password: "" -netbox_napalm_timeout: 30 +# Secrets — MUST come from host_vars / ansible-vault. Listed here so the +# role's contract is self-documenting; the role refuses to render config +# until they're provided. +# netbox_secret_key: +# netbox_db_password: +# netbox_api_token_peppers: { 1: "" } -# Time zone -netbox_timezone: UTC +# Caddy reverse-proxy hostname (drives the per-host Caddyfile rendered by +# the existing `caddy` role; this role just sets up the backend). +netbox_caddy_domain: "{{ inventory_hostname }}" +netbox_caddy_max_body_size: "25MB" -# Date and time formatting -netbox_date_format: "N j, Y" -netbox_short_date_format: "Y-m-d" -netbox_time_format: "g:i a" -netbox_short_time_format: "H:i:s" -netbox_datetime_format: "N j, Y g:i a" -netbox_short_datetime_format: "Y-m-d H:i" - -# Pagination -netbox_paginate_count: 50 -netbox_max_page_size: 1000 - -# GraphQL -netbox_graphql_enabled: true - -# Webhook configuration -netbox_webhooks_enabled: true -netbox_webhook_redis_queue: default - -# Cache timeout -netbox_cache_timeout: 900 # 15 minutes - -# Changelog retention -netbox_changelog_retention: 90 # days - -# Job retention -netbox_jobresult_retention: 90 # days \ No newline at end of file +# OS deps required to build/run NetBox on Debian/Ubuntu +netbox_system_packages: + - postgresql + - postgresql-client + - redis-server + - python3 + - python3-dev + - python3-venv + - python3-pip + - build-essential + - libxml2-dev + - libxslt1-dev + - libffi-dev + - libpq-dev + - libssl-dev + - libsasl2-dev + - libldap2-dev + - zlib1g-dev + - graphviz + - git + - acl # required for community.postgresql to become postgres user + - python3-psycopg2 # for ansible postgres modules diff --git a/ansible/roles/netbox/handlers/main.yml b/ansible/roles/netbox/handlers/main.yml index 4427d01..a88be1b 100644 --- a/ansible/roles/netbox/handlers/main.yml +++ b/ansible/roles/netbox/handlers/main.yml @@ -1,20 +1,17 @@ --- -- name: restart netbox - systemd: - name: netbox-docker +- name: Restart netbox + ansible.builtin.systemd: + name: netbox state: restarted + daemon_reload: true -- name: stop netbox - systemd: - name: netbox-docker - state: stopped +- name: Restart netbox-rq + ansible.builtin.systemd: + name: netbox-rq + state: restarted + daemon_reload: true -- name: start netbox - systemd: - name: netbox-docker - state: started - -- name: restart caddy - systemd: +- name: Restart caddy + ansible.builtin.systemd: name: caddy - state: restarted \ No newline at end of file + state: restarted diff --git a/ansible/roles/netbox/tasks/main.yml b/ansible/roles/netbox/tasks/main.yml index 92d8d03..10e9be5 100644 --- a/ansible/roles/netbox/tasks/main.yml +++ b/ansible/roles/netbox/tasks/main.yml @@ -1,258 +1,200 @@ --- -- name: Install prerequisites for Docker - apt: - name: - - ca-certificates - - curl - - gnupg - - lsb-release - - git - - python3-full - state: present - update_cache: true - when: ansible_os_family == "Debian" +# Bare-metal NetBox install (mirrors live netbox.vntx.net layout). Runs: +# 1. system packages +# 2. netbox user/group +# 3. PostgreSQL role + database +# 4. Redis (default config; just ensure running) +# 5. Fetch + extract release tarball into {{ netbox_install_dir }} +# 6. Run upgrade.sh (creates venv, installs deps, runs migrate + collectstatic) +# 7. Render configuration.py + gunicorn.py +# 8. Render systemd units (netbox + netbox-rq) +# 9. Render per-host Caddyfile (the caddy role's host-specific template hook) -- name: Add Docker's official GPG key - ansible.builtin.get_url: - url: https://download.docker.com/linux/debian/gpg - dest: /usr/share/keyrings/docker-archive-keyring.asc - mode: '0644' +- name: Assert required secrets are present + ansible.builtin.assert: + that: + - netbox_secret_key is defined and netbox_secret_key | length >= 50 + - netbox_db_password is defined and netbox_db_password | length > 0 + fail_msg: >- + netbox_secret_key (>= 50 chars) and netbox_db_password must be set in + host_vars / ansible-vault before this role can run. See + ansible/roles/netbox/README.md. -- name: Add Docker repository - ansible.builtin.deb822_repository: - name: docker - types: deb - uris: https://download.docker.com/linux/debian - suites: "{{ ansible_distribution_release }}" - components: stable - architectures: "{{ ansible_architecture | replace('x86_64', 'amd64') }}" - signed_by: /usr/share/keyrings/docker-archive-keyring.asc - state: present - -- name: Install Docker packages - apt: - name: - - docker-ce - - docker-ce-cli - - containerd.io - - docker-compose-plugin +- name: Install system packages + ansible.builtin.apt: + name: "{{ netbox_system_packages }}" state: present update_cache: true -- name: Ensure Docker service is started and enabled - systemd: - name: docker +- name: Ensure PostgreSQL is started and enabled + ansible.builtin.systemd: + name: postgresql state: started enabled: true - -- name: Create netbox directories - file: - path: "{{ item }}" - state: directory - mode: '0755' - loop: - - "{{ netbox_home }}/netbox-docker" - - "{{ netbox_home }}/netbox-docker/env" - - "{{ netbox_home }}/data" - - "{{ netbox_home }}/data/redis" - - "{{ netbox_home }}/data/redis-cache" - - "{{ netbox_home }}/data/media" - - "{{ netbox_home }}/data/reports" - - "{{ netbox_home }}/data/scripts" - -- name: Create postgres data directory with specific permissions - file: - path: "{{ netbox_home }}/data/postgres" - state: directory - mode: '0700' - -- name: Clone netbox-docker repository - git: - repo: https://github.com/netbox-community/netbox-docker.git - dest: "{{ netbox_home }}/netbox-docker" - version: "{{ netbox_version }}" - force: true - notify: restart netbox - -# Configuration directory creation removed - using default configuration with env vars - -- name: Deploy docker-compose override file - template: - src: docker-compose.override.yml.j2 - dest: "{{ netbox_home }}/netbox-docker/docker-compose.override.yml" - mode: '0644' - notify: restart netbox - -- name: Deploy netbox.env file - template: - src: env.j2 - dest: "{{ netbox_home }}/netbox-docker/env/netbox.env" - mode: '0600' - notify: restart netbox - -- name: Deploy postgres.env file - template: - src: postgres.env.j2 - dest: "{{ netbox_home }}/netbox-docker/env/postgres.env" - mode: '0600' - notify: restart netbox - -- name: Deploy redis.env file - template: - src: redis.env.j2 - dest: "{{ netbox_home }}/netbox-docker/env/redis.env" - mode: '0600' - notify: restart netbox - -- name: Deploy redis-cache.env file - template: - src: redis-cache.env.j2 - dest: "{{ netbox_home }}/netbox-docker/env/redis-cache.env" - mode: '0600' - notify: restart netbox - -- name: Remove custom configuration.py if exists - file: - path: "{{ netbox_home }}/netbox-docker/configuration/configuration.py" - state: absent - -- name: Pull container images - command: docker compose -f docker-compose.yml -f docker-compose.override.yml pull - args: - chdir: "{{ netbox_home }}/netbox-docker" - register: pull_result - changed_when: "'Pulling' in pull_result.stdout" - - -- name: Deploy systemd service for NetBox - template: - src: netbox-docker.service.j2 - dest: /etc/systemd/system/netbox-docker.service - owner: root - group: root - mode: '0644' - notify: restart netbox - -- name: Reload systemd daemon - systemd: - daemon_reload: true - -- name: Start and enable NetBox service - systemd: - name: netbox-docker +- name: Ensure Redis is started and enabled + ansible.builtin.systemd: + name: redis-server state: started enabled: true -- name: Wait for initial container startup - pause: - seconds: 15 +- name: Create netbox group + ansible.builtin.group: + name: "{{ netbox_group }}" + system: true + state: present -- name: Check container status - command: docker ps -a - register: container_status - changed_when: false +- name: Create netbox user + ansible.builtin.user: + name: "{{ netbox_user }}" + group: "{{ netbox_group }}" + system: true + shell: /usr/sbin/nologin + home: "{{ netbox_install_dir }}" + create_home: false + state: present -- name: Debug container status - debug: - var: container_status.stdout_lines - when: "'ANSIBLE_DEBUG' in ansible_env or ansible_verbosity > 0" +- name: Create PostgreSQL role + community.postgresql.postgresql_user: + name: "{{ netbox_db_user }}" + password: "{{ netbox_db_password }}" + state: present + become: true + become_user: postgres -- name: Wait for NetBox to be ready - uri: - url: "http://localhost:{{ netbox_port }}/api/" - method: GET - status_code: - - 200 - - 403 # API might return 403 if auth is required - validate_certs: false - timeout: 30 - register: result - until: result.status in [200, 403] - retries: 30 - delay: 10 +- name: Create NetBox database + community.postgresql.postgresql_db: + name: "{{ netbox_db_name }}" + owner: "{{ netbox_db_user }}" + encoding: UTF-8 + state: present + become: true + become_user: postgres -- name: Check if superuser exists - command: > - docker exec netbox-docker-netbox-1 - python /opt/netbox/netbox/manage.py shell -c - "from django.contrib.auth import get_user_model; User = get_user_model(); exit(0 if User.objects.filter(username='{{ netbox_admin_user }}').exists() else 1)" - register: superuser_check +- name: Check installed NetBox version + ansible.builtin.slurp: + src: "{{ netbox_install_dir }}/netbox/release.yaml" + register: netbox_release_file failed_when: false changed_when: false -- name: Create superuser - command: > - docker exec netbox-docker-netbox-1 - python /opt/netbox/netbox/manage.py createsuperuser - --username {{ netbox_admin_user }} - --email {{ netbox_admin_email }} - --noinput - environment: - DJANGO_SUPERUSER_PASSWORD: "{{ netbox_admin_password }}" - when: - - netbox_create_superuser | bool - - superuser_check.rc != 0 +- name: Set installed-version fact + ansible.builtin.set_fact: + netbox_installed_version: >- + {{ + (netbox_release_file.content | b64decode + | regex_search('version:\s*"?([0-9.]+)"?', '\1') + | first) + if netbox_release_file is defined and netbox_release_file.content is defined + else '' + }} -# Caddy reverse proxy setup -- name: Install Caddy prerequisites - apt: - name: - - debian-keyring - - debian-archive-keyring - - apt-transport-https - - curl - state: present - update_cache: true +- name: Decide whether NetBox needs (re)install + ansible.builtin.set_fact: + netbox_needs_install: "{{ netbox_installed_version != netbox_version }}" -- name: Add Caddy GPG key - shell: | - curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg - args: - creates: /usr/share/keyrings/caddy-stable-archive-keyring.gpg +- name: Download NetBox release tarball + ansible.builtin.get_url: + url: "{{ netbox_release_url }}" + dest: "/tmp/netbox-{{ netbox_version }}.tar.gz" + mode: "0644" + when: netbox_needs_install -- name: Add Caddy repository - shell: | - curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list - args: - creates: /etc/apt/sources.list.d/caddy-stable.list +- name: Extract NetBox release into a versioned directory + ansible.builtin.unarchive: + src: "/tmp/netbox-{{ netbox_version }}.tar.gz" + dest: /opt + remote_src: true + creates: "/opt/netbox-{{ netbox_version }}" + when: netbox_needs_install -- name: Fix permissions on Caddy repository files - file: - path: "{{ item }}" - mode: '0644' - loop: - - /usr/share/keyrings/caddy-stable-archive-keyring.gpg - - /etc/apt/sources.list.d/caddy-stable.list +- name: Symlink {{ netbox_install_dir }} -> /opt/netbox-{{ netbox_version }} + ansible.builtin.file: + src: "/opt/netbox-{{ netbox_version }}" + dest: "{{ netbox_install_dir }}" + state: link + force: true + when: netbox_needs_install + notify: + - Restart netbox + - Restart netbox-rq -- name: Update apt cache after adding Caddy repo - apt: - update_cache: true - -- name: Install Caddy - apt: - name: caddy - state: present - -- name: Create Caddy log directory - file: - path: /var/log/caddy +- name: Ensure netbox owns mutable subdirs + ansible.builtin.file: + path: "{{ netbox_install_dir }}/netbox/{{ item }}" state: directory - owner: caddy - group: caddy - mode: '0755' + owner: "{{ netbox_user }}" + group: "{{ netbox_group }}" + mode: "0755" + loop: + - media + - reports + - scripts + - static -- name: Configure Caddy for NetBox - template: - src: Caddyfile.j2 - dest: /etc/caddy/Caddyfile +- name: Render local_requirements.txt (plugin Python deps) + ansible.builtin.copy: + dest: "{{ netbox_install_dir }}/local_requirements.txt" + content: "{{ netbox_local_requirements | join('\n') }}\n" owner: root group: root - mode: '0644' - notify: restart caddy + mode: "0644" + notify: + - Restart netbox + - Restart netbox-rq -- name: Ensure Caddy is started and enabled - systemd: - name: caddy +- name: Render configuration.py + ansible.builtin.template: + src: configuration.py.j2 + dest: "{{ netbox_install_dir }}/netbox/netbox/configuration.py" + owner: root + group: "{{ netbox_group }}" + mode: "0640" + notify: + - Restart netbox + - Restart netbox-rq + +- name: Render gunicorn.py + ansible.builtin.template: + src: gunicorn.py.j2 + dest: "{{ netbox_install_dir }}/gunicorn.py" + owner: "{{ netbox_user }}" + group: "{{ netbox_group }}" + mode: "0644" + notify: Restart netbox + +- name: Run NetBox upgrade.sh (creates venv, installs deps, migrates DB, collectstatic) + ansible.builtin.command: "{{ netbox_install_dir }}/upgrade.sh" + args: + chdir: "{{ netbox_install_dir }}" + register: netbox_upgrade + when: netbox_needs_install + changed_when: netbox_upgrade.rc == 0 + +- name: Render netbox systemd unit + ansible.builtin.template: + src: netbox.service.j2 + dest: /etc/systemd/system/netbox.service + owner: root + group: root + mode: "0644" + notify: Restart netbox + +- name: Render netbox-rq systemd unit + ansible.builtin.template: + src: netbox-rq.service.j2 + dest: /etc/systemd/system/netbox-rq.service + owner: root + group: root + mode: "0644" + notify: Restart netbox-rq + +- name: Enable and start NetBox services + ansible.builtin.systemd: + name: "{{ item }}" + enabled: true state: started - enabled: true \ No newline at end of file + daemon_reload: true + loop: + - netbox + - netbox-rq diff --git a/ansible/roles/netbox/templates/Caddyfile.j2 b/ansible/roles/netbox/templates/Caddyfile.j2 deleted file mode 100644 index 785e4a2..0000000 --- a/ansible/roles/netbox/templates/Caddyfile.j2 +++ /dev/null @@ -1,54 +0,0 @@ -# {{ ansible_managed }} -# Caddy configuration for NetBox reverse proxy - -{{ netbox_domain | default('netbox.vntx.net') }} { - # Enable compression - encode gzip - - # Reverse proxy to NetBox - reverse_proxy localhost:{{ netbox_port }} { - # Set headers for proper operation - header_up Host {host} - header_up X-Real-IP {remote} - header_up X-Forwarded-For {remote} - header_up X-Forwarded-Proto {scheme} - - # WebSocket support - header_up Connection {>Connection} - header_up Upgrade {>Upgrade} - } - - # Logging - log { - output file /var/log/caddy/netbox-access.log - format json - } - - # Security headers - header { - # Enable HSTS - Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" - - # Disable content type sniffing - X-Content-Type-Options "nosniff" - - # Enable XSS protection - X-XSS-Protection "1; mode=block" - - # Clickjacking protection - X-Frame-Options "SAMEORIGIN" - - # Referrer policy - Referrer-Policy "strict-origin-when-cross-origin" - } - - # Handle large uploads (for NetBox file uploads) - request_body { - max_size 100MB - } -} - -# Redirect HTTP to HTTPS -http://{{ netbox_domain | default('netbox.vntx.net') }} { - redir https://{{ netbox_domain | default('netbox.vntx.net') }}{uri} permanent -} \ No newline at end of file diff --git a/ansible/roles/netbox/templates/configuration.py.j2 b/ansible/roles/netbox/templates/configuration.py.j2 new file mode 100644 index 0000000..2ea8898 --- /dev/null +++ b/ansible/roles/netbox/templates/configuration.py.j2 @@ -0,0 +1,126 @@ +{{ ansible_managed | comment(decoration='# ') }} +# +# NetBox configuration. All knobs come from ansible vars; see +# roles/netbox/defaults/main.yml for defaults and host_vars / vault for +# per-host overrides + secrets. + +ALLOWED_HOSTS = {{ netbox_allowed_hosts | to_json }} + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': {{ netbox_db_name | to_json }}, + 'USER': {{ netbox_db_user | to_json }}, + 'PASSWORD': {{ netbox_db_password | to_json }}, + 'HOST': {{ netbox_db_host | to_json }}, + 'PORT': {{ netbox_db_port | to_json }}, + 'CONN_MAX_AGE': {{ netbox_db_conn_max_age }}, + } +} + +REDIS = { + 'tasks': { + 'HOST': {{ netbox_redis_host | to_json }}, + 'PORT': {{ netbox_redis_port }}, + 'USERNAME': {{ netbox_redis_username | to_json }}, + 'PASSWORD': {{ netbox_redis_password | to_json }}, + 'DATABASE': {{ netbox_redis_tasks_db }}, + 'SSL': {{ netbox_redis_ssl | bool }}, + }, + 'caching': { + 'HOST': {{ netbox_redis_host | to_json }}, + 'PORT': {{ netbox_redis_port }}, + 'USERNAME': {{ netbox_redis_username | to_json }}, + 'PASSWORD': {{ netbox_redis_password | to_json }}, + 'DATABASE': {{ netbox_redis_caching_db }}, + 'SSL': {{ netbox_redis_ssl | bool }}, + }, +} + +SECRET_KEY = {{ netbox_secret_key | to_json }} + +{% if netbox_api_token_peppers is defined %} +API_TOKEN_PEPPERS = {{ netbox_api_token_peppers | to_json }} +{% endif %} + +ADMINS = [ +{% for a in netbox_admins %} + ({{ a.name | to_json }}, {{ a.email | to_json }}), +{% endfor %} +] + +ALLOW_TOKEN_RETRIEVAL = {{ netbox_allow_token_retrieval | bool }} + +AUTH_PASSWORD_VALIDATORS = {{ netbox_auth_password_validators | to_json }} + +BASE_PATH = {{ netbox_base_path | to_json }} + +CORS_ORIGIN_ALLOW_ALL = {{ netbox_cors_origin_allow_all | bool }} +CORS_ORIGIN_WHITELIST = {{ netbox_cors_origin_whitelist | to_json }} +CORS_ORIGIN_REGEX_WHITELIST = {{ netbox_cors_origin_regex_whitelist | to_json }} + +CSRF_COOKIE_NAME = {{ netbox_csrf_cookie_name | to_json }} + +DEBUG = {{ netbox_debug | bool }} +DEFAULT_LANGUAGE = {{ netbox_default_language | to_json }} + +EMAIL = { + 'SERVER': {{ netbox_email.SERVER | to_json }}, + 'PORT': {{ netbox_email.PORT }}, + 'USERNAME': {{ netbox_email.USERNAME | to_json }}, + 'PASSWORD': {{ netbox_email.PASSWORD | to_json }}, + 'USE_SSL': {{ netbox_email.USE_SSL | bool }}, + 'USE_TLS': {{ netbox_email.USE_TLS | bool }}, + 'TIMEOUT': {{ netbox_email.TIMEOUT }}, + 'FROM_EMAIL': {{ netbox_email.FROM_EMAIL | to_json }}, +} + +EXEMPT_VIEW_PERMISSIONS = {{ netbox_exempt_view_permissions | to_json }} + +INTERNAL_IPS = {{ netbox_internal_ips | to_json }} + +LOGGING = {{ netbox_logging | to_json }} + +LOGIN_PERSISTENCE = {{ netbox_login_persistence | bool }} +LOGIN_REQUIRED = {{ netbox_login_required | bool }} +{% if netbox_login_timeout %} +LOGIN_TIMEOUT = {{ netbox_login_timeout }} +{% else %} +LOGIN_TIMEOUT = None +{% endif %} +LOGIN_FORM_HIDDEN = {{ netbox_login_form_hidden | bool }} +LOGOUT_REDIRECT_URL = {{ netbox_logout_redirect_url | to_json }} + +METRICS_ENABLED = {{ netbox_metrics_enabled | bool }} + +PLUGINS = {{ netbox_plugins | to_json }} +{% if netbox_plugins_config %} +PLUGINS_CONFIG = {{ netbox_plugins_config | to_json }} +{% endif %} + +REMOTE_AUTH_ENABLED = {{ netbox_remote_auth_enabled | bool }} +REMOTE_AUTH_BACKEND = {{ netbox_remote_auth_backend | to_json }} +REMOTE_AUTH_HEADER = {{ netbox_remote_auth_header | to_json }} +REMOTE_AUTH_USER_FIRST_NAME = {{ netbox_remote_auth_user_first_name | to_json }} +REMOTE_AUTH_USER_LAST_NAME = {{ netbox_remote_auth_user_last_name | to_json }} +REMOTE_AUTH_USER_EMAIL = {{ netbox_remote_auth_user_email | to_json }} +REMOTE_AUTH_AUTO_CREATE_USER = {{ netbox_remote_auth_auto_create_user | bool }} +REMOTE_AUTH_DEFAULT_GROUPS = {{ netbox_remote_auth_default_groups | to_json }} +REMOTE_AUTH_DEFAULT_PERMISSIONS = {{ netbox_remote_auth_default_permissions | to_json }} + +{% if netbox_release_check_url %} +RELEASE_CHECK_URL = {{ netbox_release_check_url | to_json }} +{% else %} +RELEASE_CHECK_URL = None +{% endif %} + +RQ_DEFAULT_TIMEOUT = {{ netbox_rq_default_timeout }} + +SESSION_COOKIE_NAME = {{ netbox_session_cookie_name | to_json }} +{% if netbox_session_file_path %} +SESSION_FILE_PATH = {{ netbox_session_file_path | to_json }} +{% else %} +SESSION_FILE_PATH = None +{% endif %} + +TIME_ZONE = {{ netbox_time_zone | to_json }} diff --git a/ansible/roles/netbox/templates/docker-compose.override.yml.j2 b/ansible/roles/netbox/templates/docker-compose.override.yml.j2 deleted file mode 100644 index f9a3a89..0000000 --- a/ansible/roles/netbox/templates/docker-compose.override.yml.j2 +++ /dev/null @@ -1,37 +0,0 @@ -services: - netbox: - ports: - - "{{ netbox_listen_address }}:{{ netbox_port }}:8080" - volumes: - - {{ netbox_home }}/data/media:/opt/netbox/netbox/media - - {{ netbox_home }}/data/reports:/opt/netbox/netbox/reports - - {{ netbox_home }}/data/scripts:/opt/netbox/netbox/scripts - environment: - SKIP_SUPERUSER: "false" - SUPERUSER_API_TOKEN: "{{ netbox_superuser_api_token }}" - SUPERUSER_EMAIL: "{{ netbox_admin_email }}" - SUPERUSER_NAME: "{{ netbox_admin_user }}" - SUPERUSER_PASSWORD: "{{ netbox_admin_password }}" - restart: unless-stopped - - netbox-worker: - volumes: - - {{ netbox_home }}/data/media:/opt/netbox/netbox/media - - {{ netbox_home }}/data/reports:/opt/netbox/netbox/reports - - {{ netbox_home }}/data/scripts:/opt/netbox/netbox/scripts - restart: unless-stopped - - postgres: - volumes: - - {{ netbox_home }}/data/postgres:/var/lib/postgresql/data - restart: unless-stopped - - redis: - volumes: - - {{ netbox_home }}/data/redis:/data - restart: unless-stopped - - redis-cache: - volumes: - - {{ netbox_home }}/data/redis-cache:/data - restart: unless-stopped \ No newline at end of file diff --git a/ansible/roles/netbox/templates/env.j2 b/ansible/roles/netbox/templates/env.j2 deleted file mode 100644 index f2dc121..0000000 --- a/ansible/roles/netbox/templates/env.j2 +++ /dev/null @@ -1,82 +0,0 @@ -# NetBox environment configuration -# {{ ansible_managed }} - -# PostgreSQL database configuration -DB_NAME={{ netbox_db_name }} -DB_USER={{ netbox_db_user }} -DB_PASSWORD={{ netbox_db_password }} -DB_HOST={{ netbox_db_host }} -DB_PORT={{ netbox_db_port }} - -# Redis configuration -REDIS_HOST={{ netbox_redis_host }} -REDIS_PORT={{ netbox_redis_port }} -REDIS_PASSWORD={{ netbox_redis_password }} -REDIS_DATABASE=0 -REDIS_SSL=false - -# Redis cache configuration -REDIS_CACHE_HOST={{ netbox_redis_cache_host }} -REDIS_CACHE_PORT={{ netbox_redis_cache_port }} -REDIS_CACHE_PASSWORD={{ netbox_redis_cache_password }} -REDIS_CACHE_DATABASE=1 -REDIS_CACHE_SSL=false - -# NetBox configuration -SECRET_KEY={{ netbox_secret_key }} -SUPERUSER_API_TOKEN={{ netbox_superuser_api_token }} -ALLOWED_HOSTS={{ netbox_allowed_hosts }} -DEBUG={{ netbox_debug | lower }} -METRICS_ENABLED={{ netbox_metrics_enabled | lower }} - -# Email configuration -EMAIL_SERVER={{ netbox_email_server }} -EMAIL_PORT={{ netbox_email_port }} -EMAIL_USERNAME={{ netbox_email_username }} -EMAIL_PASSWORD={{ netbox_email_password }} -EMAIL_USE_SSL={{ netbox_email_use_ssl | lower }} -EMAIL_USE_TLS={{ netbox_email_use_tls | lower }} -EMAIL_SSL_CERTFILE= -EMAIL_SSL_KEYFILE= -EMAIL_FROM={{ netbox_email_from }} -EMAIL_TIMEOUT={{ netbox_email_timeout }} - -# NAPALM configuration -NAPALM_USERNAME={{ netbox_napalm_username }} -NAPALM_PASSWORD={{ netbox_napalm_password }} -NAPALM_TIMEOUT={{ netbox_napalm_timeout }} - -# Housekeeping -HOUSEKEEPING_INTERVAL={{ netbox_housekeeping_interval }} - -# Webhooks -WEBHOOKS_ENABLED={{ netbox_webhooks_enabled | lower }} - -# GraphQL API -GRAPHQL_ENABLED={{ netbox_graphql_enabled | lower }} - -# Time zone -TZ={{ netbox_timezone }} - -# Change logging -CHANGELOG_RETENTION={{ netbox_changelog_retention }} - -# Job results -JOBRESULT_RETENTION={{ netbox_jobresult_retention }} - -# Maximum upload size in bytes -MAX_UPLOAD_SIZE={{ netbox_max_upload_size }} - -# Pagination -PAGINATE_COUNT={{ netbox_paginate_count }} -MAX_PAGE_SIZE={{ netbox_max_page_size }} - -# Cache timeout -CACHE_TIMEOUT={{ netbox_cache_timeout }} - -# CORS -CORS_ORIGIN_ALLOW_ALL={{ netbox_cors_origin_allow_all | lower }} - -# Login persistence -LOGIN_PERSISTENCE={{ netbox_login_timeout }} -LOGIN_REQUIRED={{ netbox_login_required | lower }} \ No newline at end of file diff --git a/ansible/roles/netbox/templates/gunicorn.py.j2 b/ansible/roles/netbox/templates/gunicorn.py.j2 new file mode 100644 index 0000000..4397f5f --- /dev/null +++ b/ansible/roles/netbox/templates/gunicorn.py.j2 @@ -0,0 +1,10 @@ +{{ ansible_managed | comment(decoration='# ') }} + +bind = '{{ netbox_listen_addr }}' + +workers = {{ netbox_gunicorn_workers }} +threads = {{ netbox_gunicorn_threads }} +timeout = {{ netbox_gunicorn_timeout }} + +max_requests = {{ netbox_gunicorn_max_requests }} +max_requests_jitter = {{ netbox_gunicorn_max_requests_jitter }} diff --git a/ansible/roles/netbox/templates/netbox-docker.service.j2 b/ansible/roles/netbox/templates/netbox-docker.service.j2 deleted file mode 100644 index ade362f..0000000 --- a/ansible/roles/netbox/templates/netbox-docker.service.j2 +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=NetBox Docker Compose Application -Requires=docker.service -After=docker.service network-online.target -RequiresMountsFor={{ netbox_home }} - -[Service] -Type=exec -WorkingDirectory={{ netbox_home }}/netbox-docker -ExecStart=/usr/bin/docker compose -f docker-compose.yml -f docker-compose.override.yml up -ExecStop=/usr/bin/docker compose -f docker-compose.yml -f docker-compose.override.yml down -TimeoutStopSec=70 -Restart=always -RestartSec=10 - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/ansible/roles/netbox/templates/netbox-rq.service.j2 b/ansible/roles/netbox/templates/netbox-rq.service.j2 new file mode 100644 index 0000000..e442d23 --- /dev/null +++ b/ansible/roles/netbox/templates/netbox-rq.service.j2 @@ -0,0 +1,22 @@ +{{ ansible_managed | comment(decoration='# ') }} +[Unit] +Description=NetBox Request Queue Worker +Documentation=https://docs.netbox.dev/ +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple + +User={{ netbox_user }} +Group={{ netbox_group }} +WorkingDirectory={{ netbox_install_dir }} + +ExecStart={{ netbox_install_dir }}/venv/bin/python3 {{ netbox_install_dir }}/netbox/manage.py rqworker high default low + +Restart=on-failure +RestartSec=30 +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/ansible/roles/netbox/templates/netbox.service.j2 b/ansible/roles/netbox/templates/netbox.service.j2 new file mode 100644 index 0000000..9140b13 --- /dev/null +++ b/ansible/roles/netbox/templates/netbox.service.j2 @@ -0,0 +1,23 @@ +{{ ansible_managed | comment(decoration='# ') }} +[Unit] +Description=NetBox WSGI Service +Documentation=https://docs.netbox.dev/ +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple + +User={{ netbox_user }} +Group={{ netbox_group }} +PIDFile=/var/tmp/netbox.pid +WorkingDirectory={{ netbox_install_dir }} + +ExecStart={{ netbox_install_dir }}/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath {{ netbox_install_dir }}/netbox --config {{ netbox_install_dir }}/gunicorn.py netbox.wsgi + +Restart=on-failure +RestartSec=30 +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/ansible/roles/netbox/templates/postgres.env.j2 b/ansible/roles/netbox/templates/postgres.env.j2 deleted file mode 100644 index f3f31f2..0000000 --- a/ansible/roles/netbox/templates/postgres.env.j2 +++ /dev/null @@ -1,6 +0,0 @@ -# PostgreSQL environment configuration -# {{ ansible_managed }} - -POSTGRES_USER={{ netbox_db_user }} -POSTGRES_PASSWORD={{ netbox_db_password }} -POSTGRES_DB={{ netbox_db_name }} \ No newline at end of file diff --git a/ansible/roles/netbox/templates/redis-cache.env.j2 b/ansible/roles/netbox/templates/redis-cache.env.j2 deleted file mode 100644 index 068ca71..0000000 --- a/ansible/roles/netbox/templates/redis-cache.env.j2 +++ /dev/null @@ -1,6 +0,0 @@ -# Redis cache environment configuration -# {{ ansible_managed }} - -{% if netbox_redis_cache_password %} -REDIS_PASSWORD={{ netbox_redis_cache_password }} -{% endif %} \ No newline at end of file diff --git a/ansible/roles/netbox/templates/redis.env.j2 b/ansible/roles/netbox/templates/redis.env.j2 deleted file mode 100644 index f5527f7..0000000 --- a/ansible/roles/netbox/templates/redis.env.j2 +++ /dev/null @@ -1,6 +0,0 @@ -# Redis environment configuration -# {{ ansible_managed }} - -{% if netbox_redis_password %} -REDIS_PASSWORD={{ netbox_redis_password }} -{% endif %} \ No newline at end of file