From c7a3513fa1005ebcd69c5918c658d163782bfb9a Mon Sep 17 00:00:00 2001 From: Timotej Lazar Date: Tue, 28 May 2024 12:32:28 +0200 Subject: [PATCH] Add netbox role MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kinda ouroborosish if you think about it. Better don’t. --- roles/facts/tasks/main.yml | 5 + roles/netbox/files/default.conf | 11 ++ roles/netbox/files/local_requirements.txt | 2 + roles/netbox/handlers/main.yml | 27 ++++ roles/netbox/tasks/app.yml | 150 ++++++++++++++++++++++ roles/netbox/tasks/db.yml | 55 ++++++++ roles/netbox/tasks/main.yml | 25 ++++ roles/netbox/templates/netbox-rq.initd.j2 | 10 ++ roles/netbox/templates/netbox.conf.j2 | 23 ++++ roles/netbox/templates/netbox.initd.j2 | 10 ++ roles/nginx/files/default.conf | 11 ++ roles/nginx/handlers/main.yml | 5 + roles/nginx/tasks/main.yml | 39 ++++++ setup.yml | 6 + 14 files changed, 379 insertions(+) create mode 100644 roles/netbox/files/default.conf create mode 100644 roles/netbox/files/local_requirements.txt create mode 100644 roles/netbox/handlers/main.yml create mode 100644 roles/netbox/tasks/app.yml create mode 100644 roles/netbox/tasks/db.yml create mode 100644 roles/netbox/tasks/main.yml create mode 100644 roles/netbox/templates/netbox-rq.initd.j2 create mode 100644 roles/netbox/templates/netbox.conf.j2 create mode 100644 roles/netbox/templates/netbox.initd.j2 create mode 100644 roles/nginx/files/default.conf create mode 100644 roles/nginx/handlers/main.yml create mode 100644 roles/nginx/tasks/main.yml diff --git a/roles/facts/tasks/main.yml b/roles/facts/tasks/main.yml index 46e881b..13207d6 100644 --- a/roles/facts/tasks/main.yml +++ b/roles/facts/tasks/main.yml @@ -11,3 +11,8 @@ cluster: '{{ query("netbox.netbox.nb_lookup", "clusters", raw_data=true, api_filter="name="+cluster) | first }}' nodes: '{{ groups["cluster_"+cluster] | map("extract", hostvars) | rejectattr("is_virtual") }}' when: cluster + +- name: Get my domain names if any + set_fact: + fqdns: '{{ interfaces | map(attribute="ip_addresses") | flatten + | map(attribute="dns_name") | reject("==", "") | sort | unique }}' diff --git a/roles/netbox/files/default.conf b/roles/netbox/files/default.conf new file mode 100644 index 0000000..cd539a6 --- /dev/null +++ b/roles/netbox/files/default.conf @@ -0,0 +1,11 @@ +# handle .well-known for all domains +server { + listen 80 default_server; + listen [::]:80 default_server; + location /.well-known/ { + alias /srv/http/.well-known/; + } + location / { + return 301 https://$host$request_uri; + } +} diff --git a/roles/netbox/files/local_requirements.txt b/roles/netbox/files/local_requirements.txt new file mode 100644 index 0000000..79ce173 --- /dev/null +++ b/roles/netbox/files/local_requirements.txt @@ -0,0 +1,2 @@ +dulwich # for git data sources +netbox-topology-views diff --git a/roles/netbox/handlers/main.yml b/roles/netbox/handlers/main.yml new file mode 100644 index 0000000..3280c02 --- /dev/null +++ b/roles/netbox/handlers/main.yml @@ -0,0 +1,27 @@ +- name: reload nginx + service: + name: nginx + state: reloaded + when: "'handler' not in ansible_skip_tags" + +- name: reload package cache + package: + update_cache: yes + when: "'handler' not in ansible_skip_tags" + +- name: restart netbox + service: + name: '{{ item }}' + state: restarted + loop: + - netbox + - netbox-rq + when: "'handler' not in ansible_skip_tags" + +- name: run migrations + become: yes + become_method: su + become_user: '{{ user }}' + command: sh ~/app/upgrade.sh + notify: restart netbox + when: "'handler' not in ansible_skip_tags" diff --git a/roles/netbox/tasks/app.yml b/roles/netbox/tasks/app.yml new file mode 100644 index 0000000..581cade --- /dev/null +++ b/roles/netbox/tasks/app.yml @@ -0,0 +1,150 @@ +- name: Install dependencies + package: + name: + - git + - python3 + - python3-dev + - py3-pip + - py3-virtualenv + - bash # for upgrade script + - build-base # to build psycopg if not available + - postgresql-dev # likewise + +- name: Checkout repo + become: yes + become_method: su + become_user: '{{ user }}' + git: + repo: https://github.com/netbox-community/netbox.git + dest: '{{ user_info.home }}/app' + version: 'v{{ netbox_version }}' + notify: run migrations + +- name: Copy default config + copy: + dest: '{{ user_info.home }}/app/netbox/netbox/configuration.py' + src: '{{ user_info.home }}/app/netbox/netbox/configuration_example.py' + remote_src: yes + owner: '{{ user_info.uid }}' + group: '{{ user_info.group }}' + force: no + notify: run migrations + +- name: Restrict access to config + file: + path: '{{ user_info.home }}/app/netbox/netbox/configuration.py' + mode: 0600 + +- name: Configure secret key + lineinfile: + path: '{{ user_info.home }}/app/netbox/netbox/configuration.py' + regexp: "^SECRET_KEY = ''" + line: "SECRET_KEY = '{{ lookup('password', '/dev/null', length=50) }}'" + backrefs: yes # don’t set if set already + +- name: Configure base settings and database + lineinfile: + path: '{{ user_info.home }}/app/netbox/netbox/configuration.py' + regexp: '{{ item.key }}' + line: '{{ item.line }}' + loop: + - key: '^ALLOWED_HOSTS = ' + line: "ALLOWED_HOSTS = [{{ fqdns | map('regex_replace', '^(.*)$', '\"\\1\"') | join(', ') }}]" + - key: 'USER.*PostgreSQL username' + line: " 'USER': '{{ user }}', # PostgreSQL username" + # XXX unnecessary? + #- key: '(OPTIONS|PASSWORD).*PostgreSQL password' + # line: " 'OPTIONS': { 'passfile': '{{ user_info.home }}/.pgpass' }, # PostgreSQL password" + # not yet compatible, see https://github.com/netbox-community/netbox-topology-views/issues/503 + #- key: '^PLUGINS = ' + # line: "PLUGINS = ['netbox_topology_views']" + notify: run migrations + +- name: Configure OIDC authentication + lineinfile: + path: '{{ user_info.home }}/app/netbox/netbox/configuration.py' + regexp: '{{ item.key }}' + line: '{{ item.line }}' + loop: + - key: "^REMOTE_AUTH_ENABLED =" + line: "REMOTE_AUTH_ENABLED = True" + - key: "^REMOTE_AUTH_BACKEND =" + line: "REMOTE_AUTH_BACKEND = 'social_core.backends.open_id_connect.OpenIdConnectAuth'" + - key: "^SOCIAL_AUTH_OIDC_OIDC_ENDPOINT =" + line: "SOCIAL_AUTH_OIDC_OIDC_ENDPOINT = '{{ lookup('passwordstore', 'vm/'~inventory_hostname, subkey='oidc_endpoint') }}'" + - key: "^SOCIAL_AUTH_OIDC_KEY =" + line: "SOCIAL_AUTH_OIDC_KEY = '{{ lookup('passwordstore', 'vm/'~inventory_hostname, subkey='oidc_client_id') }}'" + - key: "^SOCIAL_AUTH_OIDC_SECRET =" + line: "SOCIAL_AUTH_OIDC_SECRET = '{{ lookup('passwordstore', 'vm/'~inventory_hostname, subkey='oidc_client_secret') }}'" + # TODO the key should really be upn but it doesn’t seem to work + - key: "^SOCIAL_AUTH_OIDC_USERNAME_KEY =" + line: "SOCIAL_AUTH_OIDC_USERNAME_KEY = 'email'" + notify: run migrations + +- name: Set additional requirements + become: yes + become_method: su + become_user: '{{ user }}' + copy: + dest: '{{ user_info.home }}/app/' + src: local_requirements.txt + notify: run migrations + +- meta: flush_handlers + +- name: Create superuser + become: yes + become_method: su + become_user: '{{ user }}' + command: + cmd: '{{ user_info.home }}/app/venv/bin/python {{ user_info.home }}/app/netbox/manage.py shell --interface python' + stdin: | + import sys + from users.models import User + #from django.contrib.auth.models import User + username = '{{ lookup('passwordstore', 'vm/'~inventory_hostname, subkey='admin_user') }}' + if not User.objects.filter(username=username): + User.objects.create_superuser(username, '', # TODO email + '{{ lookup('passwordstore', 'vm/'~inventory_hostname, subkey='admin_pass') }}') + sys.exit(1) + register: result + changed_when: result.rc != 0 + +- name: Set up gunicorn + copy: + dest: /srv/netbox/gunicorn.py + src: /srv/netbox/app/contrib/gunicorn.py + remote_src: yes + force: no + owner: netbox + group: netbox + +- name: Set up cron job + file: + dest: /etc/periodic/daily/netbox-housekeeping.sh + src: /srv/netbox/app/contrib/netbox-housekeeping.sh + state: link + +- name: Install services + template: + dest: '/etc/init.d/{{ item }}' + src: '{{ item }}.initd.j2' + mode: 0755 + loop: + - netbox + - netbox-rq + +- name: Enable services + service: + name: '{{ item }}' + enabled: true + state: started + loop: + - netbox + - netbox-rq + +- name: Set up nginx site + template: + dest: '/etc/nginx/http.d/netbox.conf' + src: 'netbox.conf.j2' + notify: reload nginx diff --git a/roles/netbox/tasks/db.yml b/roles/netbox/tasks/db.yml new file mode 100644 index 0000000..81fac5a --- /dev/null +++ b/roles/netbox/tasks/db.yml @@ -0,0 +1,55 @@ +- name: Install packages + package: + name: + - postgresql + - py3-psycopg2 + - redis + +- name: Enable services + service: + name: '{{ item }}' + enabled: true + state: started + loop: + - postgresql + - redis + +- name: Create .pgpass + copy: + dest: '{{ user_info.home }}/.pgpass' + content: | + localhost:5432:{{ database }}:{{ user }}:{{ db_password }} + force: no + mode: 0600 + owner: '{{ user_info.uid }}' + group: '{{ user_info.group }}' + +- become: yes + become_method: su + become_user: postgres + block: + - name: Create database + postgresql_db: + name: '{{ database }}' + + - name: Create database user + postgresql_user: + db: '{{ database }}' + name: '{{ user }}' + password: '{{ db_password }}' + no_password_changes: yes + + - name: Set schema owner + postgresql_owner: + db: '{{ database }}' + new_owner: '{{ user }}' + obj_name: public + obj_type: schema + + - name: Grant database privileges + postgresql_privs: + db: '{{ database }}' + role: '{{ user }}' + privs: CREATE + type: database + diff --git a/roles/netbox/tasks/main.yml b/roles/netbox/tasks/main.yml new file mode 100644 index 0000000..c1547de --- /dev/null +++ b/roles/netbox/tasks/main.yml @@ -0,0 +1,25 @@ +- name: Set variables + set_fact: + user: '{{ user | default("netbox") }}' + database: '{{ database | default("netbox") }}' + db_password: '{{ lookup("password", "/dev/null", chars=["ascii_letters", "digits"]) }}' + +- name: Create group for web service + group: + name: '{{ user }}' + system: yes + +- name: Create user for web service + user: + name: '{{ user }}' + group: '{{ user }}' + home: '/srv/{{ user }}' + shell: /bin/sh + system: yes + register: user_info + +- name: Set up database + import_tasks: db.yml + +- name: Set up app + import_tasks: app.yml diff --git a/roles/netbox/templates/netbox-rq.initd.j2 b/roles/netbox/templates/netbox-rq.initd.j2 new file mode 100644 index 0000000..c45eb3d --- /dev/null +++ b/roles/netbox/templates/netbox-rq.initd.j2 @@ -0,0 +1,10 @@ +#!/sbin/openrc-run + +description="NetBox request queue worker" + +command="{{ user_info.home }}/app/venv/bin/python3" +command_args="{{ user_info.home }}/app/netbox/manage.py rqworker high default low" +command_user="{{ user }}:{{ user }}" + +command_background=true +pidfile="/run/${RC_SVCNAME}.pid" diff --git a/roles/netbox/templates/netbox.conf.j2 b/roles/netbox/templates/netbox.conf.j2 new file mode 100644 index 0000000..c23c6e5 --- /dev/null +++ b/roles/netbox/templates/netbox.conf.j2 @@ -0,0 +1,23 @@ +{% for fqdn in fqdns %} +server { + server_name {{ fqdn }}; + + listen [::]:443 ssl ipv6only=off; + ssl_certificate /etc/letsencrypt/live/{{ fqdn }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ fqdn }}/privkey.pem; + + client_max_body_size 100m; + + location /static/ { + alias {{ user_info.home }}/app/netbox/static/; + } + + location / { + proxy_pass http://127.0.0.1:8001; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +{% endfor %} diff --git a/roles/netbox/templates/netbox.initd.j2 b/roles/netbox/templates/netbox.initd.j2 new file mode 100644 index 0000000..0ea55e0 --- /dev/null +++ b/roles/netbox/templates/netbox.initd.j2 @@ -0,0 +1,10 @@ +#!/sbin/openrc-run + +description="NetBox WSGI service" + +command="{{ user_info.home }}/app/venv/bin/gunicorn" +command_args="--pythonpath {{ user_info.home }}/app/netbox --config {{ user_info.home }}/gunicorn.py netbox.wsgi" +command_user="{{ user }}:{{ user }}" + +command_background=true +pidfile="/run/${RC_SVCNAME}.pid" diff --git a/roles/nginx/files/default.conf b/roles/nginx/files/default.conf new file mode 100644 index 0000000..acec931 --- /dev/null +++ b/roles/nginx/files/default.conf @@ -0,0 +1,11 @@ +# handle .well-known and HTTPS redirect for all domains +server { + listen 80 default_server; + listen [::]:80 default_server; + location /.well-known/ { + alias /srv/http/.well-known/; + } + location / { + return 301 https://$host$request_uri; + } +} diff --git a/roles/nginx/handlers/main.yml b/roles/nginx/handlers/main.yml new file mode 100644 index 0000000..f6a6249 --- /dev/null +++ b/roles/nginx/handlers/main.yml @@ -0,0 +1,5 @@ +- name: reload nginx + service: + name: nginx + state: reloaded + when: "'handler' not in ansible_skip_tags" diff --git a/roles/nginx/tasks/main.yml b/roles/nginx/tasks/main.yml new file mode 100644 index 0000000..2bbc284 --- /dev/null +++ b/roles/nginx/tasks/main.yml @@ -0,0 +1,39 @@ +- name: Install packages + package: + name: + - certbot + - nginx + +- name: Create HTTP server directories + file: + path: /srv/http/.well-known + recurse: true + state: directory + owner: nginx + group: nginx + +- name: Set up default HTTP server + copy: + dest: /etc/nginx/http.d + src: default.conf + notify: reload nginx + +- name: Enable nginx service + service: + name: nginx + enabled: true + state: started + +- name: Get LE certificate + command: + cmd: certbot certonly --non-interactive --agree-tos --register-unsafely-without-email --webroot --webroot-path /srv/http -d {{ item }} + creates: '/etc/letsencrypt/renewal/{{ item }}.conf' + loop: '{{ fqdns }}' + +- name: Enable certbot renewal + cron: + name: "certbot renew" + job: "certbot renew --quiet" + user: root + hour: "2,14" + minute: "18" diff --git a/setup.yml b/setup.yml index 36e345e..b2a8c3a 100644 --- a/setup.yml +++ b/setup.yml @@ -20,6 +20,12 @@ - alpine - dokuwiki +- hosts: netbox + roles: + - alpine + - nginx + - netbox + - hosts: samba roles: - debian