diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0bca39 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.sql.gz +.netbox +.python-version +docker-compose.override.yml +*.pem +configuration/* +!configuration/configuration.py +!configuration/extra.py +configuration/ldap/* +!configuration/ldap/extra.py +!configuration/ldap/ldap_config.py +!configuration/logging.py +!configuration/plugins.py +super-linter.log +env/netbox.env +env/postgres.env +yadisk diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5cb51d2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,117 @@ +ARG FROM +FROM ${FROM} AS builder + +COPY --from=ghcr.io/astral-sh/uv:0.11 /uv /usr/local/bin/ +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get update -qq \ + && apt-get upgrade \ + --yes -qq --no-install-recommends \ + && apt-get install \ + --yes -qq --no-install-recommends \ + build-essential \ + ca-certificates \ + libldap-dev \ + libpq-dev \ + libsasl2-dev \ + libssl-dev \ + libxml2-dev \ + libxmlsec1-1 \ + libxmlsec1-dev \ + libxmlsec1-openssl1 \ + libxslt-dev \ + pkg-config \ + python3-dev \ + && /usr/local/bin/uv venv /opt/netbox/venv + +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 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. + # That's why we just replace it in the original requirements.txt. + sed -i -e 's/social-auth-core/social-auth-core\[all\]/g' /requirements.txt && \ + # The same is true for 'django-storages' + sed -i -e 's/django-storages/django-storages\[azure,boto3,dropbox,google,libcloud,sftp\]/g' /requirements.txt && \ + /usr/local/bin/uv pip install \ + -r /requirements.txt \ + -r /requirements-container.txt + +### +# Main stage +### + +ARG FROM +FROM ${FROM} AS main + +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get update -qq \ + && apt-get upgrade \ + --yes -qq --no-install-recommends \ + && apt-get install \ + --yes -qq --no-install-recommends \ + bzip2 \ + ca-certificates \ + curl \ + libldap-common \ + libpq5 \ + libxmlsec1-openssl1 \ + openssh-client \ + openssl \ + python3 \ + tini \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /usr/local/bin/uv /usr/local/bin/ +COPY --from=builder /opt/netbox/venv /opt/netbox/venv + +ARG NETBOX_PATH +COPY ${NETBOX_PATH} /opt/netbox +# Copy the modified 'requirements*.txt' files, to have the files actually used during installation +COPY --from=builder /requirements.txt /requirements-container.txt /opt/netbox/ + +COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py +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 docker/super_user.py /opt/netbox/super_user.py +COPY configuration/ /etc/netbox/config/ +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 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/zensical build \ + --config-file /opt/netbox/mkdocs.yml \ + && 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", "--" ] + +CMD [ "/opt/netbox/docker-entrypoint.sh", "/opt/netbox/launch-netbox.sh" ] + +LABEL netbox.original-tag="" \ + netbox.git-branch="" \ + netbox.git-ref="" \ + netbox.git-url="" \ +# See https://github.com/opencontainers/image-spec/blob/master/annotations.md#pre-defined-annotation-keys + org.opencontainers.image.created="" \ + org.opencontainers.image.title="NetBox Docker" \ + org.opencontainers.image.description="A container based distribution of NetBox, the free and open IPAM and DCIM solution." \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.authors="The netbox-docker contributors." \ + org.opencontainers.image.vendor="The netbox-docker contributors." \ + org.opencontainers.image.url="https://github.com/netbox-community/netbox-docker" \ + org.opencontainers.image.documentation="https://github.com/netbox-community/netbox-docker/wiki" \ + org.opencontainers.image.source="https://github.com/netbox-community/netbox-docker.git" \ + org.opencontainers.image.revision="" \ + org.opencontainers.image.version="" diff --git a/Dockerfile-Plugins b/Dockerfile-Plugins index bfe4aa4..f7fd9ec 100644 --- a/Dockerfile-Plugins +++ b/Dockerfile-Plugins @@ -4,4 +4,8 @@ RUN /usr/local/bin/uv pip install -r /opt/netbox/plugin_requirements.txt # Скопируйте конфигурацию, если она у вас есть COPY configuration/configuration.py /etc/netbox/config/configuration.py COPY configuration/plugins.py /etc/netbox/config/plugins.py +#RUN python /opt/netbox/netbox/manage.py collectstatic --no-input + +# Временно устанавливаем SECRET_KEY для сборки (будет заменён при запуске) +ENV SECRET_KEY=temp-key-for-building-only-50-characters-minimum!!!!! RUN python /opt/netbox/netbox/manage.py collectstatic --no-input diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e454a52 --- /dev/null +++ b/LICENSE @@ -0,0 +1,178 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..a3071eb --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,19 @@ +# Maintainers of _NetBox Docker_ + +This file lists all currently recognized maintainers of the _NetBox Docker_ project in alphabetical order: + +- @cimnine +- @tobiasge + +## Stepping Down + +Every maintainer is a volunteer and may step down as maintainer at any time without providing any reason. +To make this explicit, the maintainer is asked to update this file. + +The last maintainer stepping down is asked to archive the project on GitHub to indicate that the project is no longer maintained. + +## Signing up + +Everyone is welcome to sign up as maintainer by creating a PR and add their own username to the list. +The current maintainers shall discuss the application. +They may turn down an application if they don't feel confident that the new maintainer is a positive addition. diff --git a/PRINCIPALS.md b/PRINCIPALS.md new file mode 100644 index 0000000..f2d0355 --- /dev/null +++ b/PRINCIPALS.md @@ -0,0 +1,71 @@ +# Development, Maintenance and Community Principals for _NetBox Docker_ + +These principals shall guide the development and the maintenance of _NetBox Docker_. + +## Basic principals + +This project is maintained on voluntary basis. +Everyone is asked to respect that. + +This means, that … + +- … sometimes features are not implemented as fast as one might like -- or not at all. +- … sometimes nobody is looking at bugs, or they are not fixed as fast as one might like -- or not at all. +- … sometimes PRs are not reviewed for an extended period. + +Everyone is welcome to provide improvements and bugfixes to the benefit of everyone else. + +## Development Principals + +The goal of the _NetBox Docker_ project is to provide a container to run the basic NetBox project. +The container should feel like a native container -- as if it were provided by NetBox itself: + +- Configuration via environment variables where feasible. + - Except: Whenever a complex type such as a `dict` is required as value of a configuration setting, + then it shall not be provided through an environment variable. +- Configuration of secrets via secret files. +- Log output to standard out (STDOUT/`&1`) / standard error (STDERR/`&2`). +- Volumes for data and cache directories. + - Otherwise, no mounts shall be necessary. +- Runs a non-root user by default. +- One process / role for each instance. + +The container generally does not provide more features than the basic NetBox project itself provides. +It may provide additional Python dependencies than the upstream project, +so that all configurable features of NetBox can be used in the container without further modification. +The container may provide helpers, so that it feels and behaves like a native container. + +The container does not bundle any community plugins. + +## Maintenance Principals + +The main goals of maintaining _NetBox Docker_ are: + +- Keeping the project at a high quality level. +- Keeping the maintenance effort minimal. +- Coordinating development efforts. + +The following guidelines help us to achieve these goals: + +- As many maintenance tasks as possible shall be automated or scripted. +- All manual tasks must be documented. +- All changes are reviewed by at least one maintainer. + - Changes of maintainers are reviewed by at least one other maintainer. + (Except if there's only one maintainer left.) +- The infrastructure beyond what GitHub provides shall be kept to a minimum. + - On request, every maintainer shall get access to infrastructure that is beyond GitHub + (at the time of writing that's _Docker Hub_ and _Quay_ in particular). + +## Community Principals + +This project is developed by the NetBox community for the NetBox community. +We welcome contributions, as long as they are in line with the principals above. + +The maintainers of NetBox Docker are not the support team. +The community is expected to help each other out. + +Always remember: +Behind every screen (or screen-reader) on the other end is a fellow human. +Be nice and respectful, thankful for help, +and value ideas and contributions, +even when they don't fit the goals. diff --git a/README.md b/README.md new file mode 100644 index 0000000..74b1ad7 --- /dev/null +++ b/README.md @@ -0,0 +1,163 @@ +# netbox-docker + +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/netbox-community/netbox-docker)][github-release] +[![GitHub stars](https://img.shields.io/github/stars/netbox-community/netbox-docker)][github-stargazers] +![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/netbox-community/netbox-docker) +![Github release workflow](https://img.shields.io/github/actions/workflow/status/netbox-community/netbox-docker/release.yml?branch=release) +![Docker Pulls](https://img.shields.io/docker/pulls/netboxcommunity/netbox) +[![GitHub license](https://img.shields.io/github/license/netbox-community/netbox-docker)][netbox-docker-license] + +[The GitHub repository][netbox-docker-github] houses the components needed to build NetBox as a container. +Images are built regularly using the code in that repository +and are pushed to [Docker Hub][netbox-dockerhub], +[Quay.io][netbox-quayio] and [GitHub Container Registry][netbox-ghcr]. +_NetBox Docker_ is a project developed and maintained by the _NetBox_ community. + +Do you have any questions? +Before opening an issue on GitHub, +please join [our Slack][netbox-docker-slack] +and ask for help in the [`#netbox-docker`][netbox-docker-slack-channel] channel, +or start a new [GitHub Discussion][github-discussions]. + +[github-stargazers]: https://github.com/netbox-community/netbox-docker/stargazers +[github-release]: https://github.com/netbox-community/netbox-docker/releases +[netbox-dockerhub]: https://hub.docker.com/r/netboxcommunity/netbox/ +[netbox-quayio]: https://quay.io/repository/netboxcommunity/netbox +[netbox-ghcr]: https://github.com/netbox-community/netbox-docker/pkgs/container/netbox +[netbox-docker-github]: https://github.com/netbox-community/netbox-docker/ +[netbox-docker-slack]: https://join.slack.com/t/netdev-community/shared_invite/zt-mtts8g0n-Sm6Wutn62q_M4OdsaIycrQ +[netbox-docker-slack-channel]: https://netdev-community.slack.com/archives/C01P0GEVBU7 +[netbox-slack-channel]: https://netdev-community.slack.com/archives/C01P0FRSXRV +[netbox-docker-license]: https://github.com/netbox-community/netbox-docker/blob/release/LICENSE +[github-discussions]: https://github.com/netbox-community/netbox-docker/discussions + +## Quickstart + +To get _NetBox Docker_ up and running run the following commands. +There is a more complete [_Getting Started_ guide on our wiki][wiki-getting-started] which explains every step. + +```bash +git clone -b release https://github.com/netbox-community/netbox-docker.git +cd netbox-docker +# Copy the example override file +cp docker-compose.override.yml.example docker-compose.override.yml +# Read and edit the file to your liking +docker compose pull +docker compose up +``` + +The whole application will be available after a few minutes. +Open the URL `http://0.0.0.0:8000/` in a web-browser. +You should see the NetBox homepage. + +To create the first admin user run this command: + +```bash +docker compose exec netbox /opt/netbox/netbox/manage.py createsuperuser +``` + +If you need to restart Netbox from an empty database often, +you can also set the `SUPERUSER_*` variables in your `docker-compose.override.yml`. + +[wiki-getting-started]: https://github.com/netbox-community/netbox-docker/wiki/Getting-Started + +## Container Image Tags + +New container images are built and published automatically every ~24h. + +> We recommend to use either the `vX.Y.Z-a.b.c` tags or the `vX.Y-a.b.c` tags in production! + +- `vX.Y.Z-a.b.c`, `vX.Y-a.b.c`: + These are release builds containing _NetBox version_ `vX.Y.Z`. + They contain the support files of _NetBox Docker version_ `a.b.c`. + You must use _NetBox Docker version_ `a.b.c` to guarantee the compatibility. + These images are automatically built from [the corresponding releases of NetBox][netbox-releases]. +- `latest-a.b.c`: + These are release builds, containing the latest stable version of NetBox. + They contain the support files of _NetBox Docker version_ `a.b.c`. + You must use _NetBox Docker version_ `a.b.c` to guarantee the compatibility. +- `snapshot-a.b.c`: + These are prerelease builds. + They contain the support files of _NetBox Docker version_ `a.b.c`. + You must use _NetBox Docker version_ `a.b.c` to guarantee the compatibility. + These images are automatically built from the [`main` branch of NetBox][netbox-main]. + +For each of the above tag, there is an extra tag: + +- `vX.Y.Z`, `vX.Y`: + This is the same version as `vX.Y.Z-a.b.c` (or `vX.Y-a.b.c`, respectively). +- `latest` + This is the same version as `latest-a.b.c`. + It always points to the latest version of _NetBox Docker_. +- `snapshot` + This is the same version as `snapshot-a.b.c`. + It always points to the latest version of _NetBox Docker_. + +[netbox-releases]: https://github.com/netbox-community/netbox/releases +[netbox-main]: https://github.com/netbox-community/netbox/tree/main + +## Documentation + +Please refer [to our wiki on GitHub][netbox-docker-wiki] for further information on how to use the NetBox Docker image properly. +The wiki covers advanced topics such as using files for secrets, configuring TLS, deployment to Kubernetes, monitoring and configuring LDAP. + +Our wiki is a community effort. +Feel free to correct errors, update outdated information or provide additional guides and insights. + +[netbox-docker-wiki]: https://github.com/netbox-community/netbox-docker/wiki/ + +## Getting Help + +Feel free to ask questions in our [GitHub Community][netbox-community] +or [join our Slack][netbox-docker-slack] and ask [in our channel `#netbox-docker`][netbox-docker-slack-channel], +which is free to use and where there are almost always people online that can help you. + +If you need help with using NetBox or developing for it or against it's API +you may find [the `#netbox` channel][netbox-slack-channel] on the same Slack instance very helpful. + +[netbox-community]: https://github.com/netbox-community/netbox-docker/discussions + +## Dependencies + +This project relies only on _Docker_ and _docker-compose_ meeting these requirements: + +- The _Docker version_ must be at least `20.10.10`. +- The _containerd version_ must be at least `1.5.6`. +- The _docker-compose version_ must be at least `1.28.0`. + +To check the version installed on your system run `docker --version` and `docker compose version`. + +## Updating + +Please read [the release notes][releases] carefully when updating to a new image version. +Note that the version of the NetBox Docker container image must stay in sync with the version of the Git repository. + +If you update for the first time, be sure [to follow our _How To Update NetBox Docker_ guide in the wiki][netbox-docker-wiki-updating]. + +[releases]: https://github.com/netbox-community/netbox-docker/releases +[netbox-docker-wiki-updating]: https://github.com/netbox-community/netbox-docker/wiki/Updating + +## Rebuilding the Image + +`./build.sh` can be used to rebuild the container image. +See `./build.sh --help` for more information or `./build-latest.sh` for an example. + +For more details on custom builds [consult our wiki][netbox-docker-wiki-build]. + +[netbox-docker-wiki-build]: https://github.com/netbox-community/netbox-docker/wiki/Build + +## Tests + +We have a test script. +It runs NetBox's own unit tests and ensures that NetBox starts: + +```bash +IMAGE=docker.io/netboxcommunity/netbox:latest ./test.sh +``` + +## Support + +This repository is currently maintained by the community. +The community is expected to help each other. + +Please consider sponsoring the maintainers of this project. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..32f3eaa --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +5.0.1 \ No newline at end of file diff --git a/actionlint.yml b/actionlint.yml new file mode 100644 index 0000000..28fadea --- /dev/null +++ b/actionlint.yml @@ -0,0 +1,5 @@ +--- +paths: + .github/workflows/**/*.{yml,yaml}: + ignore: + - ".*ubuntu-24.04-arm.*" diff --git a/backup.sh b/backup.sh new file mode 100755 index 0000000..c09a7e0 --- /dev/null +++ b/backup.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# Скрипт резервного копирования NetBox Docker с отправкой на Яндекс.Диск + +BACKUP_DIR="/opt/netbox/backups" +YADISK_MOUNT="/opt/netbox/yadisk" +DATE=$(date +%Y%m%d_%H%M%S) + +# Создаем папку для временных бэкапов (с правами текущего пользователя) +mkdir -p ${BACKUP_DIR} + +echo "🔄 [$(date)] Начинаем резервное копирование NetBox..." + +# Проверяем, примонтирован ли Яндекс.Диск +if ! mountpoint -q ${YADISK_MOUNT}; then + echo " ❌ ОШИБКА: Яндекс.Диск не примонтирован!" + exit 1 +fi + +# Создаём папку для бэкапов на Яндекс.Диске (если её нет) +mkdir -p ${YADISK_MOUNT}/netbox-backups 2>/dev/null + +# 1. Бэкап базы данных PostgreSQL +echo " → Бэкап базы данных..." +cd /opt/netbox +docker compose exec -T postgres sh -c 'pg_dump -cU $POSTGRES_USER $POSTGRES_DB' | gzip > ${BACKUP_DIR}/netbox_db_${DATE}.sql.gz + +if [ $? -eq 0 ] && [ -s ${BACKUP_DIR}/netbox_db_${DATE}.sql.gz ]; then + echo " ✅ База данных: $(du -h ${BACKUP_DIR}/netbox_db_${DATE}.sql.gz | cut -f1)" +else + echo " ❌ Ошибка бэкапа базы данных!" +fi + +# 2. Бэкап медиа-файлов +echo " → Бэкап медиа-файлов..." +docker compose exec -T netbox tar czf - -C /opt/netbox/netbox/media . 2>/dev/null > ${BACKUP_DIR}/netbox_media_${DATE}.tar.gz + +if [ -s ${BACKUP_DIR}/netbox_media_${DATE}.tar.gz ]; then + echo " ✅ Медиа-файлы: $(du -h ${BACKUP_DIR}/netbox_media_${DATE}.tar.gz | cut -f1)" +fi + +# 3. Бэкап .env файлов +echo " → Бэкап .env файлов..." +tar czf ${BACKUP_DIR}/netbox_env_${DATE}.tar.gz -C /opt/netbox env/ 2>/dev/null + +if [ -s ${BACKUP_DIR}/netbox_env_${DATE}.tar.gz ]; then + echo " ✅ .env файлы: $(du -h ${BACKUP_DIR}/netbox_env_${DATE}.tar.gz | cut -f1)" +fi + +# 4. Копирование на Яндекс.Диск +echo "☁️ Копирование на Яндекс.Диск..." + +# Копируем файлы, если они существуют +for file in netbox_db_${DATE}.sql.gz netbox_media_${DATE}.tar.gz netbox_env_${DATE}.tar.gz; do + if [ -f ${BACKUP_DIR}/${file} ]; then + cp ${BACKUP_DIR}/${file} ${YADISK_MOUNT}/netbox-backups/ + echo " ✅ ${file}" + fi +done + +# 5. Очистка старых бэкапов +# Локально: оставляем 7 дней +find ${BACKUP_DIR} -type f -name "netbox_*" -mtime +7 -delete 2>/dev/null + +# На Яндекс.Диске: оставляем 30 дней +find ${YADISK_MOUNT}/netbox-backups -type f -name "netbox_*" -mtime +30 -delete 2>/dev/null + +echo "✅ [$(date)] Резервное копирование завершено!" + +# Показываем список созданных бэкапов +echo "" +echo "📊 Созданные бэкапы:" +ls -lh ${BACKUP_DIR}/netbox_*_${DATE}.* 2>/dev/null || echo " (нет файлов)" diff --git a/backups/netbox_env_20260517_062410.tar.gz b/backups/netbox_env_20260517_062410.tar.gz new file mode 100644 index 0000000..9b30685 Binary files /dev/null and b/backups/netbox_env_20260517_062410.tar.gz differ diff --git a/backups/netbox_env_20260517_064207.tar.gz b/backups/netbox_env_20260517_064207.tar.gz new file mode 100644 index 0000000..9b30685 Binary files /dev/null and b/backups/netbox_env_20260517_064207.tar.gz differ diff --git a/backups/netbox_env_20260518_020001.tar.gz b/backups/netbox_env_20260518_020001.tar.gz new file mode 100644 index 0000000..9b30685 Binary files /dev/null and b/backups/netbox_env_20260518_020001.tar.gz differ diff --git a/backups/netbox_env_20260519_020001.tar.gz b/backups/netbox_env_20260519_020001.tar.gz new file mode 100644 index 0000000..9b30685 Binary files /dev/null and b/backups/netbox_env_20260519_020001.tar.gz differ diff --git a/backups/netbox_env_20260520_020001.tar.gz b/backups/netbox_env_20260520_020001.tar.gz new file mode 100644 index 0000000..9b30685 Binary files /dev/null and b/backups/netbox_env_20260520_020001.tar.gz differ diff --git a/backups/netbox_env_20260521_020001.tar.gz b/backups/netbox_env_20260521_020001.tar.gz new file mode 100644 index 0000000..9b30685 Binary files /dev/null and b/backups/netbox_env_20260521_020001.tar.gz differ diff --git a/backups/netbox_media_20260517_062410.tar.gz b/backups/netbox_media_20260517_062410.tar.gz new file mode 100644 index 0000000..9fa36e6 Binary files /dev/null and b/backups/netbox_media_20260517_062410.tar.gz differ diff --git a/backups/netbox_media_20260517_064207.tar.gz b/backups/netbox_media_20260517_064207.tar.gz new file mode 100644 index 0000000..9fa36e6 Binary files /dev/null and b/backups/netbox_media_20260517_064207.tar.gz differ diff --git a/backups/netbox_media_20260518_020001.tar.gz b/backups/netbox_media_20260518_020001.tar.gz new file mode 100644 index 0000000..9fa36e6 Binary files /dev/null and b/backups/netbox_media_20260518_020001.tar.gz differ diff --git a/backups/netbox_media_20260519_020001.tar.gz b/backups/netbox_media_20260519_020001.tar.gz new file mode 100644 index 0000000..9fa36e6 Binary files /dev/null and b/backups/netbox_media_20260519_020001.tar.gz differ diff --git a/backups/netbox_media_20260520_020001.tar.gz b/backups/netbox_media_20260520_020001.tar.gz new file mode 100644 index 0000000..9fa36e6 Binary files /dev/null and b/backups/netbox_media_20260520_020001.tar.gz differ diff --git a/backups/netbox_media_20260521_020001.tar.gz b/backups/netbox_media_20260521_020001.tar.gz new file mode 100644 index 0000000..9fa36e6 Binary files /dev/null and b/backups/netbox_media_20260521_020001.tar.gz differ diff --git a/build-functions/check-commands.sh b/build-functions/check-commands.sh new file mode 100644 index 0000000..e998490 --- /dev/null +++ b/build-functions/check-commands.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +NEEDED_COMMANDS="curl jq docker skopeo" +for c in $NEEDED_COMMANDS; do + if ! command -v "$c" &>/dev/null; then + echo "⚠️ '$c' is not installed. Can't proceed with build." + exit 1 + fi +done diff --git a/build-functions/get-public-image-config.sh b/build-functions/get-public-image-config.sh new file mode 100644 index 0000000..0a19c3f --- /dev/null +++ b/build-functions/get-public-image-config.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +check_if_tags_exists() { + local image=$1 + local tag=$2 + skopeo list-tags "docker://$image" | jq -r ".Tags | contains([\"$tag\"])" +} + +get_image_label() { + local label=$1 + local image=$2 + skopeo inspect "docker://$image" | jq -r ".Labels[\"$label\"]" +} + +get_image_last_layer() { + local image=$1 + skopeo inspect "docker://$image" | jq -r ".Layers | last" +} diff --git a/build-functions/gh-functions.sh b/build-functions/gh-functions.sh new file mode 100644 index 0000000..4c04dc0 --- /dev/null +++ b/build-functions/gh-functions.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +### +# A regular echo, that only prints if ${GH_ACTION} is defined. +### +gh_echo() { + if [ -n "${GH_ACTION}" ]; then + echo "${@}" + fi +} + +### +# Prints the output to the file defined in ${GITHUB_ENV}. +# Only executes if ${GH_ACTION} is defined. +# Example Usage: gh_env "FOO_VAR=bar_value" +### +gh_env() { + if [ -n "${GH_ACTION}" ]; then + echo "${@}" >>"${GITHUB_ENV}" + fi +} + +### +# Prints the output to the file defined in ${GITHUB_OUTPUT}. +# Only executes if ${GH_ACTION} is defined. +# Example Usage: gh_env "FOO_VAR=bar_value" +### +gh_out() { + if [ -n "${GH_ACTION}" ]; then + echo "${@}" >>"$GITHUB_OUTPUT" + fi +} diff --git a/build-latest.sh b/build-latest.sh new file mode 100755 index 0000000..0cc6c38 --- /dev/null +++ b/build-latest.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# Builds the latest released version + +# Check if we have everything needed for the build +source ./build-functions/check-commands.sh + +source ./build-functions/gh-functions.sh + +echo "▶️ $0 $*" + +CURL_ARGS=( + --silent +) + +### +# Checking for the presence of GITHUB_TOKEN +### +if [ -n "${GITHUB_TOKEN}" ]; then + echo "🗝 Performing authenticated Github API calls." + CURL_ARGS+=( + --header "Authorization: Bearer ${GITHUB_TOKEN}" + ) +else + echo "🕶 Performing unauthenticated Github API calls. This might result in lower Github rate limits!" +fi + +### +# Checking if PRERELEASE is either unset, 'true' or 'false' +### +if [ -n "${PRERELEASE}" ] && + { [ "${PRERELEASE}" != "true" ] && [ "${PRERELEASE}" != "false" ]; }; then + + if [ -z "${DEBUG}" ]; then + echo "⚠️ PRERELEASE must be either unset, 'true' or 'false', but was '${PRERELEASE}'!" + exit 1 + else + echo "⚠️ Would exit here with code '1', but DEBUG is enabled." + fi +fi + +### +# Calling Github to get the latest version +### +ORIGINAL_GITHUB_REPO="netbox-community/netbox" +GITHUB_REPO="${GITHUB_REPO-$ORIGINAL_GITHUB_REPO}" +URL_RELEASES="https://api.github.com/repos/${GITHUB_REPO}/releases" + +# Composing the JQ commans to extract the most recent version number +JQ_LATEST="group_by(.prerelease) | .[] | sort_by(.published_at) | reverse | .[0] | select(.prerelease==${PRERELEASE-false}) | .tag_name" + +CURL="curl" + +# Querying the Github API to fetch the most recent version number +VERSION=$($CURL "${CURL_ARGS[@]}" "${URL_RELEASES}" | jq -r "${JQ_LATEST}" 2>/dev/null) + +### +# Check if the prerelease version is actually higher than stable version +### +if [ "${PRERELEASE}" == "true" ]; then + JQ_STABLE="group_by(.prerelease) | .[] | sort_by(.published_at) | reverse | .[0] | select(.prerelease==false) | .tag_name" + STABLE_VERSION=$($CURL "${CURL_ARGS[@]}" "${URL_RELEASES}" | jq -r "${JQ_STABLE}" 2>/dev/null) + + MAJOR_STABLE=$(expr "${STABLE_VERSION}" : 'v\([0-9]\+\)') + MINOR_STABLE=$(expr "${STABLE_VERSION}" : 'v[0-9]\+\.\([0-9]\+\)') + MAJOR_UNSTABLE=$(expr "${VERSION}" : 'v\([0-9]\+\)') + MINOR_UNSTABLE=$(expr "${VERSION}" : 'v[0-9]\+\.\([0-9]\+\)') + + if { + [ "${MAJOR_STABLE}" -eq "${MAJOR_UNSTABLE}" ] && + [ "${MINOR_STABLE}" -ge "${MINOR_UNSTABLE}" ] + } || [ "${MAJOR_STABLE}" -gt "${MAJOR_UNSTABLE}" ]; then + + echo "❎ Latest unstable version '${VERSION}' is not higher than the latest stable version '$STABLE_VERSION'." + if [ -z "$DEBUG" ]; then + gh_out "skipped=true" + exit 0 + else + echo "⚠️ Would exit here with code '0', but DEBUG is enabled." + fi + fi +fi + +# shellcheck disable=SC2068 +./build.sh "${VERSION}" $@ +exit $? diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..903c38e --- /dev/null +++ b/build.sh @@ -0,0 +1,460 @@ +#!/bin/bash +# Clones the NetBox repository with git from Github and builds the Dockerfile + +echo "▶️ $0 $*" + +set -e + +if [ "${1}x" == "x" ] || [ "${1}" == "--help" ] || [ "${1}" == "-h" ]; then + _BOLD=$(tput bold) + _GREEN=$(tput setaf 2) + _CYAN=$(tput setaf 6) + _CLEAR=$(tput sgr0) + + cat < [--push] + +branch The branch or tag to build. Required. +--push Pushes the built container image to the registry. + +${_BOLD}You can use the following ENV variables to customize the build:${_CLEAR} + +SRC_ORG Which fork of netbox to use (i.e. github.com/\${SRC_ORG}/\${SRC_REPO}). + ${_GREEN}Default:${_CLEAR} netbox-community + +SRC_REPO The name of the repository to use (i.e. github.com/\${SRC_ORG}/\${SRC_REPO}). + ${_GREEN}Default:${_CLEAR} netbox + +URL Where to fetch the code from. + Must be a git repository. Can be private. + ${_GREEN}Default:${_CLEAR} https://github.com/\${SRC_ORG}/\${SRC_REPO}.git + +NETBOX_PATH The path where netbox will be checkout out. + Must not be outside of the netbox-docker repository (because of Docker)! + ${_GREEN}Default:${_CLEAR} .netbox + +SKIP_GIT If defined, git is not invoked and \${NETBOX_PATH} will not be altered. + This may be useful, if you are manually managing the NETBOX_PATH. + ${_GREEN}Default:${_CLEAR} undefined + +TAG The version part of the image tag. + ${_GREEN}Default:${_CLEAR} + When =main: snapshot + Else: same as + +IMAGE_NAMES The names used for the image including the registry + Used for tagging the image. + ${_GREEN}Default:${_CLEAR} docker.io/netboxcommunity/netbox + ${_CYAN}Example:${_CLEAR} 'docker.io/netboxcommunity/netbox quay.io/netboxcommunity/netbox' + +DOCKER_TAG The name of the tag which is applied to the image. + Useful for pushing into another registry than hub.docker.com. + ${_GREEN}Default:${_CLEAR} \${DOCKER_REGISTRY}/\${DOCKER_ORG}/\${DOCKER_REPO}:\${TAG} + +DOCKER_SHORT_TAG The name of the short tag which is applied to the + image. This is used to tag all patch releases to their + containing version e.g. v2.5.1 -> v2.5 + ${_GREEN}Default:${_CLEAR} \${DOCKER_REGISTRY}/\${DOCKER_ORG}/\${DOCKER_REPO}:. + +DOCKERFILE The name of Dockerfile to use. + ${_GREEN}Default:${_CLEAR} Dockerfile + +DOCKER_FROM The base image to use. + ${_GREEN}Default:${_CLEAR} 'ubuntu:26.04' + +BUILDX_PLATFORM + Specifies the platform(s) to build the image for. + ${_CYAN}Example:${_CLEAR} 'linux/amd64,linux/arm64' + ${_GREEN}Default:${_CLEAR} 'linux/amd64' + +BUILDX_BUILDER_NAME + If defined, the image build will be assigned to the given builder. + If you specify this variable, make sure that the builder exists. + If this value is not defined, a new builx builder with the directory name of the + current directory (i.e. '$(basename "${PWD}")') is created." + ${_CYAN}Example:${_CLEAR} 'clever_lovelace' + ${_GREEN}Default:${_CLEAR} undefined + +BUILDX_REMOVE_BUILDER + If defined (and only if BUILDX_BUILDER_NAME is undefined), + then the buildx builder created by this script will be removed after use. + This is useful if you build NetBox Docker on an automated system that does + not manage the builders for you. + ${_CYAN}Example:${_CLEAR} 'on' + ${_GREEN}Default:${_CLEAR} undefined + +HTTP_PROXY The proxy to use for http requests. + ${_CYAN}Example:${_CLEAR} http://proxy.domain.tld:3128 + ${_GREEN}Default:${_CLEAR} undefined + +NO_PROXY Comma-separated list of domain extensions proxy should not be used for. + ${_CYAN}Example:${_CLEAR} .domain1.tld,.domain2.tld + ${_GREEN}Default:${_CLEAR} undefined + +DEBUG If defined, the script does not stop when certain checks are unsatisfied. + ${_GREEN}Default:${_CLEAR} undefined + +DRY_RUN Prints all build statements instead of running them. + ${_GREEN}Default:${_CLEAR} undefined + +GH_ACTION If defined, special 'echo' statements are enabled that set the + following environment variables in Github Actions: + - FINAL_DOCKER_TAG: The final value of the DOCKER_TAG env variable + ${_GREEN}Default:${_CLEAR} undefined + +CHECK_ONLY Only checks if the build is needed and sets the GH Action output. + ${_GREEN}Default:${_CLEAR} undefined + +${_BOLD}Examples:${_CLEAR} + +${0} main + This will fetch the latest 'main' branch, build a Docker Image and tag it + 'netboxcommunity/netbox:snapshot'. + +${0} v4.2.0 + This will fetch the 'v4.2.0' tag, build a Docker Image and tag it + 'netboxcommunity/netbox:v4.2.0' and 'netboxcommunity/netbox:v4.2'. + +${0} feature + This will fetch the 'feature' branch, build a Docker Image and tag it + 'netboxcommunity/netbox:feature'. + +SRC_ORG=cimnine ${0} feature-x + This will fetch the 'feature-x' branch from https://github.com/cimnine/netbox.git, + build a Docker Image and tag it 'netboxcommunity/netbox:feature-x'. + +SRC_ORG=cimnine DOCKER_ORG=cimnine ${0} feature-x + This will fetch the 'feature-x' branch from https://github.com/cimnine/netbox.git, + build a Docker Image and tag it 'cimnine/netbox:feature-x'. +END_OF_HELP + + if [ "${1}x" == "x" ]; then + exit 1 + else + exit 0 + fi +fi + +# Check if we have everything needed for the build +source ./build-functions/check-commands.sh +# Load all build functions +source ./build-functions/get-public-image-config.sh +source ./build-functions/gh-functions.sh + +IMAGE_NAMES="${IMAGE_NAMES-docker.io/netboxcommunity/netbox}" +IFS=' ' read -ra IMAGE_NAMES <<<"${IMAGE_NAMES}" + +### +# Enabling dry-run mode +### +if [ -z "${DRY_RUN}" ]; then + DRY="" +else + echo "⚠️ DRY_RUN MODE ON ⚠️" + DRY="echo" +fi + +gh_echo "::group::⤵️ Fetching the NetBox source code" + +### +# Variables for fetching the NetBox source +### +SRC_ORG="${SRC_ORG-netbox-community}" +SRC_REPO="${SRC_REPO-netbox}" +NETBOX_BRANCH="${1}" +URL="${URL-https://github.com/${SRC_ORG}/${SRC_REPO}.git}" +NETBOX_PATH="${NETBOX_PATH-.netbox}" + +### +# Fetching the NetBox source +### +if [ "${2}" != "--push-only" ] && [ -z "${SKIP_GIT}" ]; then + REMOTE_EXISTS=$(git ls-remote --heads --tags "${URL}" "${NETBOX_BRANCH}" | wc -l) + if [ "${REMOTE_EXISTS}" == "0" ]; then + echo "❌ Remote branch '${NETBOX_BRANCH}' not found in '${URL}'; Nothing to do" + gh_out "skipped=true" + exit 0 + fi + echo "🌐 Checking out '${NETBOX_BRANCH}' of NetBox from the url '${URL}' into '${NETBOX_PATH}'" + if [ ! -d "${NETBOX_PATH}" ]; then + $DRY git clone -q --depth 10 -b "${NETBOX_BRANCH}" "${URL}" "${NETBOX_PATH}" + fi + + ( + $DRY cd "${NETBOX_PATH}" + # shellcheck disable=SC2030 + if [ -n "${HTTP_PROXY}" ]; then + git config http.proxy "${HTTP_PROXY}" + fi + + $DRY git remote set-url origin "${URL}" + $DRY git fetch -qp --depth 10 origin "${NETBOX_BRANCH}" + $DRY git checkout -qf FETCH_HEAD + $DRY git prune + ) + echo "✅ Checked out NetBox" +fi + +gh_echo "::endgroup::" +gh_echo "::group::🧮 Calculating Values" + +### +# Determining the value for DOCKERFILE +# and checking whether it exists +### +DOCKERFILE="${DOCKERFILE-Dockerfile}" +if [ ! -f "${DOCKERFILE}" ]; then + echo "🚨 The Dockerfile ${DOCKERFILE} doesn't exist." + + if [ -z "${DEBUG}" ]; then + exit 1 + else + echo "⚠️ Would exit here with code '1', but DEBUG is enabled." + fi +fi + +### +# Determining the value for DOCKER_FROM +### +if [ -z "$DOCKER_FROM" ]; then + DOCKER_FROM="docker.io/ubuntu:26.04" +fi + +### +# Variables for labelling the docker image +### +BUILD_DATE="$(date -u '+%Y-%m-%dT%H:%M:%S+00:00')" + +if [ -d ".git" ] && [ -z "${SKIP_GIT}" ]; then + GIT_REF="$(git rev-parse HEAD)" +fi + +# Read the project version from the `VERSION` file and trim it, see https://stackoverflow.com/a/3232433/172132 +PROJECT_VERSION="${PROJECT_VERSION-$(sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' VERSION)}" + +# Get the Git information from the netbox directory +if [ -d "${NETBOX_PATH}/.git" ] && [ -z "${SKIP_GIT}" ]; then + NETBOX_GIT_REF=$( + cd "${NETBOX_PATH}" + git rev-parse HEAD + ) + NETBOX_GIT_BRANCH=$( + cd "${NETBOX_PATH}" + git rev-parse --abbrev-ref HEAD + ) + NETBOX_GIT_URL=$( + cd "${NETBOX_PATH}" + git remote get-url origin + ) +fi + +### +# Variables for tagging the docker image +### +DOCKER_REGISTRY="${DOCKER_REGISTRY-docker.io}" +DOCKER_ORG="${DOCKER_ORG-netboxcommunity}" +DOCKER_REPO="${DOCKER_REPO-netbox}" +case "${NETBOX_BRANCH}" in +main) + TAG="${TAG-snapshot}" + ;; +*) + TAG="${TAG-$NETBOX_BRANCH}" + ;; +esac + +### +# composing the final TARGET_DOCKER_TAG +### +TARGET_DOCKER_TAG="${DOCKER_TAG-${TAG}}" +TARGET_DOCKER_TAG_PROJECT="${TARGET_DOCKER_TAG}-${PROJECT_VERSION}" + +### +# composing the additional DOCKER_SHORT_TAG, +# i.e. "v4.2.0" becomes "v4.2", +# which is only relevant for version tags +# Also let "latest" follow the highest version +### +if [[ "${TAG}" =~ ^v([0-9]+)\.([0-9]+)\.[0-9]+$ ]]; then + MAJOR=${BASH_REMATCH[1]} + MINOR=${BASH_REMATCH[2]} + + TARGET_DOCKER_SHORT_TAG="${DOCKER_SHORT_TAG-v${MAJOR}.${MINOR}}" + TARGET_DOCKER_LATEST_TAG="latest" + TARGET_DOCKER_SHORT_TAG_PROJECT="${TARGET_DOCKER_SHORT_TAG}-${PROJECT_VERSION}" + TARGET_DOCKER_LATEST_TAG_PROJECT="${TARGET_DOCKER_LATEST_TAG}-${PROJECT_VERSION}" +fi + +IMAGE_NAME_TAGS=() +for IMAGE_NAME in "${IMAGE_NAMES[@]}"; do + IMAGE_NAME_TAGS+=("${IMAGE_NAME}:${TARGET_DOCKER_TAG}") + IMAGE_NAME_TAGS+=("${IMAGE_NAME}:${TARGET_DOCKER_TAG_PROJECT}") +done +if [ -n "${TARGET_DOCKER_SHORT_TAG}" ]; then + for IMAGE_NAME in "${IMAGE_NAMES[@]}"; do + IMAGE_NAME_TAGS+=("${IMAGE_NAME}:${TARGET_DOCKER_SHORT_TAG}") + IMAGE_NAME_TAGS+=("${IMAGE_NAME}:${TARGET_DOCKER_SHORT_TAG_PROJECT}") + IMAGE_NAME_TAGS+=("${IMAGE_NAME}:${TARGET_DOCKER_LATEST_TAG}") + IMAGE_NAME_TAGS+=("${IMAGE_NAME}:${TARGET_DOCKER_LATEST_TAG_PROJECT}") + done +fi + +FINAL_DOCKER_TAG="${IMAGE_NAME_TAGS[0]}" +gh_env "FINAL_DOCKER_TAG=${IMAGE_NAME_TAGS[0]}" + +### +# Checking if the build is necessary, +# meaning build only if one of those values changed: +# - a new tag is being created +# - base image digest +# - netbox git ref (Label: netbox.git-ref) +# - netbox-docker git ref (Label: org.opencontainers.image.revision) +### +# Load information from registry (only for first registry in "IMAGE_NAMES") +SHOULD_BUILD="false" +BUILD_REASON="" +if [ -z "${GH_ACTION}" ]; then + # Assuming non Github builds should always proceed + SHOULD_BUILD="true" + BUILD_REASON="${BUILD_REASON} interactive" +elif [ "false" == "$(check_if_tags_exists "${IMAGE_NAMES[0]}" "$TARGET_DOCKER_TAG")" ]; then + SHOULD_BUILD="true" + BUILD_REASON="${BUILD_REASON} newtag" +else + echo "Checking labels for '${FINAL_DOCKER_TAG}'" + BASE_LAST_LAYER=$(get_image_last_layer "${DOCKER_FROM}") + OLD_BASE_LAST_LAYER=$(get_image_label netbox.last-base-image-layer "${FINAL_DOCKER_TAG}") + NETBOX_GIT_REF_OLD=$(get_image_label netbox.git-ref "${FINAL_DOCKER_TAG}") + GIT_REF_OLD=$(get_image_label org.opencontainers.image.revision "${FINAL_DOCKER_TAG}") + + if [ "${BASE_LAST_LAYER}" != "${OLD_BASE_LAST_LAYER}" ]; then + SHOULD_BUILD="true" + BUILD_REASON="${BUILD_REASON} ubuntu" + fi + if [ "${NETBOX_GIT_REF}" != "${NETBOX_GIT_REF_OLD}" ]; then + SHOULD_BUILD="true" + BUILD_REASON="${BUILD_REASON} netbox" + fi + if [ "${GIT_REF}" != "${GIT_REF_OLD}" ]; then + SHOULD_BUILD="true" + BUILD_REASON="${BUILD_REASON} netbox-docker" + fi +fi + +if [ "${SHOULD_BUILD}" != "true" ]; then + echo "Build skipped because sources didn't change" + gh_out "skipped=true" + exit 0 # Nothing to do -> exit +else + gh_out "skipped=false" +fi +gh_echo "::endgroup::" + +if [ "${CHECK_ONLY}" = "true" ]; then + echo "Only check if build needed was requested. Exiting" + exit 0 +fi + +### +# Build the image +### +gh_echo "::group::🏗 Building the image" +### +# Composing all arguments for `docker build` +### +DOCKER_BUILD_ARGS=( + --pull + --target main + -f "${DOCKERFILE}" +) +for IMAGE_NAME in "${IMAGE_NAME_TAGS[@]}"; do + DOCKER_BUILD_ARGS+=(-t "${IMAGE_NAME}") +done + +# --label +DOCKER_BUILD_ARGS+=( + --label "netbox.original-tag=${TARGET_DOCKER_TAG_PROJECT}" + --label "org.opencontainers.image.created=${BUILD_DATE}" + --label "org.opencontainers.image.version=${PROJECT_VERSION}" +) +if [ -d ".git" ] && [ -z "${SKIP_GIT}" ]; then + DOCKER_BUILD_ARGS+=( + --label "org.opencontainers.image.revision=${GIT_REF}" + ) +fi +if [ -d "${NETBOX_PATH}/.git" ] && [ -z "${SKIP_GIT}" ]; then + DOCKER_BUILD_ARGS+=( + --label "netbox.git-branch=${NETBOX_GIT_BRANCH}" + --label "netbox.git-ref=${NETBOX_GIT_REF}" + --label "netbox.git-url=${NETBOX_GIT_URL}" + ) +fi +if [ -n "${BUILD_REASON}" ]; then + BUILD_REASON=$(sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' <<<"$BUILD_REASON") + DOCKER_BUILD_ARGS+=(--label "netbox.build-reason=${BUILD_REASON}") + DOCKER_BUILD_ARGS+=(--label "netbox.last-base-image-layer=${BASE_LAST_LAYER}") +fi + +# --build-arg +DOCKER_BUILD_ARGS+=(--build-arg "NETBOX_PATH=${NETBOX_PATH}") + +if [ -n "${DOCKER_FROM}" ]; then + DOCKER_BUILD_ARGS+=(--build-arg "FROM=${DOCKER_FROM}") +fi +# shellcheck disable=SC2031 +if [ -n "${HTTP_PROXY}" ]; then + DOCKER_BUILD_ARGS+=(--build-arg "http_proxy=${HTTP_PROXY}") +fi +if [ -n "${HTTPS_PROXY}" ]; then + DOCKER_BUILD_ARGS+=(--build-arg "https_proxy=${HTTPS_PROXY}") +fi +if [ -n "${NO_PROXY}" ]; then + DOCKER_BUILD_ARGS+=(--build-arg "no_proxy=${NO_PROXY}") +fi + +DOCKER_BUILD_ARGS+=(--platform "${BUILDX_PLATFORM-linux/amd64}") +if [ "${2}" == "--push" ]; then + # output type=docker does not work with pushing + DOCKER_BUILD_ARGS+=( + --output=type=image + --push + ) +else + DOCKER_BUILD_ARGS+=( + --output=type=docker + ) +fi + +### +# Building the docker image +### +if [ -z "${BUILDX_BUILDER_NAME}" ]; then + BUILDX_BUILDER_NAME="$(basename "${PWD}")" +fi +if ! docker buildx ls | grep --quiet --word-regexp "${BUILDX_BUILDER_NAME}"; then + echo "👷 Creating new Buildx Builder '${BUILDX_BUILDER_NAME}'" + $DRY docker buildx create --name "${BUILDX_BUILDER_NAME}" + BUILDX_BUILDER_CREATED="yes" +fi + +echo "🐳 Building the Docker image '${TARGET_DOCKER_TAG_PROJECT}'." +echo " Build reason set to: ${BUILD_REASON}" +$DRY docker buildx \ + --builder "${BUILDX_BUILDER_NAME}" \ + build \ + "${DOCKER_BUILD_ARGS[@]}" \ + . +echo "✅ Finished building the Docker images" +gh_echo "::endgroup::" # End group for Build + +gh_echo "::group::🏗 Image Labels" +echo "🔎 Inspecting labels on '${IMAGE_NAME_TAGS[0]}'" +$DRY docker inspect "${IMAGE_NAME_TAGS[0]}" --format "{{json .Config.Labels}}" | jq +gh_echo "::endgroup::" + +gh_echo "::group::🏗 Clean up" +if [ -n "${BUILDX_REMOVE_BUILDER}" ] && [ "${BUILDX_BUILDER_CREATED}" == "yes" ]; then + echo "👷 Removing Buildx Builder '${BUILDX_BUILDER_NAME}'" + $DRY docker buildx rm "${BUILDX_BUILDER_NAME}" +fi +gh_echo "::endgroup::" diff --git a/configuration/configuration.py b/configuration/configuration.py new file mode 100644 index 0000000..41de069 --- /dev/null +++ b/configuration/configuration.py @@ -0,0 +1,393 @@ +#### +## We recommend to not edit this file. +## Create separate files to overwrite the settings. +## See `extra.py` as an example. +#### + +import re +from collections.abc import Callable +from os import environ +from os.path import abspath, dirname, join +from typing import Any + +# For reference see https://docs.netbox.dev/en/stable/configuration/ +# Based on https://github.com/netbox-community/netbox/blob/develop/netbox/netbox/configuration_example.py + +### +# NetBox-Docker Helper functions +### + + +# Read secret from file +def _read_secret(secret_name: str, default: str | None = None) -> str | None: + try: + with open('/run/secrets/' + secret_name, encoding='utf-8') as f: + return f.readline().strip() + except OSError: + return default + + +# If the `map_fn` isn't defined, then the value that is read from the environment (or the default value if not found) is returned. +# If the `map_fn` is defined, then `map_fn` is invoked and the value (that was read from the environment or the default value if not found) +# is passed to it as a parameter. The value returned from `map_fn` is then the return value of this function. +# The `map_fn` is not invoked, if the value (that was read from the environment or the default value if not found) is None. +def _environ_get_and_map( + variable_name: str, default: str | None = None, map_fn: Callable[[str], Any | None] | None = None +) -> Any | None: + env_value = environ.get(variable_name, default) + + if env_value == None: + return env_value + + if not map_fn: + return env_value + + return map_fn(env_value) + + +_AS_BOOL = lambda value: value.lower() == 'true' +_AS_INT = lambda value: int(value) +_AS_LIST = lambda value: list(filter(None, value.split(' '))) + +_BASE_DIR = dirname(dirname(abspath(__file__))) + +######################### +# # +# Required settings # +# # +######################### + +# This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write +# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name. +# +# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local'] +ALLOWED_HOSTS = environ.get('ALLOWED_HOSTS', '*').split(' ') +# ensure that '*' or 'localhost' is always in ALLOWED_HOSTS (needed for health checks) +if '*' not in ALLOWED_HOSTS and 'localhost' not in ALLOWED_HOSTS: + ALLOWED_HOSTS.append('localhost') + +# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters: +# https://docs.djangoproject.com/en/stable/ref/settings/#databases +DATABASES = { + 'default': { + 'NAME': environ.get('DB_NAME', 'netbox'), # Database name + 'USER': environ.get('DB_USER', ''), # PostgreSQL username + 'PASSWORD': _read_secret('db_password', environ.get('DB_PASSWORD', '')), + # PostgreSQL password + 'HOST': environ.get('DB_HOST', 'localhost'), # Database server + 'PORT': environ.get('DB_PORT', ''), # Database port (leave blank for default) + 'OPTIONS': {'sslmode': environ.get('DB_SSLMODE', 'prefer')}, + # Database connection SSLMODE + 'CONN_MAX_AGE': _environ_get_and_map('DB_CONN_MAX_AGE', '300', _AS_INT), + # Max database connection age + 'DISABLE_SERVER_SIDE_CURSORS': _environ_get_and_map('DB_DISABLE_SERVER_SIDE_CURSORS', 'False', _AS_BOOL), + # Disable the use of server-side cursors transaction pooling + } +} + +# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate +# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended +# to use two separate database IDs. +REDIS = { + 'tasks': { + 'HOST': environ.get('REDIS_HOST', 'localhost'), + 'PORT': _environ_get_and_map('REDIS_PORT', 6379, _AS_INT), + 'SENTINELS': [ + tuple(uri.split(':')) for uri in _environ_get_and_map('REDIS_SENTINELS', '', _AS_LIST) if uri != '' + ], + 'SENTINEL_SERVICE': environ.get('REDIS_SENTINEL_SERVICE', 'default'), + 'SENTINEL_TIMEOUT': _environ_get_and_map('REDIS_SENTINEL_TIMEOUT', 10, _AS_INT), + 'USERNAME': environ.get('REDIS_USERNAME', ''), + 'PASSWORD': _read_secret('redis_password', environ.get('REDIS_PASSWORD', '')), + 'DATABASE': _environ_get_and_map('REDIS_DATABASE', 0, _AS_INT), + 'SSL': _environ_get_and_map('REDIS_SSL', 'False', _AS_BOOL), + 'INSECURE_SKIP_TLS_VERIFY': _environ_get_and_map('REDIS_INSECURE_SKIP_TLS_VERIFY', 'False', _AS_BOOL), + }, + 'caching': { + 'HOST': environ.get('REDIS_CACHE_HOST', environ.get('REDIS_HOST', 'localhost')), + 'PORT': _environ_get_and_map('REDIS_CACHE_PORT', environ.get('REDIS_PORT', '6379'), _AS_INT), + 'SENTINELS': [ + tuple(uri.split(':')) for uri in _environ_get_and_map('REDIS_CACHE_SENTINELS', '', _AS_LIST) if uri != '' + ], + 'SENTINEL_SERVICE': environ.get( + 'REDIS_CACHE_SENTINEL_SERVICE', environ.get('REDIS_SENTINEL_SERVICE', 'default') + ), + 'USERNAME': environ.get('REDIS_CACHE_USERNAME', environ.get('REDIS_USERNAME', '')), + 'PASSWORD': _read_secret( + 'redis_cache_password', environ.get('REDIS_CACHE_PASSWORD', environ.get('REDIS_PASSWORD', '')) + ), + 'DATABASE': _environ_get_and_map('REDIS_CACHE_DATABASE', '1', _AS_INT), + 'SSL': _environ_get_and_map('REDIS_CACHE_SSL', environ.get('REDIS_SSL', 'False'), _AS_BOOL), + 'INSECURE_SKIP_TLS_VERIFY': _environ_get_and_map( + 'REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY', environ.get('REDIS_INSECURE_SKIP_TLS_VERIFY', 'False'), _AS_BOOL + ), + }, +} + +# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file. +# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and +# symbols. NetBox will not run without this defined. For more information, see +# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY +SECRET_KEY = _read_secret('secret_key', environ.get('SECRET_KEY', '')) + +API_TOKEN_PEPPERS = {} +if api_token_pepper := _read_secret('api_token_pepper_1', environ.get('API_TOKEN_PEPPER_1', '')): + API_TOKEN_PEPPERS.update({1: api_token_pepper}) + + +######################### +# # +# Optional settings # +# # +######################### + +# # Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of +# # application errors (assuming correct email settings are provided). +# ADMINS = [ +# # ['John Doe', 'jdoe@example.com'], +# ] + +if 'ALLOWED_URL_SCHEMES' in environ: + ALLOWED_URL_SCHEMES = _environ_get_and_map('ALLOWED_URL_SCHEMES', None, _AS_LIST) + +# Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same +# content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP. +if 'BANNER_TOP' in environ: + BANNER_TOP = environ.get('BANNER_TOP', None) +if 'BANNER_BOTTOM' in environ: + BANNER_BOTTOM = environ.get('BANNER_BOTTOM', None) + +# Text to include on the login page above the login form. HTML is allowed. +if 'BANNER_LOGIN' in environ: + BANNER_LOGIN = environ.get('BANNER_LOGIN', None) + +# Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90) +if 'CHANGELOG_RETENTION' in environ: + CHANGELOG_RETENTION = _environ_get_and_map('CHANGELOG_RETENTION', None, _AS_INT) + +# Maximum number of days to retain job results (scripts and reports). Set to 0 to retain job results in the database indefinitely. (Default: 90) +if 'JOB_RETENTION' in environ: + JOB_RETENTION = _environ_get_and_map('JOB_RETENTION', None, _AS_INT) +# JOBRESULT_RETENTION was renamed to JOB_RETENTION in the v3.5.0 release of NetBox. For backwards compatibility, map JOBRESULT_RETENTION to JOB_RETENTION +elif 'JOBRESULT_RETENTION' in environ: + JOB_RETENTION = _environ_get_and_map('JOBRESULT_RETENTION', None, _AS_INT) + +# API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be +# allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or +# CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers +CORS_ORIGIN_ALLOW_ALL = _environ_get_and_map('CORS_ORIGIN_ALLOW_ALL', 'False', _AS_BOOL) +CORS_ORIGIN_WHITELIST = _environ_get_and_map('CORS_ORIGIN_WHITELIST', 'https://localhost', _AS_LIST) +CORS_ORIGIN_REGEX_WHITELIST = [re.compile(r) for r in _environ_get_and_map('CORS_ORIGIN_REGEX_WHITELIST', '', _AS_LIST)] + +# Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal +# sensitive information about your installation. Only enable debugging while performing testing. +# Never enable debugging on a production system. +DEBUG = _environ_get_and_map('DEBUG', 'False', _AS_BOOL) + +# This parameter serves as a safeguard to prevent some potentially dangerous behavior, +# such as generating new database schema migrations. +# Set this to True only if you are actively developing the NetBox code base. +DEVELOPER = _environ_get_and_map('DEVELOPER', 'False', _AS_BOOL) + +# Email settings +EMAIL = { + 'SERVER': environ.get('EMAIL_SERVER', 'localhost'), + 'PORT': _environ_get_and_map('EMAIL_PORT', 25, _AS_INT), + 'USERNAME': environ.get('EMAIL_USERNAME', ''), + 'PASSWORD': _read_secret('email_password', environ.get('EMAIL_PASSWORD', '')), + 'USE_SSL': _environ_get_and_map('EMAIL_USE_SSL', 'False', _AS_BOOL), + 'USE_TLS': _environ_get_and_map('EMAIL_USE_TLS', 'False', _AS_BOOL), + 'SSL_CERTFILE': environ.get('EMAIL_SSL_CERTFILE', ''), + 'SSL_KEYFILE': environ.get('EMAIL_SSL_KEYFILE', ''), + 'TIMEOUT': _environ_get_and_map('EMAIL_TIMEOUT', 10, _AS_INT), # seconds + 'FROM_EMAIL': environ.get('EMAIL_FROM', ''), +} + +# Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table +# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True. +if 'ENFORCE_GLOBAL_UNIQUE' in environ: + ENFORCE_GLOBAL_UNIQUE = _environ_get_and_map('ENFORCE_GLOBAL_UNIQUE', None, _AS_BOOL) + +# By default, netbox sends census reporting data using a single HTTP request each time a worker starts. +# This data enables the project maintainers to estimate how many NetBox deployments exist and track the adoption of new versions over time. +# The only data reported by this function are the NetBox version, Python version, and a pseudorandom unique identifier. +# To opt out of census reporting, set CENSUS_REPORTING_ENABLED to False. +if 'CENSUS_REPORTING_ENABLED' in environ: + CENSUS_REPORTING_ENABLED = _environ_get_and_map('CENSUS_REPORTING_ENABLED', None, _AS_BOOL) + +# Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and +# by anonymous users. List models in the form `.`. Add '*' to this list to exempt all models. +EXEMPT_VIEW_PERMISSIONS = _environ_get_and_map('EXEMPT_VIEW_PERMISSIONS', '', _AS_LIST) + +# HTTP proxies NetBox should use when sending outbound HTTP requests (e.g. for webhooks). +HTTP_PROXIES = { + 'http': environ.get('HTTP_PROXY', None), + 'https': environ.get('HTTPS_PROXY', None), +} + +# IP addresses recognized as internal to the system. The debugging toolbar will be available only to clients accessing +# NetBox from an internal IP. +INTERNAL_IPS = _environ_get_and_map('INTERNAL_IPS', '127.0.0.1 ::1', _AS_LIST) + +# Enable GraphQL API. +if 'GRAPHQL_ENABLED' in environ: + GRAPHQL_ENABLED = _environ_get_and_map('GRAPHQL_ENABLED', None, _AS_BOOL) + +# # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: +# # https://docs.djangoproject.com/en/stable/topics/logging/ +# LOGGING = {} + +# Automatically reset the lifetime of a valid session upon each authenticated request. Enables users to remain +# authenticated to NetBox indefinitely. +LOGIN_PERSISTENCE = _environ_get_and_map('LOGIN_PERSISTENCE', 'False', _AS_BOOL) + +# When enabled, only authenticated users are permitted to access any part of NetBox. +# Disabling this will allow unauthenticated users to access most areas of NetBox (but not make any changes). +LOGIN_REQUIRED = _environ_get_and_map('LOGIN_REQUIRED', 'True', _AS_BOOL) + +# The length of time (in seconds) for which a user will remain logged into the web UI before being prompted to +# re-authenticate. (Default: 1209600 [14 days]) +LOGIN_TIMEOUT = _environ_get_and_map('LOGIN_TIMEOUT', 1209600, _AS_INT) + +# Setting this to True will display a "maintenance mode" banner at the top of every page. +if 'MAINTENANCE_MODE' in environ: + MAINTENANCE_MODE = _environ_get_and_map('MAINTENANCE_MODE', None, _AS_BOOL) + +# Maps provider +if 'MAPS_URL' in environ: + MAPS_URL = environ.get('MAPS_URL', None) + +# An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g. +# "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request +# all objects by specifying "?limit=0". +if 'MAX_PAGE_SIZE' in environ: + MAX_PAGE_SIZE = _environ_get_and_map('MAX_PAGE_SIZE', None, _AS_INT) + +# The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that +# the default value of this setting is derived from the installed location. +MEDIA_ROOT = environ.get('MEDIA_ROOT', join(_BASE_DIR, 'media')) + +# Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics' +METRICS_ENABLED = _environ_get_and_map('METRICS_ENABLED', 'False', _AS_BOOL) + +# Determine how many objects to display per page within a list. (Default: 50) +if 'PAGINATE_COUNT' in environ: + PAGINATE_COUNT = _environ_get_and_map('PAGINATE_COUNT', None, _AS_INT) + +# # Enable installed plugins. Add the name of each plugin to the list. +# PLUGINS = [] + +# # Plugins configuration settings. These settings are used by various plugins that the user may have installed. +# # Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings. +# PLUGINS_CONFIG = { +# } + +# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to +# prefer IPv4 instead. +if 'PREFER_IPV4' in environ: + PREFER_IPV4 = _environ_get_and_map('PREFER_IPV4', None, _AS_BOOL) + +# The default value for the amperage field when creating new power feeds. +if 'POWERFEED_DEFAULT_AMPERAGE' in environ: + POWERFEED_DEFAULT_AMPERAGE = _environ_get_and_map('POWERFEED_DEFAULT_AMPERAGE', None, _AS_INT) + +# The default value (percentage) for the max_utilization field when creating new power feeds. +if 'POWERFEED_DEFAULT_MAX_UTILIZATION' in environ: + POWERFEED_DEFAULT_MAX_UTILIZATION = _environ_get_and_map('POWERFEED_DEFAULT_MAX_UTILIZATION', None, _AS_INT) + +# The default value for the voltage field when creating new power feeds. +if 'POWERFEED_DEFAULT_VOLTAGE' in environ: + POWERFEED_DEFAULT_VOLTAGE = _environ_get_and_map('POWERFEED_DEFAULT_VOLTAGE', None, _AS_INT) + +# Rack elevation size defaults, in pixels. For best results, the ratio of width to height should be roughly 10:1. +if 'RACK_ELEVATION_DEFAULT_UNIT_HEIGHT' in environ: + RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = _environ_get_and_map('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', None, _AS_INT) +if 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH' in environ: + RACK_ELEVATION_DEFAULT_UNIT_WIDTH = _environ_get_and_map('RACK_ELEVATION_DEFAULT_UNIT_WIDTH', None, _AS_INT) + +# Remote authentication support +REMOTE_AUTH_AUTO_CREATE_GROUPS = _environ_get_and_map('REMOTE_AUTH_AUTO_CREATE_GROUPS', 'False', _AS_BOOL) +REMOTE_AUTH_AUTO_CREATE_USER = _environ_get_and_map('REMOTE_AUTH_AUTO_CREATE_USER', 'False', _AS_BOOL) +REMOTE_AUTH_BACKEND = _environ_get_and_map('REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend', _AS_LIST) +REMOTE_AUTH_DEFAULT_GROUPS = _environ_get_and_map('REMOTE_AUTH_DEFAULT_GROUPS', '', _AS_LIST) +# REMOTE_AUTH_DEFAULT_PERMISSIONS = {} # dicts can't be configured via environment variables. See extra.py instead. +REMOTE_AUTH_ENABLED = _environ_get_and_map('REMOTE_AUTH_ENABLED', 'False', _AS_BOOL) +REMOTE_AUTH_GROUP_HEADER = _environ_get_and_map('REMOTE_AUTH_GROUP_HEADER', 'HTTP_REMOTE_USER_GROUP') +REMOTE_AUTH_GROUP_SEPARATOR = _environ_get_and_map('REMOTE_AUTH_GROUP_SEPARATOR', '|') +REMOTE_AUTH_GROUP_SYNC_ENABLED = _environ_get_and_map('REMOTE_AUTH_GROUP_SYNC_ENABLED', 'False', _AS_BOOL) +REMOTE_AUTH_HEADER = environ.get('REMOTE_AUTH_HEADER', 'HTTP_REMOTE_USER') +REMOTE_AUTH_USER_EMAIL = environ.get('REMOTE_AUTH_USER_EMAIL', 'HTTP_REMOTE_USER_EMAIL') +REMOTE_AUTH_USER_FIRST_NAME = environ.get('REMOTE_AUTH_USER_FIRST_NAME', 'HTTP_REMOTE_USER_FIRST_NAME') +REMOTE_AUTH_USER_LAST_NAME = environ.get('REMOTE_AUTH_USER_LAST_NAME', 'HTTP_REMOTE_USER_LAST_NAME') +REMOTE_AUTH_SUPERUSER_GROUPS = _environ_get_and_map('REMOTE_AUTH_SUPERUSER_GROUPS', '', _AS_LIST) +REMOTE_AUTH_SUPERUSERS = _environ_get_and_map('REMOTE_AUTH_SUPERUSERS', '', _AS_LIST) +REMOTE_AUTH_STAFF_GROUPS = _environ_get_and_map('REMOTE_AUTH_STAFF_GROUPS', '', _AS_LIST) +REMOTE_AUTH_STAFF_USERS = _environ_get_and_map('REMOTE_AUTH_STAFF_USERS', '', _AS_LIST) +# SSO Configuration +SOCIAL_AUTH_OKTA_OPENIDCONNECT_KEY = environ.get('SOCIAL_AUTH_OKTA_OPENIDCONNECT_KEY') +SOCIAL_AUTH_OKTA_OPENIDCONNECT_SECRET = _read_secret( + 'okta_openidconnect_secret', environ.get('SOCIAL_AUTH_OKTA_OPENIDCONNECT_SECRET', '') +) +SOCIAL_AUTH_OKTA_OPENIDCONNECT_API_URL = environ.get('SOCIAL_AUTH_OKTA_OPENIDCONNECT_API_URL') +SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = environ.get('SOCIAL_AUTH_GOOGLE_OAUTH2_KEY') +SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = _read_secret( + 'google_oauth2_secret', environ.get('SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET', '') +) + +# OIDC Configuration +SOCIAL_AUTH_OIDC_OIDC_ENDPOINT = environ.get('SOCIAL_AUTH_OIDC_OIDC_ENDPOINT') +SOCIAL_AUTH_OIDC_KEY = environ.get('SOCIAL_AUTH_OIDC_KEY') +SOCIAL_AUTH_OIDC_SECRET = _read_secret('oidc_secret', environ.get('SOCIAL_AUTH_OIDC_SECRET', '')) +SOCIAL_AUTH_OIDC_SCOPE = _environ_get_and_map('SOCIAL_AUTH_OIDC_SCOPE', '', _AS_LIST) +LOGOUT_REDIRECT_URL = environ.get('LOGOUT_REDIRECT_URL', '/') +SOCIAL_AUTH_OIDC_JWT_ALGORITHMS = _environ_get_and_map('SOCIAL_AUTH_OIDC_JWT_ALGORITHMS', 'RS256', _AS_LIST) + +# This repository is used to check whether there is a new release of NetBox available. Set to None to disable the +# version check or use the URL below to check for release in the official NetBox repository. +RELEASE_CHECK_URL = environ.get('RELEASE_CHECK_URL', None) +# RELEASE_CHECK_URL = 'https://api.github.com/repos/netbox-community/netbox/releases' + +# Maximum execution time for background tasks, in seconds. +RQ_DEFAULT_TIMEOUT = _environ_get_and_map('RQ_DEFAULT_TIMEOUT', 300, _AS_INT) + +# The name to use for the csrf token cookie. +CSRF_COOKIE_NAME = environ.get('CSRF_COOKIE_NAME', 'csrftoken') + +# Cross-Site-Request-Forgery-Attack settings. If Netbox is sitting behind a reverse proxy, you might need to set the CSRF_TRUSTED_ORIGINS flag. +# Django 4.0 requires to specify the URL Scheme in this setting. An example environment variable could be specified like: +# CSRF_TRUSTED_ORIGINS=https://demo.netbox.dev http://demo.netbox.dev +CSRF_TRUSTED_ORIGINS = _environ_get_and_map('CSRF_TRUSTED_ORIGINS', '', _AS_LIST) + +# The name to use for the session cookie. +SESSION_COOKIE_NAME = environ.get('SESSION_COOKIE_NAME', 'sessionid') + +# If true, the `includeSubDomains` directive will be included in the HTTP Strict Transport Security (HSTS) header. +# This directive instructs the browser to apply the HSTS policy to all subdomains of the current domain. +SECURE_HSTS_INCLUDE_SUBDOMAINS = _environ_get_and_map('SECURE_HSTS_INCLUDE_SUBDOMAINS', 'False', _AS_BOOL) + +# If true, the `preload` directive will be included in the HTTP Strict Transport Security (HSTS) header. +# This directive instructs the browser to preload the site in HTTPS. Browsers that use the HSTS preload list will force the +# site to be accessed via HTTPS even if the user types HTTP in the address bar. +SECURE_HSTS_PRELOAD = _environ_get_and_map('SECURE_HSTS_PRELOAD', 'False', _AS_BOOL) + +# If set to a non-zero integer value, the SecurityMiddleware sets the HTTP Strict Transport Security (HSTS) header on all +# responses that do not already have it. This will instruct the browser that the website must be accessed via HTTPS, +# blocking any HTTP request. +SECURE_HSTS_SECONDS = _environ_get_and_map('SECURE_HSTS_SECONDS', 0, _AS_INT) + +# If true, all non-HTTPS requests will be automatically redirected to use HTTPS. +SECURE_SSL_REDIRECT = _environ_get_and_map('SECURE_SSL_REDIRECT', 'False', _AS_BOOL) + +# By default, NetBox will store session data in the database. Alternatively, a file path can be specified here to use +# local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only +# database access.) Note that the user as which NetBox runs must have read and write permissions to this path. +SESSION_FILE_PATH = environ.get('SESSION_FILE_PATH', environ.get('SESSIONS_ROOT', None)) + +# Time zone (default: UTC) +TIME_ZONE = environ.get('TIME_ZONE', 'UTC') + +# If true disables miscellaneous functionality which depends on access to the Internet. +ISOLATED_DEPLOYMENT = _environ_get_and_map('ISOLATED_DEPLOYMENT', 'False', _AS_BOOL) + +# Enables or disables the NetBox Copilot agent globally. When enabled, users can opt to toggle the agent individually. +COPILOT_ENABLED = _environ_get_and_map('COPILOT_ENABLED', 'True', _AS_BOOL) diff --git a/configuration/extra.py b/configuration/extra.py new file mode 100644 index 0000000..3a10ea2 --- /dev/null +++ b/configuration/extra.py @@ -0,0 +1,56 @@ +#### +## This file contains extra configuration options that can't be configured +## directly through environment variables. +#### + +## Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of +## application errors (assuming correct email settings are provided). +# ADMINS = [ +# # ['John Doe', 'jdoe@example.com'], +# ] + + +## URL schemes that are allowed within links in NetBox +# ALLOWED_URL_SCHEMES = ( +# 'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp', +# ) + +## Enable installed plugins. Add the name of each plugin to the list. +# from netbox.configuration.configuration import PLUGINS +# PLUGINS.append('my_plugin') + +## Plugins configuration settings. These settings are used by various plugins that the user may have installed. +## Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings. +# from netbox.configuration.configuration import PLUGINS_CONFIG +# PLUGINS_CONFIG['my_plugin'] = { +# 'foo': 'bar', +# 'buzz': 'bazz' +# } + + +## Remote authentication support +# REMOTE_AUTH_DEFAULT_PERMISSIONS = {} + + +## By default uploaded media is stored on the local filesystem. Using Django-storages is also supported. Provide the +## class path of the storage driver and any configuration options in STORAGES. For example: +# STORAGES = { +# 'default': { +# 'BACKEND': 'storages.backends.s3boto3.S3Boto3Storage', +# 'OPTIONS': { +# 'access_key': 'Key ID', +# 'secret_key': 'Secret', +# 'bucket_name': 'netbox', +# 'region_name': 'us-west-1', +# } +# }, +# 'staticfiles': { +# 'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage', +# } +# } + + +## This file can contain arbitrary Python code, e.g.: +# from datetime import datetime +# now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") +# BANNER_TOP = f'This instance started on {now}.' diff --git a/configuration/logging.py b/configuration/logging.py new file mode 100644 index 0000000..d786768 --- /dev/null +++ b/configuration/logging.py @@ -0,0 +1,55 @@ +# # Remove first comment(#) on each line to implement this working logging example. +# # Add LOGLEVEL environment variable to netbox if you use this example & want a different log level. +# from os import environ + +# # Set LOGLEVEL in netbox.env or docker-compose.overide.yml to override a logging level of INFO. +# LOGLEVEL = environ.get('LOGLEVEL', 'INFO') + +# LOGGING = { + +# 'version': 1, +# 'disable_existing_loggers': False, +# 'formatters': { +# 'verbose': { +# 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', +# 'style': '{', +# }, +# 'simple': { +# 'format': '{levelname} {message}', +# 'style': '{', +# }, +# }, +# 'filters': { +# 'require_debug_false': { +# '()': 'django.utils.log.RequireDebugFalse', +# }, +# }, +# 'handlers': { +# 'console': { +# 'level': LOGLEVEL, +# 'filters': ['require_debug_false'], +# 'class': 'logging.StreamHandler', +# 'formatter': 'simple' +# }, +# 'mail_admins': { +# 'level': 'ERROR', +# 'class': 'django.utils.log.AdminEmailHandler', +# 'filters': ['require_debug_false'] +# } +# }, +# 'loggers': { +# 'django': { +# 'handlers': ['console'], +# 'propagate': True, +# }, +# 'django.request': { +# 'handlers': ['mail_admins'], +# 'level': 'ERROR', +# 'propagate': False, +# }, +# 'django_auth_ldap': { +# 'handlers': ['console',], +# 'level': LOGLEVEL, +# } +# } +# } diff --git a/configuration/plugins.py b/configuration/plugins.py new file mode 100644 index 0000000..9c3e458 --- /dev/null +++ b/configuration/plugins.py @@ -0,0 +1,196 @@ +# Add your plugins and plugin settings here. +# Of course uncomment this file out. + +# To learn how to build images with your required plugins +# See https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins + +# PLUGINS = ["netbox_bgp"] +PLUGINS = [ + "netbox_prometheus_sd", + "netbox_lists", + "netbox_inventory", + "netbox_interface_synchronization", + "netbox_documents", + "netbox_contract", + "netbox_data_flows", + "netbox_config_diff", + "netbox_attachments", + "netbox_topology_views", + "netbox_reorder_rack", + "netbox_secrets", + "netbox_qrcode", + "netbox_floorplan" +] + +# PLUGINS_CONFIG = { +# "netbox_bgp": { +# ADD YOUR SETTINGS HERE +# } +# } +PLUGINS_CONFIG = { + "netbox_prometheus_sd": { +# https://github.com/FlxPeters/netbox-plugin-prometheus-sd +# The plugin has not further plugin configuration. + }, + "netbox_lists": { +# https://github.com/devon-mar/netbox-lists + # Return IPs as /32 or /128. + # Default: True + "as_cidr": True, + # For services without any explicit IPs configured, + # use the primary IPs of the associated device/vm. + # Default: True + "service_primary_ips": True, + # Summarize responses + "summarize": True, + # A list of attributes for the devices-vms-attrs endpoint + # + # Attributes will be joined with "__" in the returned object. + # eg. ("primary_ip", "address") -> primary_ip__address + "devices_vms_attrs": [ + ("id",), + ("name",), + ("role", "slug"), + ("platform", "slug"), + ("primary_ip", "address"), + ("tags",), + ], + # Tuple/list of attributes to use for Prometheus VM SD target. Defaults are shown. + # + # If all attributes return None, the device's name will be used. + "prometheus_vm_sd_target": ( + # For a custom field + # ("cf", "fqdn"), + # If this returns none, try Name. + ("primary_ip", "address", "ip"), + ("name",), # not necessary + ), + # Dictionary of label to VM attribute for Prometheus VM SD. Defaults are shown. + "prometheus_vm_sd_labels": { + "__meta_netbox_id": ("id",), + "__meta_netbox_name": ("name",), + "__meta_netbox_status": ("status",), + "__meta_netbox_cluster_name": ("cluster", "name"), + "__meta_netbox_site_name": ("site", "name"), + "__meta_netbox_role_name": ("role", "name"), + "__meta_netbox_platform_name": ("platform", "name"), + "__meta_netbox_primary_ip": ("primary_ip", "address", "ip"), + "__meta_netbox_primary_ip4": ("primary_ip4", "address", "ip"), + "__meta_netbox_primary_ip6": ("primary_ip6", "address", "ip"), + # A custom field. Will be an empty string if None. + # "__meta_netbox_fqdn": ("cf", "fqdn"), + }, + # Tuple/list of attributes to use for Prometheus device SD target. Defaults are shown. + # + # If all attributes return None, the device's name will be used. + "prometheus_device_sd_target": ( + # For a custom field + # ("cf", "fqdn"), + ("primary_ip", "address", "ip"), + ("name",), # not necessary + ), + # Dictionary of label to device attribute for Prometheus device SD. Defaults are shown. + "prometheus_device_sd_labels": { + "__meta_netbox_id": ("id",), + "__meta_netbox_name": ("name",), + "__meta_netbox_status": ("status",), + "__meta_netbox_site_name": ("site", "name"), + "__meta_netbox_platform_name": ("platform", "name"), + "__meta_netbox_primary_ip": ("primary_ip", "address", "ip"), + "__meta_netbox_primary_ip4": ("primary_ip4", "address", "ip"), + "__meta_netbox_primary_ip6": ("primary_ip6", "address", "ip"), + "__meta_netbox_serial": ("serial",), + # A custom field. Will be an empty string if None. + # "__meta_netbox_fqdn": ("cf", "fqdn"), + }, + # Tuple/list of attributes to use for Prometheus IP address SD target. Defaults are shown. + # + # If all attributes return None, the address in CIDR format will be used. + "prometheus_ipaddress_sd_target": ( + ("address", "ip"), + ), + # Dictionary of label to IP address attribute for Prometheus ip address SD. Defaults are shown. + "prometheus_ipaddress_sd_labels": { + "__meta_netbox_id": ("id",), + "__meta_netbox_role": ("role",), + "__meta_netbox_dns_name": ("dns_name",), + "__meta_netbox_status": ("status",), + # For addresses assigned to interfaces + #"__meta_netbox_device": ("assigned_object", "device", "name"), + #"__meta_netbox_interface": ("assigned_object", "name"), + }, + }, + "netbox_inventory": { +# https://github.com/ArnesSI/netbox-inventory + # Example settings below, see "Available settings" + # in README.md for all possible settings + "used_status_name": "used", + "stored_status_name": "stored", + "sync_hardware_serial_asset_tag": True, + }, + "netbox-interface-synchronization": { +# https://github.com/NetTech2001/netbox-interface-synchronization + 'exclude_virtual_interfaces': True + }, + "netbox-documents": { +# https://github.com/jasonyates/netbox-documents + #All settings are optional -- the defaults are shown below + # Enable the global navigation menu + 'enable_navigation_menu': True, + + # Location of the documents panel on object detail pages (left/right) + 'documents_location': 'left', + + # Custom document types (see below) + 'custom_doc_types': [], + + # Per-model document type filtering (see below) + 'allowed_doc_types': {}, + }, + "netbox-contract": { +# https://github.com/mlebreuil/netbox-contract + }, + "netbox-data-flows": { +# https://github.com/Alef-Burzmali/netbox-data-flows/ + }, + "netbox-config-diff": { +# https://github.com/miaow2/netbox-config-diff + #define credentials for devices connection: + "USERNAME": "foo", + "PASSWORD": "bar", + "AUTH_SECONDARY": "foobar", # define here password for accessing Privileged EXEC mode, this variable is optional + "PATH_TO_SSH_CONFIG_FILE": "/home/.ssh/config", # define here PATH to SSH config file, it will be used for device connections, this variable is optional + }, + "netbox-attachments": { +# https://github.com/Kani999/netbox-attachments + "applied_scope": "model", + "scope_filter": ["dcim.device", "ipam.prefix", "tenancy"], + "display_default": "right_page", + "create_add_button": True, # show top "Attachments" dropdown in additional_tab mode + "display_setting": {"ipam.vlan": "left_page"}, + }, + "netbox-topology-views": { +# https://github.com/netbox-community/netbox-topology-views + 'static_image_directory': 'netbox_topology_views/img', + 'allow_coordinates_saving': True, + 'always_save_coordinates': True + }, + "netbox-reorder-rack": { +# https://github.com/minitriga/netbox-reorder-rack/ + }, + "netbox-secrets": { +# https://github.com/Onemind-Services-LLC/netbox-secrets/blob/master/docs +# A list of NetBox models where secrets can be assigned and displayed. Each entry is app_label.model. + 'apps': [ + 'dcim.device', + 'virtualization.virtualmachine', + ], + }, + "netbox-qrcode": { +# https://github.com/netbox-community/netbox-qrcode + }, + "netbox-floorplan": { +# https://github.com/netbox-community/netbox-floorplan-plugin + }, +} + diff --git a/docker-compose.override.yaml b/docker-compose.override.yaml deleted file mode 100644 index 23b67b4..0000000 --- a/docker-compose.override.yaml +++ /dev/null @@ -1,10 +0,0 @@ -services: - netbox: - build: - context: . - dockerfile: Dockerfile-Plugins - image: my-netbox-image:latest - netbox-worker: - image: my-netbox-image:latest - netbox-housekeeping: - image: my-netbox-image:latest diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example new file mode 100644 index 0000000..8c2ff70 --- /dev/null +++ b/docker-compose.override.yml.example @@ -0,0 +1,33 @@ +services: + netbox: + ports: + - "8000:8080" + # 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, + # when all tables need to be created the start_period should be higher than on + # subsequent starts. For the first start after major version upgrades of NetBox + # the start_period might also need to be set higher. + # Default value in our docker-compose.yml is 60s + # start_period: 90s + # environment: + # SKIP_SUPERUSER: "false" + # SUPERUSER_API_TOKEN: "" + # SUPERUSER_EMAIL: "" + # SUPERUSER_NAME: "" + # SUPERUSER_PASSWORD: "" + # SSO Configuration + # SOCIAL_AUTH_OKTA_OPENIDCONNECT_KEY: "your_okta_client_id" + # SOCIAL_AUTH_OKTA_OPENIDCONNECT_API_URL: "https://your-domain.okta.com" + # SOCIAL_AUTH_GOOGLE_OAUTH2_KEY: "your_google_client_id" + # secrets: + # - okta_openidconnect_secret + # - google_oauth2_secret + +# Uncomment to use Docker secrets for SSO credentials +# secrets: +# okta_openidconnect_secret: +# file: ./secrets/okta_secret.txt +# google_oauth2_secret: +# file: ./secrets/google_secret.txt + diff --git a/docker-compose.test.override.yml b/docker-compose.test.override.yml new file mode 100644 index 0000000..749e11a --- /dev/null +++ b/docker-compose.test.override.yml @@ -0,0 +1,5 @@ +services: + netbox: + ports: + - "127.0.0.1:8000:8080" + diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..8e22aa6 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,61 @@ +services: + netbox: &netbox + image: ${IMAGE-docker.io/netboxcommunity/netbox:latest} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + redis-cache: + condition: service_healthy + env_file: env/netbox.env + user: "netbox:root" + volumes: + - ./test-configuration/test_config.py:/etc/netbox/config/test_config.py:z,ro + healthcheck: + test: curl -f http://localhost:8080/login/ || exit 1 + start_period: ${NETBOX_START_PERIOD-120s} + timeout: 3s + interval: 15s + netbox-worker: + <<: *netbox + command: + - /opt/netbox/venv/bin/python + - /opt/netbox/netbox/manage.py + - rqworker + healthcheck: + test: ps -aux | grep -v grep | grep -q rqworker || exit 1 + start_period: 40s + timeout: 3s + interval: 15s + + postgres: + image: docker.io/postgres:18-alpine + env_file: env/postgres.env + healthcheck: + test: pg_isready -q -t 2 -d $$POSTGRES_DB -U $$POSTGRES_USER ## $$ because of docker-compose + start_period: 20s + interval: 1s + timeout: 5s + retries: 5 + + redis: &redis + image: docker.io/valkey/valkey:9.0-alpine + command: + - sh + - -c # this is to evaluate the $REDIS_PASSWORD from the env + - valkey-server --save "" --appendonly no --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose + env_file: env/redis.env + healthcheck: + test: '[ $$(valkey-cli --pass "$${REDIS_PASSWORD}" ping) = ''PONG'' ]' + start_period: 5s + timeout: 3s + interval: 1s + retries: 5 + redis-cache: + <<: *redis + env_file: env/redis-cache.env + +volumes: + netbox-media-files: + driver: local diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8a40ba5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,87 @@ +services: + netbox: &netbox + image: docker.io/netboxcommunity/netbox:${VERSION-v4.6-5.0.1} + depends_on: + - postgres + - redis + - redis-cache + env_file: env/netbox.env + user: "netbox:root" + healthcheck: + test: curl -f http://localhost:8080/login/ || exit 1 + start_period: 90s + timeout: 3s + interval: 15s + volumes: + - ./configuration:/etc/netbox/config:z,ro + - netbox-media-files:/opt/netbox/netbox/media:rw + - netbox-reports-files:/opt/netbox/netbox/reports:rw + - netbox-scripts-files:/opt/netbox/netbox/scripts:rw + netbox-worker: + <<: *netbox + depends_on: + netbox: + condition: service_healthy + command: + - /opt/netbox/venv/bin/python + - /opt/netbox/netbox/manage.py + - rqworker + healthcheck: + test: ps -aux | grep -v grep | grep -q rqworker || exit 1 + start_period: 20s + timeout: 3s + interval: 15s + + # postgres + postgres: + image: docker.io/postgres:18-alpine + healthcheck: + test: pg_isready -q -t 2 -d $$POSTGRES_DB -U $$POSTGRES_USER + start_period: 20s + timeout: 30s + interval: 10s + retries: 5 + env_file: env/postgres.env + volumes: + - netbox-postgres:/var/lib/postgresql + + # redis + redis: + image: docker.io/valkey/valkey:9.0-alpine + command: + - sh + - -c # this is to evaluate the $REDIS_PASSWORD from the env + - valkey-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose + healthcheck: &redis-healthcheck + test: '[ $$(valkey-cli --pass "$${REDIS_PASSWORD}" ping) = ''PONG'' ]' + start_period: 5s + timeout: 3s + interval: 1s + retries: 5 + env_file: env/redis.env + volumes: + - netbox-redis-data:/data + redis-cache: + image: docker.io/valkey/valkey:9.0-alpine + command: + - sh + - -c # this is to evaluate the $REDIS_PASSWORD from the env + - valkey-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose + healthcheck: *redis-healthcheck + env_file: env/redis-cache.env + volumes: + - netbox-redis-cache-data:/data + +volumes: + netbox-media-files: + driver: local + netbox-postgres: + driver: local + netbox-redis-cache-data: + driver: local + netbox-redis-data: + driver: local + netbox-reports-files: + driver: local + netbox-scripts-files: + driver: local diff --git a/docker/configuration.docker.py b/docker/configuration.docker.py new file mode 100644 index 0000000..13034fb --- /dev/null +++ b/docker/configuration.docker.py @@ -0,0 +1,91 @@ +## Generic Parts +# These functions are providing the functionality to load +# arbitrary configuration files. +# +# They can be imported by other code (see `ldap_config.py` for an example). + +import importlib.util +import sys +from os import scandir +from os.path import abspath, isfile + + +def _filename(f): + return f.name + + +def _import(module_name, path, loaded_configurations): + spec = importlib.util.spec_from_file_location('', path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + sys.modules[module_name] = module + + loaded_configurations.insert(0, module) + + print(f"🧬 loaded config '{path}'") + + +def read_configurations(config_module, config_dir, main_config): + loaded_configurations = [] + + main_config_path = abspath(f'{config_dir}/{main_config}.py') + if isfile(main_config_path): + _import(f'{config_module}.{main_config}', main_config_path, loaded_configurations) + else: + print(f"⚠️ Main configuration '{main_config_path}' not found.") + + with scandir(config_dir) as it: + for f in sorted(it, key=_filename): + if not f.is_file(): + continue + + if f.name.startswith('__'): + continue + + if not f.name.endswith('.py'): + continue + + if f.name == f'{main_config}.py': + continue + + if f.name == f'{config_dir}.py': + continue + + module_name = f'{config_module}.{f.name[: -len(".py")]}'.replace('.', '_') + _import(module_name, f.path, loaded_configurations) + + if len(loaded_configurations) == 0: + print(f"‼️ No configuration files found in '{config_dir}'.") + raise ImportError(f"No configuration files found in '{config_dir}'.") + + return loaded_configurations + + +## Specific Parts +# This section's code actually loads the various configuration files +# into the module with the given name. +# It contains the logic to resolve arbitrary configuration options by +# levaraging dynamic programming using `__getattr__`. + + +_loaded_configurations = read_configurations( + config_dir='/etc/netbox/config/', + config_module='netbox.configuration', + main_config='configuration', +) + + +def __getattr__(name): + for config in _loaded_configurations: + try: + return getattr(config, name) + except: + pass + raise AttributeError + + +def __dir__(): + names = [] + for config in _loaded_configurations: + names.extend(config.__dir__()) + return names diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100755 index 0000000..1d36167 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Runs on every start of the NetBox Docker container + +# Stop when an error occures +set -e + +# Allows NetBox to be run as non-root users +umask 002 + +# Load correct Python3 env +# shellcheck disable=SC1091 +source /opt/netbox/venv/bin/activate + +# Try to connect to the DB +DB_WAIT_TIMEOUT=${DB_WAIT_TIMEOUT-3} +MAX_DB_WAIT_TIME=${MAX_DB_WAIT_TIME-30} +CUR_DB_WAIT_TIME=0 +while [ "${CUR_DB_WAIT_TIME}" -lt "${MAX_DB_WAIT_TIME}" ]; do + # Read and truncate connection error tracebacks to last line by default + exec {psfd}< <(./manage.py showmigrations 2>&1) + read -rd '' DB_ERR <&$psfd || : + exec {psfd}<&- + wait $! && break + if [ -n "$DB_WAIT_DEBUG" ]; then + echo "$DB_ERR" + else + readarray -tn 0 DB_ERR_LINES <<<"$DB_ERR" + echo "${DB_ERR_LINES[@]: -1}" + echo "[ Use DB_WAIT_DEBUG=1 in netbox.env to print full traceback for errors here ]" + fi + echo "⏳ Waiting on DB... (${CUR_DB_WAIT_TIME}s / ${MAX_DB_WAIT_TIME}s)" + sleep "${DB_WAIT_TIMEOUT}" + CUR_DB_WAIT_TIME=$((CUR_DB_WAIT_TIME + DB_WAIT_TIMEOUT)) +done +if [ "${CUR_DB_WAIT_TIME}" -ge "${MAX_DB_WAIT_TIME}" ]; then + echo "❌ Waited ${MAX_DB_WAIT_TIME}s or more for the DB to become ready." + exit 1 +fi +# Check if update is needed +if ! ./manage.py migrate --check >/dev/null 2>&1; then + echo "⚙️ Applying database migrations" + ./manage.py migrate --no-input + echo "⚙️ Running trace_paths" + ./manage.py trace_paths --no-input + echo "⚙️ Removing stale content types" + ./manage.py remove_stale_contenttypes --no-input + echo "⚙️ Removing expired user sessions" + ./manage.py clearsessions + echo "⚙️ Building search index (lazy)" + ./manage.py reindex --lazy +fi + +# Create Superuser if required +if [ "$SKIP_SUPERUSER" == "true" ]; then + echo "↩️ Skip creating the superuser" +else + ./manage.py shell --no-startup --no-imports --interface python \ + str | None: + try: + with open('/run/secrets/' + secret_name, encoding='utf-8') as f: + return f.readline().strip() + except OSError: + return default + + +su_name = environ.get('SUPERUSER_NAME', 'admin') +su_email = environ.get('SUPERUSER_EMAIL', 'admin@example.com') +su_password = _read_secret('superuser_password', environ.get('SUPERUSER_PASSWORD')) +su_api_token = _read_secret('superuser_api_token', environ.get('SUPERUSER_API_TOKEN')) +su_api_key = _read_secret('superuser_api_key', environ.get('SUPERUSER_API_KEY')) + +if User.objects.filter(username=su_name).exists(): + print(f'User with name "{su_name}" already exists.') + sys.exit(0) + +if not su_password: + print( + '⚠️ Warning: No superuser password provided. Please set the SUPERUSER_PASSWORD environment variable or provide a secret file. The superuser will not be created.' + ) + sys.exit(0) + +u = User.objects.create_superuser(su_name, su_email, su_password) +if not settings.API_TOKEN_PEPPERS: + print('⚠️ No API token was created as API_TOKEN_PEPPERS is not set') + print(f'💡 Superuser Username: {su_name}, E-Mail: {su_email}') +else: + if su_api_key and su_api_token: + t = Token.objects.create(user=u, token=su_api_token, version=TokenVersionChoices.V2, key=su_api_key) + print(f'💡 Superuser Username: {su_name}, E-Mail: {su_email},') + print(f"💡 API Token: use with '{t.get_auth_header_prefix()}'") + else: + print('⚠️ No API token was created for the superuser as SUPERUSER_API_TOKEN and SUPERUSER_API_KEY are not set') + print(f'💡 Superuser Username: {su_name}, E-Mail: {su_email}') diff --git a/env/redis-cache.env b/env/redis-cache.env new file mode 100644 index 0000000..6285c33 --- /dev/null +++ b/env/redis-cache.env @@ -0,0 +1 @@ +REDIS_PASSWORD=t4Ph722qJ5QHeQ1qfu36 diff --git a/env/redis.env b/env/redis.env new file mode 100644 index 0000000..44a1987 --- /dev/null +++ b/env/redis.env @@ -0,0 +1 @@ +REDIS_PASSWORD=H733Kdjndks81 diff --git a/plugin_requirements.txt b/plugin_requirements.txt index 89b951d..5e63f4e 100644 --- a/plugin_requirements.txt +++ b/plugin_requirements.txt @@ -1,2 +1,14 @@ -netbox-plugin-circuit-maintenance==0.5.0 -netbox-documents==0.7.4 +netbox-plugin-prometheus-sd +netbox-lists +netbox-inventory +netbox-interface-synchronization +netbox-documents +netbox-contract +netbox-data-flows +netbox-config-diff +netbox-attachments +netbox-topology-views +netbox-reorder-rack +netbox-secrets +netbox-qrcode +netbox-floorplan-plugin diff --git a/release.sh b/release.sh new file mode 100755 index 0000000..5294799 --- /dev/null +++ b/release.sh @@ -0,0 +1,188 @@ +#!/bin/bash + +DEFAULT_REPO=netbox-community/netbox-docker +REPO="${REPO-${DEFAULT_REPO}}" + +echomoji() { + EMOJI=${1} + TEXT=${2} + shift 2 + if [ -z "$DISABLE_EMOJI" ]; then + echo "${EMOJI}" "${@}" + else + echo "${TEXT}" "${@}" + fi +} + +echo_nok() { + echomoji "❌" "!" "${@}" +} +echo_ok() { + echomoji "✅" "-" "${@}" +} +echo_hint() { + echomoji "👉" ">" "${@}" +} + +# check errors shall exit with code 1 + +check_clean_repo() { + changes=$(git status --porcelain 2>/dev/null) + if [ ${?} ] && [ -n "$changes" ]; then + echo_nok "There are git changes pending:" + echo "$changes" + echo_hint "Please clean the repository before continueing: git stash --include-untracked" + exit 1 + fi + echo_ok "Repository has no pending changes." +} + +check_branch() { + expected_branch="${1}" + actual_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) + if [ ${?} ] && [ "${actual_branch}" != "${expected_branch}" ]; then + echo_nok "Current branch should be '${expected_branch}', but is '${actual_branch}'." + echo_hint "Please change to the '${expected_branch}' branch: git checkout ${expected_branch}" + exit 1 + fi + echo_ok "The current branch is '${actual_branch}'." +} + +check_upstream() { + expected_upstream_branch="origin/${1}" + actual_upstream_branch=$(git rev-parse --abbrev-ref '@{upstream}' 2>/dev/null) + if [ ${?} ] && [ "${actual_upstream_branch}" != "${expected_upstream_branch}" ]; then + echo_nok "Current upstream branch should be '${expected_upstream_branch}', but is '${actual_upstream_branch}'." + echo_hint "Please set '${expected_upstream_branch}' as the upstream branch: git branch --set-upstream-to=${expected_upstream_branch}" + exit 1 + fi + echo_ok "The current upstream branch is '${actual_upstream_branch}'." +} + +check_origin() { + expected_origin="git@github.com:${REPO}.git" + actual_origin=$(git remote get-url origin 2>/dev/null) + if [ ${?} ] && [ "${actual_origin}" != "${expected_origin}" ]; then + echo_nok "The url of origin is '${actual_origin}', but '${expected_origin}' is expected." + echo_hint "Please set '${expected_origin}' as the url for origin: git origin set-url '${expected_origin}'" + exit 1 + fi + echo_ok "The current origin url is '${actual_origin}'." +} + +check_latest() { + git fetch --tags origin + + local_head_commit=$(git rev-parse HEAD 2>/dev/null) + remote_head_commit=$(git rev-parse FETCH_HEAD 2>/dev/null) + if [ "${local_head_commit}" != "${remote_head_commit}" ]; then + echo_nok "HEAD is at '${local_head_commit}', but FETCH_HEAD is at '${remote_head_commit}'." + echo_hint "Please ensure that you have pushed and pulled all the latest chanegs: git pull --prune --rebase origin; git push origin" + exit 1 + fi + echo_ok "HEAD and FETCH_HEAD both point to '${local_head_commit}'." +} + +check_tag() { + local tag + + tag=$(/dev/null >/dev/null; then + echo_nok "The tag '${tag}' already points to '$(git rev-parse "${tag}" 2>/dev/null)'." + echo_hint "Please ensure that the 'VERSION' file has been updated before trying to release: echo X.Y.Z > VERSION" + exit 1 + fi + echo_ok "The tag '${tag}' does not exist yet." +} + +check_develop() { + echomoji 📋 "?" "Checking 'develop' branch" + + check_branch develop + check_upstream develop + check_clean_repo + check_latest +} + +check_release() { + echomoji 📋 "?" "Checking 'release' branch" + + check_upstream release + check_clean_repo + check_latest +} + +# git errors shall exit with code 2 + +git_switch() { + echomoji 🔀 "≈" "Switching to '${1}' branch…" + if ! git checkout "${1}" >/dev/null; then + echo_nok "It was not possible to switch to the branch '${1}'." + exit 2 + fi + echo_ok "The branch is now '${1}'." +} + +git_tag() { + echomoji 🏷 "X" "Tagging version '${1}'…" + if ! git tag "${1}"; then + echo_nok "The tag '${1}' was not created because of an error." + exit 2 + fi + echo_ok "The tag '$(/dev/null)" ]; then + echo -e "\n${GREEN}Локальные бэкапы:${NC}" + ls -lh $BACKUP_DIR/netbox_db_*.sql.gz 2>/dev/null | awk '{print " " $9 " (" $5 ")"}' + fi + + # Бэкапы на Яндекс.Диске + if mountpoint -q ${YADISK_MOUNT} && [ -d "$YADISK_MOUNT/netbox-backups" ]; then + echo -e "\n${GREEN}Бэкапы на Яндекс.Диске:${NC}" + ls -lh $YADISK_MOUNT/netbox-backups/netbox_db_*.sql.gz 2>/dev/null | awk '{print " " $9 " (" $5 ")"}' + fi + echo "" +} + +# Выбор бэкапа +select_backup() { + local backup_files=() + + # Собираем все доступные бэкапы + if [ -d "$BACKUP_DIR" ]; then + for f in $BACKUP_DIR/netbox_db_*.sql.gz; do + [ -f "$f" ] && backup_files+=("$f") + done + fi + + if mountpoint -q ${YADISK_MOUNT} && [ -d "$YADISK_MOUNT/netbox-backups" ]; then + for f in $YADISK_MOUNT/netbox-backups/netbox_db_*.sql.gz; do + [ -f "$f" ] && backup_files+=("$f") + done + fi + + if [ ${#backup_files[@]} -eq 0 ]; then + print_error "Нет доступных бэкапов!" + exit 1 + fi + + echo "Выберите бэкап для восстановления:" + for i in "${!backup_files[@]}"; do + local size=$(du -h "${backup_files[$i]}" | cut -f1) + local date=$(basename "${backup_files[$i]}" | sed 's/netbox_db_\(.*\)\.sql.gz/\1/') + echo " $((i+1)). $date (${size}) - $(basename $(dirname ${backup_files[$i]}))" + done + echo " 0. Отмена" + echo "" + read -p "Введите номер [1-${#backup_files[@]}]: " choice + + if [ "$choice" == "0" ]; then + echo "Восстановление отменено." + exit 0 + fi + + if [ "$choice" -ge 1 ] && [ "$choice" -le ${#backup_files[@]} ]; then + SELECTED_BACKUP="${backup_files[$((choice-1))]}" + BACKUP_DATE=$(basename "$SELECTED_BACKUP" | sed 's/netbox_db_\(.*\)\.sql.gz/\1/') + BACKUP_SOURCE=$(basename $(dirname "$SELECTED_BACKUP")) + print_success "Выбран бэкап от $BACKUP_DATE (источник: $BACKUP_SOURCE)" + else + print_error "Неверный выбор!" + exit 1 + fi +} + +# Проверка, что NetBox не запущен +check_netbox_stopped() { + if docker compose ps | grep -q "Up"; then + print_warning "NetBox контейнеры запущены!" + read -p "Остановить их перед восстановлением? [y/N]: " stop_containers + if [[ $stop_containers =~ ^[Yy]$ ]]; then + print_info "Останавливаем контейнеры..." + docker compose down + else + print_error "Пожалуйста, остановите контейнеры вручную: docker compose down" + exit 1 + fi + fi +} + +# Основной процесс восстановления +restore_backup() { + echo "" + print_info "Начинаем восстановление из бэкапа $BACKUP_DATE" + echo "==========================================" + + # 1. Распаковка и восстановление базы данных + echo "" + print_info "1. Восстановление базы данных PostgreSQL..." + + # Запускаем только PostgreSQL + docker compose up -d postgres redis redis-cache + sleep 5 + + # Проверяем, что PostgreSQL готов + until docker compose exec -T postgres pg_isready -U netbox; do + echo " Ожидаем запуска PostgreSQL..." + sleep 2 + done + + # Восстанавливаем базу данных + echo " Восстанавливаем базу данных из $SELECTED_BACKUP..." + gunzip -c "$SELECTED_BACKUP" | docker compose exec -T postgres psql -U netbox -d netbox + + if [ $? -eq 0 ]; then + print_success "База данных восстановлена" + else + print_error "Ошибка восстановления базы данных" + exit 1 + fi + + # 2. Восстановление медиа-файлов + MEDIA_BACKUP="${BACKUP_DIR}/netbox_media_${BACKUP_DATE}.tar.gz" + if [ ! -f "$MEDIA_BACKUP" ] && mountpoint -q ${YADISK_MOUNT}; then + MEDIA_BACKUP="${YADISK_MOUNT}/netbox-backups/netbox_media_${BACKUP_DATE}.tar.gz" + fi + + if [ -f "$MEDIA_BACKUP" ]; then + echo "" + print_info "2. Восстановление медиа-файлов..." + docker compose exec -T netbox rm -rf /opt/netbox/netbox/media/* 2>/dev/null + docker compose exec -T netbox tar xzf - -C /opt/netbox/netbox/media < "$MEDIA_BACKUP" + print_success "Медиа-файлы восстановлены" + else + print_warning "Медиа-бэкап не найден: netbox_media_${BACKUP_DATE}.tar.gz" + fi + + # 3. Восстановление .env файлов + ENV_BACKUP="${BACKUP_DIR}/netbox_env_${BACKUP_DATE}.tar.gz" + if [ ! -f "$ENV_BACKUP" ] && mountpoint -q ${YADISK_MOUNT}; then + ENV_BACKUP="${YADISK_MOUNT}/netbox-backups/netbox_env_${BACKUP_DATE}.tar.gz" + fi + + if [ -f "$ENV_BACKUP" ]; then + echo "" + print_info "3. Восстановление .env файлов..." + tar xzf "$ENV_BACKUP" -C /opt/netbox + print_success ".env файлы восстановлены" + else + print_warning ".env бэкап не найден: netbox_env_${BACKUP_DATE}.tar.gz" + fi + + # 4. Запуск миграций и сбор статики + echo "" + print_info "4. Запуск миграций и сбор статики..." + docker compose up -d netbox + sleep 10 + docker compose exec netbox python /opt/netbox/netbox/manage.py migrate + docker compose exec netbox python /opt/netbox/netbox/manage.py collectstatic --no-input + + # 5. Запуск всех контейнеров + echo "" + print_info "5. Запуск всех сервисов..." + docker compose up -d + + echo "" + print_success "Восстановление завершено!" + echo "" + print_info "Проверьте логи: docker compose logs netbox --tail 50" + print_info "Веб-интерфейс: http://localhost:8080" +} + +# Функция для быстрого восстановления последнего бэкапа +quick_restore() { + local latest_backup=$(ls -t $BACKUP_DIR/netbox_db_*.sql.gz 2>/dev/null | head -1) + if [ -z "$latest_backup" ] && mountpoint -q ${YADISK_MOUNT}; then + latest_backup=$(ls -t $YADISK_MOUNT/netbox-backups/netbox_db_*.sql.gz 2>/dev/null | head -1) + fi + + if [ -z "$latest_backup" ]; then + print_error "Нет доступных бэкапов!" + exit 1 + fi + + SELECTED_BACKUP="$latest_backup" + BACKUP_DATE=$(basename "$SELECTED_BACKUP" | sed 's/netbox_db_\(.*\)\.sql.gz/\1/') + BACKUP_SOURCE=$(basename $(dirname "$SELECTED_BACKUP")) + print_info "Восстанавливаем последний бэкап: $BACKUP_DATE (источник: $BACKUP_SOURCE)" + restore_backup +} + +# Основное меню +main() { + cd /opt/netbox + + echo "🔄 NetBox Recovery Script" + echo "========================" + echo "" + echo "1. Показать доступные бэкапы" + echo "2. Восстановить из выбранного бэкапа" + echo "3. Быстрое восстановление (последний бэкап)" + echo "0. Выход" + echo "" + read -p "Выберите действие [0-3]: " action + + case $action in + 1) + show_backups + ;; + 2) + show_backups + select_backup + check_netbox_stopped + restore_backup + ;; + 3) + quick_restore + ;; + 0) + echo "Выход." + exit 0 + ;; + *) + print_error "Неверный выбор!" + exit 1 + ;; + esac +} + +# Запуск +main "$@" diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..4d7d5cd --- /dev/null +++ b/ruff.toml @@ -0,0 +1,48 @@ +# Ruff configuration +#################### +exclude = [ + ".git", + ".netbox", + ".ruff_cache", + ".venv", + "venv", +] +line-length = 120 +indent-width = 4 +respect-gitignore = true +target-version = "py312" + +[lint] +preview = true +extend-select = [ + "E1", # pycodestyle errors: indentation-related (e.g., unexpected/missing indent) + "E2", # pycodestyle errors: whitespace-related (e.g., missing whitespace, extra spaces) + "E3", # pycodestyle errors: blank lines / spacing around definitions + "E501", # pycodestyle: line too long (enforced with `line-length` above) + "W", # pycodestyle warnings (various style warnings, often whitespace/newlines) + "I", # import sorting (isort-equivalent) + "RET", # return semantics (flake8-return family: consistent/explicit returns; remove redundant else/assign before return) + "UP", # pyupgrade: modernize syntax for your target Python (e.g., f-strings, built-in generics, newer stdlib idioms) + "RUF022", # ruff: enforce sorted `__all__` lists +] + +ignore = [ + "E266", # pycodestyle: too many leading '#' for block comment + "S110" # ruff try-except-pass +] + +[lint.per-file-ignores] +"configuration/*" = [ + "E501", # pycodestyle: line too long (enforced with `line-length` above) + "E722" # pycodestyle: do not use bare `except` +] +"docker/*" = [ + "E501", # pycodestyle: line too long (enforced with `line-length` above) + "E722" # pycodestyle: do not use bare `except` +] + + +[format] +quote-style = "single" +indent-style = "space" +line-ending = "lf" diff --git a/test-configuration/test_config.py b/test-configuration/test_config.py new file mode 100644 index 0000000..d28e2c2 --- /dev/null +++ b/test-configuration/test_config.py @@ -0,0 +1,17 @@ +LOGGING = {'version': 1, 'disable_existing_loggers': True} + +PLUGINS = [ + 'netbox.tests.dummy_plugin', +] + +ALLOW_TOKEN_RETRIEVAL = True + +DEFAULT_PERMISSIONS = {} + +API_TOKEN_PEPPERS = { + 1: 'TEST-VALUE-DO-NOT-USE-TEST-VALUE-DO-NOT-USE-TEST-VALUE-DO-NOT-USE', +} + +RQ = { + 'COMMIT_MODE': 'auto', +} diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..f415cd7 --- /dev/null +++ b/test.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# Runs the original NetBox unit tests and tests whether all initializers work. +# Usage: +# ./test.sh latest +# ./test.sh v2.9.7 +# ./test.sh develop-2.10 +# IMAGE='netboxcommunity/netbox:latest' ./test.sh +# IMAGE='netboxcommunity/netbox:v2.9.7' ./test.sh +# IMAGE='netboxcommunity/netbox:develop-2.10' ./test.sh +# export IMAGE='netboxcommunity/netbox:latest'; ./test.sh +# export IMAGE='netboxcommunity/netbox:v2.9.7'; ./test.sh +# export IMAGE='netboxcommunity/netbox:develop-2.10'; ./test.sh + +# exit when a command exits with an exit code != 0 +set -e + +source ./build-functions/gh-functions.sh + +# IMAGE is used by `docker-compose.yml` do determine the tag +# of the Docker Image that is to be used +if [ "${1}x" != "x" ]; then + # Use the command line argument + export IMAGE="netboxcommunity/netbox:${1}" +else + export IMAGE="${IMAGE-netboxcommunity/netbox:latest}" +fi + +# Ensure that an IMAGE is defined +if [ -z "${IMAGE}" ]; then + echo "⚠️ No image defined" + + if [ -z "${DEBUG}" ]; then + exit 1 + else + echo "⚠️ Would 'exit 1' here, but DEBUG is '${DEBUG}'." + fi +fi + +# The docker compose command to use +doco="docker compose --file docker-compose.test.yml --file docker-compose.test.override.yml --project-name netbox_docker_test" + +test_setup() { + gh_echo "::group:: Test setup" + echo "🏗 Setup up test environment" + $doco up --detach --quiet-pull --wait --force-recreate --renew-anon-volumes --no-start + $doco start postgres + $doco start redis + $doco start redis-cache + gh_echo "::endgroup::" +} + +test_netbox_unit_tests() { + gh_echo "::group:: Netbox unit tests" + echo "⏱ Running NetBox Unit Tests" + $doco run --rm netbox /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py test + gh_echo "::endgroup::" +} + +test_compose_db_setup() { + gh_echo "::group:: Netbox DB migrations" + echo "⏱ Running NetBox DB migrations" + $doco run --rm netbox /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py migrate + gh_echo "::endgroup::" +} + +test_netbox_start() { + gh_echo "::group:: Start Netbox service" + echo "⏱ Starting NetBox services" + $doco up --detach --wait + gh_echo "::endgroup::" +} + +test_netbox_web() { + gh_echo "::group:: Web service test" + echo "⏱ Starting web service test" + RESP_CODE=$( + curl \ + --silent \ + --output /dev/null \ + --write-out '%{http_code}' \ + --request GET \ + --connect-timeout 5 \ + --max-time 10 \ + --retry 5 \ + --retry-delay 0 \ + --retry-max-time 40 \ + http://127.0.0.1:8000/login/ + ) + if [ "$RESP_CODE" == "200" ]; then + echo "Webservice running" + else + echo "⚠️ Got response code '$RESP_CODE' but expected '200'" + exit 1 + fi + gh_echo "::endgroup::" +} + +test_cleanup() { + echo "💣 Cleaning Up" + gh_echo "::group:: Docker compose logs" + $doco logs --no-color + gh_echo "::endgroup::" + gh_echo "::group:: Docker compose down" + $doco down --volumes + gh_echo "::endgroup::" +} + +echo "🐳🐳🐳 Start testing '${IMAGE}'" + +# Make sure the cleanup script is executed +trap test_cleanup EXIT ERR +test_setup + +test_netbox_unit_tests +test_compose_db_setup +test_netbox_start +test_netbox_web + +echo "🐳🐳🐳 Done testing '${IMAGE}'"