diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index f4b6305..fa6bab1 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -42,6 +42,7 @@ jobs: VALIDATE_GITHUB_ACTIONS_ZIZMOR: false VALIDATE_GITLEAKS: false VALIDATE_JSCPD: false + VALIDATE_PYTHON_PYLINT: false VALIDATE_TRIVY: false FILTER_REGEX_EXCLUDE: (.*/)?(LICENSE|configuration/.*) EDITORCONFIG_FILE_NAME: .editorconfig-checker.json diff --git a/Dockerfile b/Dockerfile index b398e9c..753200a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ ARG NETBOX_PATH COPY ${NETBOX_PATH}/requirements.txt requirements-container.txt / ENV VIRTUAL_ENV=/opt/netbox/venv RUN \ - # Gunicorn is not needed because we use Nginx Unit + # Gunicorn is not needed because we use Granian sed -i -e '/gunicorn/d' /requirements.txt && \ # We need 'social-auth-core[all]' in the Docker image. But if we put it in our own requirements-container.txt # we have potential version conflicts and the build will fail. @@ -46,8 +46,6 @@ RUN \ ARG FROM FROM ${FROM} AS main -COPY docker/unit.list /etc/apt/sources.list.d/unit.list -ADD --chmod=444 --chown=0:0 https://unit.nginx.org/keys/nginx-keyring.gpg /usr/share/keyrings/nginx-keyring.gpg RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get update -qq \ && apt-get upgrade \ @@ -64,8 +62,6 @@ RUN export DEBIAN_FRONTEND=noninteractive \ openssl \ python3 \ tini \ - unit-python3.12=1.34.2-1~noble \ - unit=1.34.2-1~noble \ && rm -rf /var/lib/apt/lists/* # Copy the modified 'requirements*.txt' files, to have the files actually used during installation @@ -81,21 +77,21 @@ COPY docker/ldap_config.docker.py /opt/netbox/netbox/netbox/ldap_config.py COPY docker/docker-entrypoint.sh /opt/netbox/docker-entrypoint.sh COPY docker/launch-netbox.sh /opt/netbox/launch-netbox.sh COPY configuration/ /etc/netbox/config/ -COPY docker/nginx-unit.json /etc/unit/ +COPY docker/granian.py /opt/netbox/netbox/netbox/granian.py COPY VERSION /opt/netbox/VERSION WORKDIR /opt/netbox/netbox # Must set permissions for '/opt/netbox/netbox/media' directory # to g+w so that pictures can be uploaded to netbox. -RUN mkdir -p static media /opt/unit/state/ /opt/unit/tmp/ \ - && chown -R unit:root /opt/unit/ media reports scripts \ - && chmod -R g+w /opt/unit/ media reports scripts \ - && cd /opt/netbox/ && SECRET_KEY="dummyKeyWithMinimumLength-------------------------" /opt/netbox/venv/bin/python -m mkdocs build \ - --config-file /opt/netbox/mkdocs.yml --site-dir /opt/netbox/netbox/project-static/docs/ \ - && DEBUG="true" SECRET_KEY="dummyKeyWithMinimumLength-------------------------" /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py collectstatic --no-input \ - && mkdir /opt/netbox/netbox/local \ - && echo "build: Docker-$(cat /opt/netbox/VERSION)" > /opt/netbox/netbox/local/release.yaml +RUN useradd --home-dir /opt/netbox/ --no-create-home --no-user-group --system --shell /bin/false --uid 999 --gid 0 netbox \ + && mkdir -p static media local \ + && chown -R netbox:root media reports scripts \ + && chmod -R g+w media reports scripts \ + && cd /opt/netbox/ && SECRET_KEY="dummyKeyWithMinimumLength-------------------------" /opt/netbox/venv/bin/python -m mkdocs build \ + --config-file /opt/netbox/mkdocs.yml --site-dir /opt/netbox/netbox/project-static/docs/ \ + && DEBUG="true" SECRET_KEY="dummyKeyWithMinimumLength-------------------------" /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py collectstatic --no-input \ + && echo "build: Docker-$(cat /opt/netbox/VERSION)" > /opt/netbox/netbox/local/release.yaml ENV LANG=C.utf8 PATH=/opt/netbox/venv/bin:$PATH VIRTUAL_ENV=/opt/netbox/venv UV_NO_CACHE=1 ENTRYPOINT [ "/usr/bin/tini", "--" ] diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example index d7ef961..1394814 100644 --- a/docker-compose.override.yml.example +++ b/docker-compose.override.yml.example @@ -2,9 +2,6 @@ services: netbox: ports: - "8000:8080" - # If you want the Nginx unit status page visible from the - # outside of the container add the following port mapping: - # - "8001:8081" # healthcheck: # Time for which the health check can fail after the container is started. # This depends mostly on the performance of your database. On the first start, @@ -19,4 +16,3 @@ services: # SUPERUSER_EMAIL: "" # SUPERUSER_NAME: "" # SUPERUSER_PASSWORD: "" - diff --git a/docker-compose.test.yml b/docker-compose.test.yml index d5acd95..8e22aa6 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -9,7 +9,7 @@ services: redis-cache: condition: service_healthy env_file: env/netbox.env - user: "unit:root" + user: "netbox:root" volumes: - ./test-configuration/test_config.py:/etc/netbox/config/test_config.py:z,ro healthcheck: diff --git a/docker-compose.yml b/docker-compose.yml index 2049c33..86586c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: - redis - redis-cache env_file: env/netbox.env - user: "unit:root" + user: "netbox:root" healthcheck: test: curl -f http://localhost:8080/login/ || exit 1 start_period: 90s diff --git a/docker/granian.py b/docker/granian.py new file mode 100644 index 0000000..651b7da --- /dev/null +++ b/docker/granian.py @@ -0,0 +1,13 @@ +from granian.utils.proxies import wrap_wsgi_with_proxy_headers +from netbox.wsgi import application + +application = wrap_wsgi_with_proxy_headers( + application, + trusted_hosts=[ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "fc00::/7", + "fe80::/10", + ], +) diff --git a/docker/launch-netbox.sh b/docker/launch-netbox.sh index 11da1de..a61e076 100755 --- a/docker/launch-netbox.sh +++ b/docker/launch-netbox.sh @@ -1,57 +1,20 @@ #!/bin/bash -UNIT_CONFIG="${UNIT_CONFIG-/etc/unit/nginx-unit.json}" -# Also used in "nginx-unit.json" -UNIT_SOCKET="/opt/unit/unit.sock" - -load_configuration() { - MAX_WAIT=10 - WAIT_COUNT=0 - while [ ! -S $UNIT_SOCKET ]; do - if [ $WAIT_COUNT -ge $MAX_WAIT ]; then - echo "⚠️ No control socket found; configuration will not be loaded." - return 1 - fi - - WAIT_COUNT=$((WAIT_COUNT + 1)) - echo "⏳ Waiting for control socket to be created... (${WAIT_COUNT}/${MAX_WAIT})" - - sleep 1 - done - - # even when the control socket exists, it does not mean unit has finished initialisation - # this curl call will get a reply once unit is fully launched - curl --silent --output /dev/null --request GET --unix-socket $UNIT_SOCKET http://localhost/ - - echo "⚙️ Applying configuration from $UNIT_CONFIG" - - RESP_CODE=$( - curl \ - --silent \ - --output /dev/null \ - --write-out '%{http_code}' \ - --request PUT \ - --data-binary "@${UNIT_CONFIG}" \ - --unix-socket $UNIT_SOCKET \ - http://localhost/config - ) - if [ "$RESP_CODE" != "200" ]; then - echo "⚠️ Could not load Unit configuration" - kill "$(cat /opt/unit/unit.pid)" - return 1 - fi - - echo "✅ Unit configuration loaded successfully" -} - -load_configuration & - -exec unitd \ - --no-daemon \ - --control unix:$UNIT_SOCKET \ - --pid /opt/unit/unit.pid \ - --log /dev/stdout \ - --statedir /opt/unit/state/ \ - --tmpdir /opt/unit/tmp/ \ - --user unit \ - --group root +exec granian \ + --host "::" \ + --port "8080" \ + --interface "wsgi" \ + --no-ws \ + --workers "${GRANIAN_WORKERS:-4}" \ + --respawn-failed-workers \ + --backpressure "${GRANIAN_BACKPRESSURE:-${GRANIAN_WORKERS:-4}}" \ + --loop "uvloop" \ + --log \ + --log-level "info" \ + --access-log \ + --working-dir "/opt/netbox/netbox/" \ + --static-path-route "/static" \ + --static-path-mount "/opt/netbox/netbox/static/" \ + --pid-file "/tmp/granian.pid" \ + "${GRANIAN_EXTRA_ARGS[@]}" \ + "netbox.granian:application" diff --git a/docker/nginx-unit.json b/docker/nginx-unit.json deleted file mode 100644 index 95fa1ef..0000000 --- a/docker/nginx-unit.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "listeners": { - "0.0.0.0:8080": { - "pass": "routes/main", - "forwarded": { - "client_ip": "X-Forwarded-For", - "protocol": "X-Forwarded-Proto", - "source": ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] - } - }, - "0.0.0.0:8081": { - "pass": "routes/status", - "forwarded": { - "client_ip": "X-Forwarded-For", - "protocol": "X-Forwarded-Proto", - "source": ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] - } - }, - "[::]:8080": { - "pass": "routes/main", - "forwarded": { - "client_ip": "X-Forwarded-For", - "protocol": "X-Forwarded-Proto", - "source": ["fc00::/7", "fe80::/10"] - } - }, - "[::]:8081": { - "pass": "routes/status", - "forwarded": { - "client_ip": "X-Forwarded-For", - "protocol": "X-Forwarded-Proto", - "source": ["fc00::/7", "fe80::/10"] - } - } - }, - "routes": { - "main": [ - { - "match": { - "uri": "/static/*" - }, - "action": { - "share": "/opt/netbox/netbox${uri}" - } - }, - { - "action": { - "pass": "applications/netbox" - } - } - ], - "status": [ - { - "match": { - "uri": "/status/*" - }, - "action": { - "proxy": "http://unix:/opt/unit/unit.sock" - } - } - ] - }, - "applications": { - "netbox": { - "type": "python 3", - "path": "/opt/netbox/netbox/", - "module": "netbox.wsgi", - "home": "/opt/netbox/venv", - "processes": { - "max": 4, - "spare": 1, - "idle_timeout": 120 - } - } - }, - "access_log": "/dev/stdout", - "settings": { - "http": { - "max_body_size": 104857600 - } - } -} diff --git a/docker/unit.list b/docker/unit.list deleted file mode 100644 index 6193723..0000000 --- a/docker/unit.list +++ /dev/null @@ -1 +0,0 @@ -deb [signed-by=/usr/share/keyrings/nginx-keyring.gpg] http://packages.nginx.org/unit/ubuntu/ noble unit diff --git a/env/netbox.env b/env/netbox.env index 52fca3b..88c8705 100644 --- a/env/netbox.env +++ b/env/netbox.env @@ -15,6 +15,8 @@ EMAIL_USERNAME=netbox # EMAIL_USE_SSL and EMAIL_USE_TLS are mutually exclusive, i.e. they can't both be `true`! EMAIL_USE_SSL=false EMAIL_USE_TLS=false +GRANIAN_BACKPRESSURE=4 +GRANIAN_WORKERS=4 GRAPHQL_ENABLED=true MEDIA_ROOT=/opt/netbox/netbox/media METRICS_ENABLED=false diff --git a/requirements-container.txt b/requirements-container.txt index 65dc789..74b4b1e 100644 --- a/requirements-container.txt +++ b/requirements-container.txt @@ -1,5 +1,6 @@ django-auth-ldap==5.2.0 dulwich==0.24.10 +granian[uvloop]==2.5.7 python3-saml==1.16.0 --no-binary lxml --no-binary xmlsec