Compare commits
70 Commits
Author | SHA1 | Date | |
---|---|---|---|
00022e7d79 | |||
dd490605ca | |||
e2711ca205 | |||
a87f2b3331 | |||
f174749f98 | |||
b02a93904e | |||
3ace32dfc2 | |||
9fae2b0f74 | |||
23c3240863 | |||
3f9e874d77 | |||
4b0f158852 | |||
51490d0039 | |||
688672de26 | |||
5624ecc65e | |||
7f93b328af | |||
ccad15ecf7 | |||
e9a9d9b70b | |||
03b36058cd | |||
9e06a38d41 | |||
b7ad18afb0 | |||
93a3784295 | |||
45f7823a17 | |||
a6584d2874 | |||
fd87c7cb98 | |||
1f38ca0a86 | |||
efec435ba0 | |||
ab8ff04852 | |||
77feec30a0 | |||
bed40b0d05 | |||
24f0545bc6 | |||
1bc1ab2a0a | |||
bfa69dc0e9 | |||
6c8042b63d | |||
795e82be46 | |||
d8a6c321a1 | |||
1ecf4853b8 | |||
5cb8e97e65 | |||
98e131fa30 | |||
d6323ce40f | |||
1c65f7af10 | |||
7fb78b3fd2 | |||
aa9f01a778 | |||
786f9b50d2 | |||
5909670690 | |||
231de236e0 | |||
9ea95950b9 | |||
d84c399c00 | |||
a217ce8ffd | |||
794fb45e0e | |||
8e34f46bad | |||
ed0d099df7 | |||
26d08302e3 | |||
64b763429f | |||
5769684c98 | |||
23e2da52f8 | |||
edba1a22fc | |||
f87ffe7c33 | |||
8bc77c1bc0 | |||
7662d81efe | |||
7f489aa11d | |||
5c3eff0823 | |||
5f90ad8c56 | |||
5f1c241145 | |||
20c7461c7b | |||
b9c44b85cc | |||
355ebadd10 | |||
880628876f | |||
8d3bd48c7a | |||
882f7bcaf2 | |||
50ade7bce1 |
@ -7,5 +7,4 @@ build*
|
||||
docker-compose.override.yml
|
||||
.netbox/.git*
|
||||
.netbox/.travis.yml
|
||||
.netbox/docs
|
||||
.netbox/scripts
|
||||
|
10
.github/no-response.yml
vendored
Normal file
10
.github/no-response.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# Configuration for probot-no-response - https://github.com/probot/no-response
|
||||
|
||||
# Number of days of inactivity before an Issue is closed for lack of response
|
||||
daysUntilClose: 30
|
||||
# Label requiring a response
|
||||
responseRequiredLabel: awaiting answer
|
||||
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
|
||||
closeComment: >
|
||||
This issue has been automatically closed because there has been no response
|
||||
to our request for more information from the original author.
|
3
.github/workflows/push.yml
vendored
3
.github/workflows/push.yml
vendored
@ -15,7 +15,8 @@ jobs:
|
||||
build_cmd:
|
||||
- ./build-latest.sh
|
||||
- PRERELEASE=true ./build-latest.sh
|
||||
- ./build-branches.sh
|
||||
- ./build-next.sh
|
||||
- ./build.sh develop
|
||||
docker_from:
|
||||
- '' # use the default of the DOCKERFILE
|
||||
- python:3.7-alpine
|
||||
|
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@ -14,14 +14,15 @@ jobs:
|
||||
build_cmd:
|
||||
- ./build-latest.sh
|
||||
- PRERELEASE=true ./build-latest.sh
|
||||
- ./build-branches.sh
|
||||
- ./build-next.sh
|
||||
- ./build.sh develop
|
||||
fail-fast: false
|
||||
runs-on: ubuntu-latest
|
||||
name: Builds new Netbox Docker Images
|
||||
steps:
|
||||
- id: git-checkout
|
||||
name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v2
|
||||
- id: docker-build
|
||||
name: Build the image with '${{ matrix.build_cmd }}'
|
||||
run: ${{ matrix.build_cmd }}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
||||
.netbox
|
||||
.initializers
|
||||
docker-compose.override.yml
|
||||
*.pem
|
||||
|
32
.travis.yml
32
.travis.yml
@ -1,32 +0,0 @@
|
||||
sudo: required
|
||||
language: python
|
||||
|
||||
env:
|
||||
- BUILD=release
|
||||
- BUILD=prerelease
|
||||
- BUILD=branches
|
||||
- BUILD=special
|
||||
|
||||
git:
|
||||
depth: 5
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
install:
|
||||
- docker-compose pull --parallel
|
||||
- docker-compose build
|
||||
|
||||
script:
|
||||
- docker-compose run netbox ./manage.py test
|
||||
|
||||
after_script:
|
||||
- docker-compose down
|
||||
|
||||
after_success:
|
||||
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||
- ./build-all.sh --push
|
||||
|
||||
notifications:
|
||||
slack:
|
||||
secure: F3VsWcvU/XYyjGjU8ZAVGpREe7F1NjKq6LuMRzhQORbXUvanxDQtLzEe0Y5zm/6+gHkx6t8cX/v2PiCI+v46pkapYMUimd+QEOL1WxbUdnw2kQxcgw/R3wX34l2FHXbG3/a+TmH3euqbSCTIrPy9ufju948i+Q0E0u0fyInmozl8qOT23C4joQOpVAq7y+wHxTxsEg46ZzL2Ties+dmqjMsvHocv7mPI2IWzAWA8SJZxS82Amoapww++QjgEmoY+tMimLkdeXCRgeoj41UGHDg54rbEXh/PTaWiuzyzTr1WLmsGRScC57fDRivp3mSM37/MlNxsRj1z+j4zrvWFQgNfJ2yMjBHroc1jOX/uCY4dwbpSPqUCpc4idMGCGZFItgzTQ3lAPYAsom0C6n8C08Xk8EsNKWwXrDSd4ZUhIwptkNPCFK+kXbLFsMzSApnaBYW0T+wba57nZdiWjOPYmvJr49MDm5NHv2KaRBX2gpw7t7ZLhTgwGEWcZvcDebiLneXcXY5hZ7v2NHJkx/2x1yNXo85xZDy0wK1FGoOOHwPhvqOB+pcQZ/pUOSPTKqGw5l/CexoRm1shFsK+19FnSgimqTHjcuCo4lFW3JlEvlFhtfFXIte2Wjp1ALZgTrSq8zSD5rRxYCUKmM7b3EJwdaIgbvKWPdS4sCXlXU1bHx0g=
|
@ -1,4 +1,4 @@
|
||||
ARG FROM=python:3.7-alpine
|
||||
ARG FROM
|
||||
FROM ${FROM} as builder
|
||||
|
||||
RUN apk add --no-cache \
|
||||
@ -75,7 +75,7 @@ WORKDIR /opt/netbox/netbox
|
||||
# container startup.
|
||||
# Must set permissions for '/opt/netbox/netbox/media' directory
|
||||
# to g+w so that pictures can be uploaded to netbox.
|
||||
RUN mkdir static && chmod g+w static media
|
||||
RUN mkdir static && chmod -R g+w static media
|
||||
|
||||
ENTRYPOINT [ "/opt/netbox/docker-entrypoint.sh" ]
|
||||
|
||||
|
82
README.md
82
README.md
@ -12,7 +12,8 @@
|
||||
[The Github repository](netbox-docker-github) houses the components needed to build Netbox as a Docker container.
|
||||
Images are built using this code and are released to [Docker Hub][netbox-dockerhub] once a day.
|
||||
|
||||
Do you have any questions? Before opening an issue on Github, please join the [Network To Code][ntc-slack] Slack and ask for help in our [`#netbox-docker`][netbox-docker-slack] channel.
|
||||
Do you have any questions?
|
||||
Before opening an issue on Github, please join the [Network To Code][ntc-slack] Slack and ask for help in our [`#netbox-docker`][netbox-docker-slack] channel.
|
||||
|
||||
[github-stargazers]: https://github.com/netbox-community/netbox-docker/stargazers
|
||||
[github-release]: https://github.com/netbox-community/netbox-docker/releases
|
||||
@ -25,16 +26,18 @@ Do you have any questions? Before opening an issue on Github, please join the [N
|
||||
|
||||
## Docker Tags
|
||||
|
||||
* `vX.Y.Z`: Release builds, built from [releases of Netbox][netbox-releases].
|
||||
* `latest`: Release builds, built from [`master` branch of Netbox][netbox-master].
|
||||
* `snapshot`: Pre-release builds, built from the [`develop` branch of Netbox][netbox-develop].
|
||||
* `develop-X.Y`: Pre-release builds, built from the corresponding [branch of Netbox][netbox-branches].
|
||||
* `vX.Y.Z`: These are release builds, automatically built from [the corresponding releases of Netbox][netbox-releases].
|
||||
* `latest`: These are release builds, automatically built from [the `master` branch of Netbox][netbox-master].
|
||||
* `snapshot`: These are pre-release builds, automatically built from the [`develop` branch of Netbox][netbox-develop].
|
||||
* `develop-X.Y`: These are pre-release builds, automatically built from the corresponding [branch of Netbox][netbox-branches].
|
||||
|
||||
Then there is currently one extra tags for each of the above labels:
|
||||
Then there is currently one extra tags for each of the above tags:
|
||||
|
||||
* `-ldap`: Contains additional dependencies and configurations for connecting Netbox to an LDAP directroy.
|
||||
* `-ldap`: Contains additional dependencies and configurations for connecting Netbox to an LDAP directory.
|
||||
[Learn more about that in our wiki][netbox-docker-ldap].
|
||||
|
||||
New images are built and published automatically every ~24h.
|
||||
|
||||
[netbox-releases]: https://github.com/netbox-community/netbox/releases
|
||||
[netbox-master]: https://github.com/netbox-community/netbox/tree/master
|
||||
[netbox-develop]: https://github.com/netbox-community/netbox/tree/develop
|
||||
@ -43,42 +46,50 @@ Then there is currently one extra tags for each of the above labels:
|
||||
|
||||
## Quickstart
|
||||
|
||||
To get Netbox up and running in Docker:
|
||||
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
|
||||
tee docker-compose.override.yml <<EOF
|
||||
version: '3.4'
|
||||
services:
|
||||
nginx:
|
||||
ports:
|
||||
- 8000:8080
|
||||
EOF
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
The application will be available after a few minutes.
|
||||
Use `docker-compose port nginx 8080` to find out where to connect to.
|
||||
|
||||
```bash
|
||||
$ echo "http://$(docker-compose port nginx 8080)/"
|
||||
http://0.0.0.0:32768/
|
||||
|
||||
# Open netbox in your default browser on macOS:
|
||||
$ open "http://$(docker-compose port nginx 8080)/"
|
||||
|
||||
# Open netbox in your default browser on (most) linuxes:
|
||||
$ xdg-open "http://$(docker-compose port nginx 8080)/" &>/dev/null &
|
||||
```
|
||||
|
||||
Alternatively, use something like [Reception][docker-reception] to connect to _docker-compose_ projects.
|
||||
|
||||
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.
|
||||
In the top-right corner you can login.
|
||||
The default credentials are:
|
||||
|
||||
* Username: **admin**
|
||||
* Password: **admin**
|
||||
* API Token: **0123456789abcdef0123456789abcdef01234567**
|
||||
|
||||
There is a more complete [Getting Started guide on our Wiki][wiki-getting-started].
|
||||
|
||||
[wiki-getting-started]: https://github.com/netbox-community/netbox-docker/wiki/Getting-Started
|
||||
[docker-reception]: https://github.com/nxt-engineering/reception
|
||||
|
||||
## Documentation
|
||||
|
||||
Please refer [to our wiki on Github][netbox-docker-wiki] for further information on how to use this Netbox Docker image properly.
|
||||
It covers advanced topics such as using secret files, deployment to Kubernetes as well as NAPALM and LDAP configuration.
|
||||
|
||||
[netbox-docker-wiki]: https://github.com/netbox-community/netbox-docker/wiki/
|
||||
|
||||
## Getting Help
|
||||
|
||||
Please join [our Slack channel `#netbox-docker`][netbox-docker-slack] on the [Network To Code Slack][ntc-slack].
|
||||
It's free to use and there are almost always people online that can help.
|
||||
|
||||
If you need help with using Netbox or developing for it or against it's API you may find the `#netbox` channel on the same Slack instance very helpful.
|
||||
|
||||
## Dependencies
|
||||
|
||||
This project relies only on *Docker* and *docker-compose* meeting these requirements:
|
||||
@ -88,17 +99,10 @@ This project relies only on *Docker* and *docker-compose* meeting these requirem
|
||||
|
||||
To check the version installed on your system run `docker --version` and `docker-compose --version`.
|
||||
|
||||
## Documentation
|
||||
## Use a Specific Netbox Version
|
||||
|
||||
Please refer [to our wiki on Github][netbox-docker-wiki] for further information on how to use this Netbox Docker image properly.
|
||||
It covers advanced topics such as using secret files, deployment to Kubernetes as well as NAPALM and LDAP configuration.
|
||||
|
||||
[netbox-docker-wiki]: https://github.com/netbox-community/netbox-docker/wiki/
|
||||
|
||||
## Netbox Version
|
||||
|
||||
The `docker-compose.yml` file is prepared to run a specific version of Netbox.
|
||||
To use this feature, set the environment-variable `VERSION` before launching `docker-compose`, as shown below.
|
||||
The `docker-compose.yml` file is prepared to run a specific version of Netbox, instead of `latest`.
|
||||
To use this feature, set and export the environment-variable `VERSION` before launching `docker-compose`, as shown below.
|
||||
`VERSION` may be set to the name of
|
||||
[any tag of the `netboxcommunity/netbox` Docker image on Docker Hub][netbox-dockerhub].
|
||||
|
||||
@ -141,10 +145,6 @@ 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
|
||||
|
||||
### Pre-made Docker Images
|
||||
|
||||
New Docker images are built and published every 24h.
|
||||
|
||||
## Tests
|
||||
|
||||
We have a test script.
|
||||
|
51
build-all.sh
51
build-all.sh
@ -1,51 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Builds all Docker images this project provides
|
||||
# Arguments:
|
||||
# BUILD: The release to build.
|
||||
# Allowed: release, prerelease, branches, special
|
||||
# Default: undefined
|
||||
|
||||
echo "▶️ $0 $*"
|
||||
|
||||
ALL_BUILDS=("release" "prerelease" "branches" "special")
|
||||
BUILDS=("${BUILD:-"${ALL_BUILDS[@]}"}")
|
||||
|
||||
echo "⚙️ Configured builds: ${BUILDS[*]}"
|
||||
|
||||
if [ -n "${DEBUG}" ]; then
|
||||
export DEBUG
|
||||
fi
|
||||
|
||||
ERROR=0
|
||||
|
||||
for BUILD in "${BUILDS[@]}"; do
|
||||
echo "🛠 Building '$BUILD' from '$DOCKERFILE'"
|
||||
case $BUILD in
|
||||
release)
|
||||
# build the latest release
|
||||
# shellcheck disable=SC2068
|
||||
./build-latest.sh $@ || ERROR=1
|
||||
;;
|
||||
prerelease)
|
||||
# build the latest pre-release
|
||||
# shellcheck disable=SC2068
|
||||
PRERELEASE=true ./build-latest.sh $@ || ERROR=1
|
||||
;;
|
||||
branches)
|
||||
# build all branches
|
||||
# shellcheck disable=SC2068
|
||||
./build-branches.sh $@ || ERROR=1
|
||||
;;
|
||||
*)
|
||||
echo "🚨 Unrecognized build '$BUILD'."
|
||||
|
||||
if [ -z "$DEBUG" ]; then
|
||||
exit 1
|
||||
else
|
||||
echo "⚠️ Would exit here with code '1', but DEBUG is enabled."
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
exit $ERROR
|
8
build-functions/docker-functions.sh
Normal file
8
build-functions/docker-functions.sh
Normal file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
push_image_to_registry() {
|
||||
local target_tag=$1
|
||||
echo "⏫ Pushing '${target_tag}'"
|
||||
$DRY docker push "${target_tag}"
|
||||
echo "✅ Finished pushing the Docker image '${target_tag}'."
|
||||
}
|
82
build-functions/get-public-image-config.sh
Normal file
82
build-functions/get-public-image-config.sh
Normal file
@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
# Retrieves image configuration from public images in DockerHub
|
||||
# Functions from https://gist.github.com/cirocosta/17ea17be7ac11594cb0f290b0a3ac0d1
|
||||
# Optimised for our use case
|
||||
|
||||
get_image_label() {
|
||||
local label=$1
|
||||
local image=$2
|
||||
local tag=$3
|
||||
local token
|
||||
token=$(_get_token "$image")
|
||||
local digest
|
||||
digest=$(_get_digest "$image" "$tag" "$token")
|
||||
local retval="null"
|
||||
if [ "$digest" != "null" ]; then
|
||||
retval=$(_get_image_configuration "$image" "$token" "$digest" "$label")
|
||||
fi
|
||||
echo "$retval"
|
||||
}
|
||||
|
||||
get_image_layers() {
|
||||
local image=$1
|
||||
local tag=$2
|
||||
local token
|
||||
token=$(_get_token "$image")
|
||||
_get_layers "$image" "$tag" "$token"
|
||||
}
|
||||
|
||||
get_image_last_layer() {
|
||||
local image=$1
|
||||
local tag=$2
|
||||
local token
|
||||
token=$(_get_token "$image")
|
||||
local layers
|
||||
mapfile -t layers < <(_get_layers "$image" "$tag" "$token")
|
||||
echo "${layers[-1]}"
|
||||
}
|
||||
|
||||
_get_image_configuration() {
|
||||
local image=$1
|
||||
local token=$2
|
||||
local digest=$3
|
||||
local label=$4
|
||||
curl \
|
||||
--silent \
|
||||
--location \
|
||||
--header "Authorization: Bearer $token" \
|
||||
"https://registry-1.docker.io/v2/$image/blobs/$digest" \
|
||||
| jq -r ".config.Labels.\"$label\""
|
||||
}
|
||||
|
||||
_get_token() {
|
||||
local image=$1
|
||||
curl \
|
||||
--silent \
|
||||
"https://auth.docker.io/token?scope=repository:$image:pull&service=registry.docker.io" \
|
||||
| jq -r '.token'
|
||||
}
|
||||
|
||||
_get_digest() {
|
||||
local image=$1
|
||||
local tag=$2
|
||||
local token=$3
|
||||
curl \
|
||||
--silent \
|
||||
--header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
|
||||
--header "Authorization: Bearer $token" \
|
||||
"https://registry-1.docker.io/v2/$image/manifests/$tag" \
|
||||
| jq -r '.config.digest'
|
||||
}
|
||||
|
||||
_get_layers() {
|
||||
local image=$1
|
||||
local tag=$2
|
||||
local token=$3
|
||||
curl \
|
||||
--silent \
|
||||
--header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
|
||||
--header "Authorization: Bearer $token" \
|
||||
"https://registry-1.docker.io/v2/$image/manifests/$tag" \
|
||||
| jq -r '.layers[].digest'
|
||||
}
|
@ -23,25 +23,17 @@ GITHUB_REPO="${GITHUB_REPO-$ORIGINAL_GITHUB_REPO}"
|
||||
URL_RELEASES="https://api.github.com/repos/${GITHUB_REPO}/branches?${GITHUB_OAUTH_PARAMS}"
|
||||
|
||||
# Composing the JQ commans to extract the most recent version number
|
||||
JQ_BRANCHES='map(.name) | .[] | scan("^[^v].+") | match("^(master|develop).*") | .string'
|
||||
JQ_NEXT='map(.name) | .[] | scan("^[^v].+") | match("^(develop-).*") | .string'
|
||||
|
||||
CURL="curl -sS"
|
||||
|
||||
# Querying the Github API to fetch all branches
|
||||
BRANCHES=$($CURL "${URL_RELEASES}" | jq -r "$JQ_BRANCHES")
|
||||
NEXT=$($CURL "${URL_RELEASES}" | jq -r "$JQ_NEXT")
|
||||
|
||||
###
|
||||
# Building each branch
|
||||
###
|
||||
|
||||
# keeping track whether an error occured
|
||||
ERROR=0
|
||||
|
||||
# calling build.sh for each branch
|
||||
for BRANCH in $BRANCHES; do
|
||||
if [ -n "$NEXT" ]; then
|
||||
# shellcheck disable=SC2068
|
||||
./build.sh "${BRANCH}" $@ || ERROR=1
|
||||
done
|
||||
|
||||
# returning whether an error occured
|
||||
exit $ERROR
|
||||
./build.sh "${NEXT}" $@
|
||||
else
|
||||
echo "No branch matching 'develop-*' found"
|
||||
echo "::set-output name=skipped::true"
|
||||
fi
|
129
build.sh
129
build.sh
@ -49,7 +49,7 @@ if [ "${1}x" == "x" ] || [ "${1}" == "--help" ] || [ "${1}" == "-h" ]; then
|
||||
echo " DOCKERFILE The name of Dockerfile to use."
|
||||
echo " Default: Dockerfile"
|
||||
echo " DOCKER_FROM The base image to use."
|
||||
echo " Default: Whatever is defined as default in the Dockerfile."
|
||||
echo " Default: 'python:3.7-alpine'"
|
||||
echo " DOCKER_TARGET A specific target to build."
|
||||
echo " It's currently not possible to pass multiple targets."
|
||||
echo " Default: main ldap"
|
||||
@ -153,6 +153,13 @@ if [ ! -f "${DOCKERFILE}" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
###
|
||||
# Determining the value for DOCKER_FROM
|
||||
###
|
||||
if [ -z "$DOCKER_FROM" ]; then
|
||||
DOCKER_FROM="python:3.7-alpine"
|
||||
fi
|
||||
|
||||
###
|
||||
# Variables for labelling the docker image
|
||||
###
|
||||
@ -167,9 +174,9 @@ PROJECT_VERSION="${PROJECT_VERSION-$(sed -e 's/^[[:space:]]*//' -e 's/[[:space:]
|
||||
|
||||
# Get the Git information from the netbox directory
|
||||
if [ -d "${NETBOX_PATH}/.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)
|
||||
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
|
||||
|
||||
###
|
||||
@ -217,15 +224,18 @@ for DOCKER_TARGET in "${DOCKER_TARGETS[@]}"; do
|
||||
# composing the additional DOCKER_SHORT_TAG,
|
||||
# i.e. "v2.6.1" becomes "v2.6",
|
||||
# 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-${DOCKER_REGISTRY}/${DOCKER_ORG}/${DOCKER_REPO}:v${MAJOR}.${MINOR}}"
|
||||
TARGET_DOCKER_LATEST_TAG="${DOCKER_REGISTRY}/${DOCKER_ORG}/${DOCKER_REPO}:latest"
|
||||
|
||||
if [ "${DOCKER_TARGET}" != "main" ]; then
|
||||
TARGET_DOCKER_SHORT_TAG="${TARGET_DOCKER_SHORT_TAG}-${DOCKER_TARGET}"
|
||||
TARGET_DOCKER_LATEST_TAG="${TARGET_DOCKER_LATEST_TAG}-${DOCKER_TARGET}"
|
||||
fi
|
||||
fi
|
||||
|
||||
@ -233,6 +243,48 @@ for DOCKER_TARGET in "${DOCKER_TARGETS[@]}"; do
|
||||
# Proceeding to buils stage, except if `--push-only` is passed
|
||||
###
|
||||
if [ "${2}" != "--push-only" ] ; then
|
||||
###
|
||||
# Checking if the build is necessary,
|
||||
# meaning build only if one of those values changed:
|
||||
# - Python base image digest (Label: PYTHON_BASE_DIGEST)
|
||||
# - netbox git ref (Label: NETBOX_GIT_REF)
|
||||
# - netbox-docker git ref (Label: org.label-schema.vcs-ref)
|
||||
###
|
||||
# Load information from registry (only for docker.io)
|
||||
SHOULD_BUILD="false"
|
||||
BUILD_REASON=""
|
||||
if [ -z "${GH_ACTION}" ]; then
|
||||
# Asuming non Github builds should always proceed
|
||||
SHOULD_BUILD="true"
|
||||
BUILD_REASON="${BUILD_REASON} interactive"
|
||||
elif [ "$DOCKER_REGISTRY" = "docker.io" ]; then
|
||||
source ./build-functions/get-public-image-config.sh
|
||||
IFS=':' read -ra DOCKER_FROM_SPLIT <<< "${DOCKER_FROM}"
|
||||
if ! [[ ${DOCKER_FROM_SPLIT[0]} =~ .*/.* ]]; then
|
||||
# Need to use "library/..." for images the have no two part name
|
||||
DOCKER_FROM_SPLIT[0]="library/${DOCKER_FROM_SPLIT[0]}"
|
||||
fi
|
||||
PYTHON_LAST_LAYER=$(get_image_last_layer "${DOCKER_FROM_SPLIT[0]}" "${DOCKER_FROM_SPLIT[1]}")
|
||||
mapfile -t IMAGES_LAYERS_OLD < <(get_image_layers "${DOCKER_ORG}"/"${DOCKER_REPO}" "${TAG}")
|
||||
NETBOX_GIT_REF_OLD=$(get_image_label NETBOX_GIT_REF "${DOCKER_ORG}"/"${DOCKER_REPO}" "${TAG}")
|
||||
GIT_REF_OLD=$(get_image_label org.label-schema.vcs-ref "${DOCKER_ORG}"/"${DOCKER_REPO}" "${TAG}")
|
||||
|
||||
if ! printf '%s\n' "${IMAGES_LAYERS_OLD[@]}" | grep -q -P "^${PYTHON_LAST_LAYER}\$"; then
|
||||
SHOULD_BUILD="true"
|
||||
BUILD_REASON="${BUILD_REASON} python"
|
||||
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
|
||||
else
|
||||
SHOULD_BUILD="true"
|
||||
BUILD_REASON="${BUILD_REASON} no-check"
|
||||
fi
|
||||
###
|
||||
# Composing all arguments for `docker build`
|
||||
###
|
||||
@ -244,32 +296,35 @@ for DOCKER_TARGET in "${DOCKER_TARGETS[@]}"; do
|
||||
)
|
||||
if [ -n "${TARGET_DOCKER_SHORT_TAG}" ]; then
|
||||
DOCKER_BUILD_ARGS+=( -t "${TARGET_DOCKER_SHORT_TAG}" )
|
||||
DOCKER_BUILD_ARGS+=( -t "${TARGET_DOCKER_LATEST_TAG}" )
|
||||
fi
|
||||
|
||||
# --label
|
||||
if [ "${DOCKER_TARGET}" == "main" ]; then
|
||||
DOCKER_BUILD_ARGS+=(
|
||||
--label "ORIGINAL_TAG=${TARGET_DOCKER_TAG}"
|
||||
|
||||
--label "org.label-schema.build-date=${BUILD_DATE}"
|
||||
--label "org.opencontainers.image.created=${BUILD_DATE}"
|
||||
|
||||
--label "org.label-schema.version=${PROJECT_VERSION}"
|
||||
--label "org.opencontainers.image.version=${PROJECT_VERSION}"
|
||||
)
|
||||
if [ -d ".git" ]; then
|
||||
DOCKER_BUILD_ARGS+=(
|
||||
--label "ORIGINAL_TAG=${TARGET_DOCKER_TAG}"
|
||||
|
||||
--label "org.label-schema.build-date=${BUILD_DATE}"
|
||||
--label "org.opencontainers.image.created=${BUILD_DATE}"
|
||||
|
||||
--label "org.label-schema.version=${PROJECT_VERSION}"
|
||||
--label "org.opencontainers.image.version=${PROJECT_VERSION}"
|
||||
--label "org.label-schema.vcs-ref=${GIT_REF}"
|
||||
--label "org.opencontainers.image.revision=${GIT_REF}"
|
||||
)
|
||||
if [ -d ".git" ]; then
|
||||
DOCKER_BUILD_ARGS+=(
|
||||
--label "org.label-schema.vcs-ref=${GIT_REF}"
|
||||
--label "org.opencontainers.image.revision=${GIT_REF}"
|
||||
)
|
||||
fi
|
||||
if [ -d "${NETBOX_PATH}/.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
|
||||
fi
|
||||
if [ -d "${NETBOX_PATH}/.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 "BUILD_REASON=${BUILD_REASON}" )
|
||||
fi
|
||||
|
||||
# --build-arg
|
||||
@ -289,23 +344,29 @@ for DOCKER_TARGET in "${DOCKER_TARGETS[@]}"; do
|
||||
###
|
||||
# Building the docker image
|
||||
###
|
||||
echo "🐳 Building the Docker image '${TARGET_DOCKER_TAG}'."
|
||||
$DRY docker build "${DOCKER_BUILD_ARGS[@]}" .
|
||||
echo "✅ Finished building the Docker images '${TARGET_DOCKER_TAG}'"
|
||||
if [ "${SHOULD_BUILD}" == "true" ]; then
|
||||
echo "🐳 Building the Docker image '${TARGET_DOCKER_TAG}'."
|
||||
echo " Build reason set to: ${BUILD_REASON}"
|
||||
$DRY docker build "${DOCKER_BUILD_ARGS[@]}" .
|
||||
echo "✅ Finished building the Docker images '${TARGET_DOCKER_TAG}'"
|
||||
echo "🔎 Inspecting labels on '${TARGET_DOCKER_TAG}'"
|
||||
$DRY docker inspect "${TARGET_DOCKER_TAG}" --format "{{json .Config.Labels}}"
|
||||
else
|
||||
echo "Build skipped because sources didn't change"
|
||||
echo "::set-output name=skipped::true"
|
||||
fi
|
||||
fi
|
||||
|
||||
###
|
||||
# Pushing the docker images if either `--push` or `--push-only` are passed
|
||||
###
|
||||
if [ "${2}" == "--push" ] || [ "${2}" == "--push-only" ] ; then
|
||||
echo "⏫ Pushing '${TARGET_DOCKER_TAG}"
|
||||
$DRY docker push "${TARGET_DOCKER_TAG}"
|
||||
echo "✅ Finished pushing the Docker image '${TARGET_DOCKER_TAG}'."
|
||||
source ./build-functions/docker-functions.sh
|
||||
push_image_to_registry "${TARGET_DOCKER_TAG}"
|
||||
|
||||
if [ -n "${TARGET_DOCKER_SHORT_TAG}" ]; then
|
||||
echo "⏫ Pushing '${TARGET_DOCKER_SHORT_TAG}'"
|
||||
$DRY docker push "${TARGET_DOCKER_SHORT_TAG}"
|
||||
echo "✅ Finished pushing the Docker image '${TARGET_DOCKER_SHORT_TAG}'."
|
||||
push_image_to_registry "${TARGET_DOCKER_SHORT_TAG}"
|
||||
push_image_to_registry "${TARGET_DOCKER_LATEST_TAG}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
@ -51,7 +51,15 @@ SECRET_KEY = os.environ.get('SECRET_KEY', read_secret('secret_key'))
|
||||
|
||||
# Redis database settings. The Redis database is used for caching and background processing such as webhooks
|
||||
REDIS = {
|
||||
'webhooks': {
|
||||
'tasks': {
|
||||
'HOST': os.environ.get('REDIS_HOST', 'localhost'),
|
||||
'PORT': int(os.environ.get('REDIS_PORT', 6379)),
|
||||
'PASSWORD': os.environ.get('REDIS_PASSWORD', read_secret('redis_password')),
|
||||
'DATABASE': int(os.environ.get('REDIS_DATABASE', 0)),
|
||||
'DEFAULT_TIMEOUT': int(os.environ.get('REDIS_TIMEOUT', 300)),
|
||||
'SSL': os.environ.get('REDIS_SSL', 'False').lower() == 'true',
|
||||
},
|
||||
'webhooks': { # legacy setting, can be removed after Netbox seizes support for it
|
||||
'HOST': os.environ.get('REDIS_HOST', 'localhost'),
|
||||
'PORT': int(os.environ.get('REDIS_PORT', 6379)),
|
||||
'PASSWORD': os.environ.get('REDIS_PASSWORD', read_secret('redis_password')),
|
||||
@ -119,6 +127,10 @@ EMAIL = {
|
||||
'PASSWORD': os.environ.get('EMAIL_PASSWORD', read_secret('email_password')),
|
||||
'TIMEOUT': int(os.environ.get('EMAIL_TIMEOUT', 10)), # seconds
|
||||
'FROM_EMAIL': os.environ.get('EMAIL_FROM', ''),
|
||||
'USE_SSL': os.environ.get('EMAIL_USE_SSL', 'False').lower() == 'true',
|
||||
'USE_TLS': os.environ.get('EMAIL_USE_TLS', 'False').lower() == 'true',
|
||||
'SSL_CERTFILE': os.environ.get('EMAIL_SSL_CERTFILE', ''),
|
||||
'SSL_KEYFILE': os.environ.get('EMAIL_SSL_KEYFILE', ''),
|
||||
}
|
||||
|
||||
# Enforcement of unique IP space can be toggled on a per-VRF basis.
|
||||
@ -138,6 +150,10 @@ LOGGING = {}
|
||||
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
|
||||
LOGIN_REQUIRED = os.environ.get('LOGIN_REQUIRED', 'False').lower() == 'true'
|
||||
|
||||
# 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 = os.environ.get('LOGIN_TIMEOUT', None)
|
||||
|
||||
# Setting this to True will display a "maintenance mode" banner at the top of every page.
|
||||
MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', 'False').lower() == 'true'
|
||||
|
||||
@ -170,6 +186,15 @@ PAGINATE_COUNT = int(os.environ.get('PAGINATE_COUNT', 50))
|
||||
# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to
|
||||
# prefer IPv4 instead.
|
||||
PREFER_IPV4 = os.environ.get('PREFER_IPV4', 'False').lower() == 'true'
|
||||
|
||||
# This determines how often the GitHub API is called to check the latest release of NetBox in seconds. Must be at least 1 hour.
|
||||
RELEASE_CHECK_TIMEOUT = os.environ.get('RELEASE_CHECK_TIMEOUT', 24 * 3600)
|
||||
|
||||
# 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.
|
||||
# https://api.github.com/repos/netbox-community/netbox/releases
|
||||
RELEASE_CHECK_URL = os.environ.get('RELEASE_CHECK_URL', None)
|
||||
|
||||
# The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of
|
||||
# this setting is derived from the installed location.
|
||||
REPORTS_ROOT = os.environ.get('REPORTS_ROOT', '/etc/netbox/reports')
|
||||
|
@ -37,6 +37,9 @@ AUTH_LDAP_BIND_PASSWORD = os.environ.get('AUTH_LDAP_BIND_PASSWORD', read_secret(
|
||||
# Set a string template that describes any user’s distinguished name based on the username.
|
||||
AUTH_LDAP_USER_DN_TEMPLATE = os.environ.get('AUTH_LDAP_USER_DN_TEMPLATE', None)
|
||||
|
||||
# Enable STARTTLS for ldap authentication.
|
||||
AUTH_LDAP_START_TLS = os.environ.get('AUTH_LDAP_START_TLS', 'False').lower() == 'true'
|
||||
|
||||
# Include this setting if you want to ignore certificate errors. This might be needed to accept a self-signed cert.
|
||||
# Note that this is a NetBox-specific setting which sets:
|
||||
# ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
||||
|
@ -5,6 +5,7 @@ services:
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- redis-cache
|
||||
env_file: env/netbox.env
|
||||
user: '101'
|
||||
volumes:
|
||||
@ -34,8 +35,15 @@ services:
|
||||
command:
|
||||
- sh
|
||||
- -c # this is to evaluate the $REDIS_PASSWORD from the env
|
||||
- redis-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
|
||||
- redis-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
|
||||
env_file: env/redis.env
|
||||
redis-cache:
|
||||
image: redis:5-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c # this is to evaluate the $REDIS_PASSWORD from the env
|
||||
- redis-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
|
||||
env_file: env/redis-cache.env
|
||||
volumes:
|
||||
netbox-static-files:
|
||||
driver: local
|
||||
|
@ -57,7 +57,7 @@ services:
|
||||
- sh
|
||||
- -c # this is to evaluate the $REDIS_PASSWORD from the env
|
||||
- redis-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
|
||||
env_file: env/redis.env
|
||||
env_file: env/redis-cache.env
|
||||
volumes:
|
||||
netbox-static-files:
|
||||
driver: local
|
||||
|
@ -29,7 +29,6 @@ http {
|
||||
proxy_pass http://netbox:8001;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
|
||||
}
|
||||
}
|
||||
|
8
env/netbox.env
vendored
8
env/netbox.env
vendored
@ -9,6 +9,11 @@ EMAIL_USERNAME=netbox
|
||||
EMAIL_PASSWORD=
|
||||
EMAIL_TIMEOUT=5
|
||||
EMAIL_FROM=netbox@bar.com
|
||||
# EMAIL_USE_SSL and EMAIL_USE_TLS are mutually exclusive, i.e. they can't both be `true`!
|
||||
EMAIL_USE_SSL=false
|
||||
EMAIL_USE_TLS=false
|
||||
EMAIL_SSL_CERTFILE=
|
||||
EMAIL_SSL_KEYFILE=
|
||||
MEDIA_ROOT=/opt/netbox/netbox/media
|
||||
NAPALM_USERNAME=
|
||||
NAPALM_PASSWORD=
|
||||
@ -20,7 +25,7 @@ REDIS_DATABASE=0
|
||||
REDIS_SSL=false
|
||||
REDIS_CACHE_HOST=redis-cache
|
||||
REDIS_CACHE_PASSWORD=t4Ph722qJ5QHeQ1qfu36
|
||||
REDIS_CACHE_DATABASE=0
|
||||
REDIS_CACHE_DATABASE=1
|
||||
REDIS_CACHE_SSL=false
|
||||
SECRET_KEY=r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj
|
||||
SKIP_STARTUP_SCRIPTS=false
|
||||
@ -30,3 +35,4 @@ SUPERUSER_EMAIL=admin@example.com
|
||||
SUPERUSER_PASSWORD=admin
|
||||
SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567
|
||||
WEBHOOKS_ENABLED=true
|
||||
RELEASE_CHECK_URL=https://api.github.com/repos/netbox-community/netbox/releases
|
||||
|
@ -1,4 +1,4 @@
|
||||
from dcim.constants import CONNECTION_STATUS_PLANNED, DEVICE_STATUS_ACTIVE
|
||||
from dcim.choices import DeviceStatusChoices
|
||||
from dcim.models import ConsolePort, Device, PowerPort
|
||||
from extras.reports import Report
|
||||
|
||||
@ -9,13 +9,14 @@ class DeviceConnectionsReport(Report):
|
||||
def test_console_connection(self):
|
||||
|
||||
# Check that every console port for every active device has a connection defined.
|
||||
for console_port in ConsolePort.objects.select_related('device').filter(device__status=DEVICE_STATUS_ACTIVE):
|
||||
active = DeviceStatusChoices.STATUS_ACTIVE
|
||||
for console_port in ConsolePort.objects.prefetch_related('device').filter(device__status=active):
|
||||
if console_port.connected_endpoint is None:
|
||||
self.log_failure(
|
||||
console_port.device,
|
||||
"No console connection defined for {}".format(console_port.name)
|
||||
)
|
||||
elif console_port.connection_status == CONNECTION_STATUS_PLANNED:
|
||||
elif not console_port.connection_status:
|
||||
self.log_warning(
|
||||
console_port.device,
|
||||
"Console connection for {} marked as planned".format(console_port.name)
|
||||
@ -26,12 +27,12 @@ class DeviceConnectionsReport(Report):
|
||||
def test_power_connections(self):
|
||||
|
||||
# Check that every active device has at least two connected power supplies.
|
||||
for device in Device.objects.filter(status=DEVICE_STATUS_ACTIVE):
|
||||
for device in Device.objects.filter(status=DeviceStatusChoices.STATUS_ACTIVE):
|
||||
connected_ports = 0
|
||||
for power_port in PowerPort.objects.filter(device=device):
|
||||
if power_port.connected_endpoint is not None:
|
||||
connected_ports += 1
|
||||
if power_port.connection_status == CONNECTION_STATUS_PLANNED:
|
||||
if not power_port.connection_status:
|
||||
self.log_warning(
|
||||
device,
|
||||
"Power connection for {} marked as planned".format(power_port.name)
|
||||
@ -43,4 +44,3 @@ class DeviceConnectionsReport(Report):
|
||||
)
|
||||
else:
|
||||
self.log_success(device)
|
||||
|
||||
|
@ -1,42 +1,23 @@
|
||||
from django.contrib.auth.models import Permission, Group, User
|
||||
from users.models import Token
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/users.yml')
|
||||
if not file.is_file():
|
||||
from django.contrib.auth.models import Group, User
|
||||
from startup_script_utils import load_yaml, set_permissions
|
||||
from users.models import Token
|
||||
|
||||
users = load_yaml('/opt/netbox/initializers/users.yml')
|
||||
if users is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml=YAML(typ='safe')
|
||||
users = yaml.load(stream)
|
||||
for username, user_details in users.items():
|
||||
if not User.objects.filter(username=username):
|
||||
user = User.objects.create_user(
|
||||
username = username,
|
||||
password = user_details.get('password', 0) or User.objects.make_random_password())
|
||||
|
||||
if users is not None:
|
||||
for username, user_details in users.items():
|
||||
if not User.objects.filter(username=username):
|
||||
user = User.objects.create_user(
|
||||
username = username,
|
||||
password = user_details.get('password', 0) or User.objects.make_random_password)
|
||||
print("👤 Created user",username)
|
||||
|
||||
print("👤 Created user",username)
|
||||
if user_details.get('api_token', 0):
|
||||
Token.objects.create(user=user, key=user_details['api_token'])
|
||||
|
||||
if user_details.get('api_token', 0):
|
||||
Token.objects.create(user=user, key=user_details['api_token'])
|
||||
|
||||
yaml_permissions = user_details.get('permissions', [])
|
||||
if yaml_permissions:
|
||||
subject = user.user_permissions
|
||||
subject.clear()
|
||||
for yaml_permission in yaml_permissions:
|
||||
if '*' in yaml_permission:
|
||||
permission_filter = '^' + yaml_permission.replace('*','.*') + '$'
|
||||
permissions = Permission.objects.filter(codename__iregex=permission_filter)
|
||||
print(" ⚿ Granting", permissions.count(), "permissions matching '" + yaml_permission + "'")
|
||||
else:
|
||||
permissions = Permission.objects.filter(codename=yaml_permission)
|
||||
print(" ⚿ Granting permission", yaml_permission)
|
||||
|
||||
for permission in permissions:
|
||||
subject.add(permission)
|
||||
yaml_permissions = user_details.get('permissions', [])
|
||||
set_permissions(user.user_permissions, yaml_permissions)
|
||||
|
@ -1,41 +1,23 @@
|
||||
from django.contrib.auth.models import Permission, Group, User
|
||||
from ruamel.yaml import YAML
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/groups.yml')
|
||||
if not file.is_file():
|
||||
from django.contrib.auth.models import Group, User
|
||||
from startup_script_utils import load_yaml, set_permissions
|
||||
|
||||
groups = load_yaml('/opt/netbox/initializers/groups.yml')
|
||||
if groups is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml=YAML(typ='safe')
|
||||
groups = yaml.load(stream)
|
||||
for groupname, group_details in groups.items():
|
||||
group, created = Group.objects.get_or_create(name=groupname)
|
||||
|
||||
if groups is not None:
|
||||
for groupname, group_details in groups.items():
|
||||
group, created = Group.objects.get_or_create(name=groupname)
|
||||
if created:
|
||||
print("👥 Created group", groupname)
|
||||
|
||||
if created:
|
||||
print("👥 Created group", groupname)
|
||||
for username in group_details.get('users', []):
|
||||
user = User.objects.get(username=username)
|
||||
|
||||
for username in group_details.get('users', []):
|
||||
user = User.objects.get(username=username)
|
||||
if user:
|
||||
user.groups.add(group)
|
||||
|
||||
if user:
|
||||
user.groups.add(group)
|
||||
|
||||
yaml_permissions = group_details.get('permissions', [])
|
||||
if yaml_permissions:
|
||||
subject = group.permissions
|
||||
subject.clear()
|
||||
for yaml_permission in yaml_permissions:
|
||||
if '*' in yaml_permission:
|
||||
permission_filter = '^' + yaml_permission.replace('*','.*') + '$'
|
||||
permissions = Permission.objects.filter(codename__iregex=permission_filter)
|
||||
print(" ⚿ Granting", permissions.count(), "permissions matching '" + yaml_permission + "'")
|
||||
else:
|
||||
permissions = Permission.objects.filter(codename=yaml_permission)
|
||||
print(" ⚿ Granting permission", yaml_permission)
|
||||
|
||||
for permission in permissions:
|
||||
subject.add(permission)
|
||||
yaml_permissions = group_details.get('permissions', [])
|
||||
set_permissions(group.permissions, yaml_permissions)
|
||||
|
@ -1,7 +1,6 @@
|
||||
from extras.models import CustomField, CustomFieldChoice
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
def get_class_for_class_path(class_path):
|
||||
@ -13,47 +12,43 @@ def get_class_for_class_path(class_path):
|
||||
clazz = getattr(module, class_name)
|
||||
return ContentType.objects.get_for_model(clazz)
|
||||
|
||||
file = Path('/opt/netbox/initializers/custom_fields.yml')
|
||||
if not file.is_file():
|
||||
customfields = load_yaml('/opt/netbox/initializers/custom_fields.yml')
|
||||
|
||||
if customfields is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
customfields = yaml.load(stream)
|
||||
for cf_name, cf_details in customfields.items():
|
||||
custom_field, created = CustomField.objects.get_or_create(name = cf_name)
|
||||
|
||||
if customfields is not None:
|
||||
for cf_name, cf_details in customfields.items():
|
||||
custom_field, created = CustomField.objects.get_or_create(name = cf_name)
|
||||
if created:
|
||||
if cf_details.get('default', 0):
|
||||
custom_field.default = cf_details['default']
|
||||
|
||||
if created:
|
||||
if cf_details.get('default', 0):
|
||||
custom_field.default = cf_details['default']
|
||||
if cf_details.get('description', 0):
|
||||
custom_field.description = cf_details['description']
|
||||
|
||||
if cf_details.get('description', 0):
|
||||
custom_field.description = cf_details['description']
|
||||
if cf_details.get('label', 0):
|
||||
custom_field.label = cf_details['label']
|
||||
|
||||
if cf_details.get('label', 0):
|
||||
custom_field.label = cf_details['label']
|
||||
for object_type in cf_details.get('on_objects', []):
|
||||
custom_field.obj_type.add(get_class_for_class_path(object_type))
|
||||
|
||||
for object_type in cf_details.get('on_objects', []):
|
||||
custom_field.obj_type.add(get_class_for_class_path(object_type))
|
||||
if cf_details.get('required', 0):
|
||||
custom_field.required = cf_details['required']
|
||||
|
||||
if cf_details.get('required', 0):
|
||||
custom_field.required = cf_details['required']
|
||||
if cf_details.get('type', 0):
|
||||
custom_field.type = cf_details['type']
|
||||
|
||||
if cf_details.get('type', 0):
|
||||
custom_field.type = cf_details['type']
|
||||
if cf_details.get('weight', 0):
|
||||
custom_field.weight = cf_details['weight']
|
||||
|
||||
if cf_details.get('weight', 0):
|
||||
custom_field.weight = cf_details['weight']
|
||||
custom_field.save()
|
||||
|
||||
custom_field.save()
|
||||
for idx, choice_details in enumerate(cf_details.get('choices', [])):
|
||||
choice, _ = CustomFieldChoice.objects.get_or_create(
|
||||
field=custom_field,
|
||||
value=choice_details['value'],
|
||||
defaults={'weight': idx * 10}
|
||||
)
|
||||
|
||||
for idx, choice_details in enumerate(cf_details.get('choices', [])):
|
||||
choice, _ = CustomFieldChoice.objects.get_or_create(
|
||||
field=custom_field,
|
||||
value=choice_details['value'],
|
||||
defaults={'weight': idx * 10}
|
||||
)
|
||||
|
||||
print("🔧 Created custom field", cf_name)
|
||||
print("🔧 Created custom field", cf_name)
|
||||
|
@ -1,31 +1,26 @@
|
||||
from dcim.models import Region
|
||||
from ruamel.yaml import YAML
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/regions.yml')
|
||||
if not file.is_file():
|
||||
regions = load_yaml('/opt/netbox/initializers/regions.yml')
|
||||
|
||||
if regions is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml=YAML(typ='safe')
|
||||
regions = yaml.load(stream)
|
||||
optional_assocs = {
|
||||
'parent': (Region, 'name')
|
||||
}
|
||||
|
||||
optional_assocs = {
|
||||
'parent': (Region, 'name')
|
||||
}
|
||||
for params in regions:
|
||||
|
||||
if regions is not None:
|
||||
for params in regions:
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
region, created = Region.objects.get_or_create(**params)
|
||||
|
||||
region, created = Region.objects.get_or_create(**params)
|
||||
|
||||
if created:
|
||||
print("🌐 Created region", region.name)
|
||||
if created:
|
||||
print("🌐 Created region", region.name)
|
||||
|
@ -1,46 +1,41 @@
|
||||
from dcim.models import Region, Site
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
from tenancy.models import Tenant
|
||||
from ruamel.yaml import YAML
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/sites.yml')
|
||||
if not file.is_file():
|
||||
sites = load_yaml('/opt/netbox/initializers/sites.yml')
|
||||
|
||||
if sites is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
sites = yaml.load(stream)
|
||||
optional_assocs = {
|
||||
'region': (Region, 'name'),
|
||||
'tenant': (Tenant, 'name')
|
||||
}
|
||||
|
||||
optional_assocs = {
|
||||
'region': (Region, 'name'),
|
||||
'tenant': (Tenant, 'name')
|
||||
}
|
||||
for params in sites:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
|
||||
if sites is not None:
|
||||
for params in sites:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
site, created = Site.objects.get_or_create(**params)
|
||||
|
||||
site, created = Site.objects.get_or_create(**params)
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=site,
|
||||
value=cf_value
|
||||
)
|
||||
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=site,
|
||||
value=cf_value
|
||||
)
|
||||
site.custom_field_values.add(custom_field_value)
|
||||
|
||||
site.custom_field_values.add(custom_field_value)
|
||||
|
||||
print("📍 Created site", site.name)
|
||||
print("📍 Created site", site.name)
|
||||
|
@ -1,19 +1,14 @@
|
||||
from dcim.models import Manufacturer
|
||||
from ruamel.yaml import YAML
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/manufacturers.yml')
|
||||
if not file.is_file():
|
||||
manufacturers = load_yaml('/opt/netbox/initializers/manufacturers.yml')
|
||||
|
||||
if manufacturers is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
manufacturers = yaml.load(stream)
|
||||
for params in manufacturers:
|
||||
manufacturer, created = Manufacturer.objects.get_or_create(**params)
|
||||
|
||||
if manufacturers is not None:
|
||||
for params in manufacturers:
|
||||
manufacturer, created = Manufacturer.objects.get_or_create(**params)
|
||||
|
||||
if created:
|
||||
print("🏭 Created Manufacturer", manufacturer.name)
|
||||
if created:
|
||||
print("🏭 Created Manufacturer", manufacturer.name)
|
||||
|
@ -1,56 +1,51 @@
|
||||
from dcim.models import DeviceType, Manufacturer, Region
|
||||
from tenancy.models import Tenant
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
from ruamel.yaml import YAML
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/device_types.yml')
|
||||
if not file.is_file():
|
||||
device_types = load_yaml('/opt/netbox/initializers/device_types.yml')
|
||||
|
||||
if device_types is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
device_types = yaml.load(stream)
|
||||
required_assocs = {
|
||||
'manufacturer': (Manufacturer, 'name')
|
||||
}
|
||||
|
||||
required_assocs = {
|
||||
'manufacturer': (Manufacturer, 'name')
|
||||
}
|
||||
optional_assocs = {
|
||||
'region': (Region, 'name'),
|
||||
'tenant': (Tenant, 'name')
|
||||
}
|
||||
|
||||
optional_assocs = {
|
||||
'region': (Region, 'name'),
|
||||
'tenant': (Tenant, 'name')
|
||||
}
|
||||
for params in device_types:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
|
||||
if device_types is not None:
|
||||
for params in device_types:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
device_type, created = DeviceType.objects.get_or_create(**params)
|
||||
|
||||
device_type, created = DeviceType.objects.get_or_create(**params)
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=device_type,
|
||||
value=cf_value
|
||||
)
|
||||
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=device_type,
|
||||
value=cf_value
|
||||
)
|
||||
device_type.custom_field_values.add(custom_field_value)
|
||||
|
||||
device_type.custom_field_values.add(custom_field_value)
|
||||
|
||||
print("🔡 Created device type", device_type.manufacturer, device_type.model)
|
||||
print("🔡 Created device type", device_type.manufacturer, device_type.model)
|
||||
|
@ -1,28 +1,23 @@
|
||||
from dcim.models import RackRole
|
||||
from ruamel.yaml import YAML
|
||||
from utilities.forms import COLOR_CHOICES
|
||||
from utilities.choices import ColorChoices
|
||||
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/rack_roles.yml')
|
||||
if not file.is_file():
|
||||
rack_roles = load_yaml('/opt/netbox/initializers/rack_roles.yml')
|
||||
|
||||
if rack_roles is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml=YAML(typ='safe')
|
||||
rack_roles = yaml.load(stream)
|
||||
for params in rack_roles:
|
||||
if 'color' in params:
|
||||
color = params.pop('color')
|
||||
|
||||
if rack_roles is not None:
|
||||
for params in rack_roles:
|
||||
if 'color' in params:
|
||||
color = params.pop('color')
|
||||
for color_tpl in ColorChoices:
|
||||
if color in color_tpl:
|
||||
params['color'] = color_tpl[0]
|
||||
|
||||
for color_tpl in COLOR_CHOICES:
|
||||
if color in color_tpl:
|
||||
params['color'] = color_tpl[0]
|
||||
rack_role, created = RackRole.objects.get_or_create(**params)
|
||||
|
||||
rack_role, created = RackRole.objects.get_or_create(**params)
|
||||
|
||||
if created:
|
||||
print("🎨 Created rack role", rack_role.name)
|
||||
if created:
|
||||
print("🎨 Created rack role", rack_role.name)
|
||||
|
@ -1,31 +1,25 @@
|
||||
from dcim.models import Site,RackGroup
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/rack_groups.yml')
|
||||
if not file.is_file():
|
||||
rack_groups = load_yaml('/opt/netbox/initializers/rack_groups.yml')
|
||||
|
||||
if rack_groups is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml=YAML(typ='safe')
|
||||
rack_groups= yaml.load(stream)
|
||||
required_assocs = {
|
||||
'site': (Site, 'name')
|
||||
}
|
||||
|
||||
required_assocs = {
|
||||
'site': (Site, 'name')
|
||||
}
|
||||
for params in rack_groups:
|
||||
|
||||
if rack_groups is not None:
|
||||
for params in rack_groups:
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
rack_group, created = RackGroup.objects.get_or_create(**params)
|
||||
|
||||
rack_group, created = RackGroup.objects.get_or_create(**params)
|
||||
|
||||
if created:
|
||||
print("🎨 Created rack group", rack_group.name)
|
||||
if created:
|
||||
print("🎨 Created rack group", rack_group.name)
|
||||
|
||||
|
@ -1,57 +1,52 @@
|
||||
from dcim.models import Site, RackRole, Rack, RackGroup
|
||||
from tenancy.models import Tenant
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
from ruamel.yaml import YAML
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/racks.yml')
|
||||
if not file.is_file():
|
||||
racks = load_yaml('/opt/netbox/initializers/racks.yml')
|
||||
|
||||
if racks is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
racks = yaml.load(stream)
|
||||
required_assocs = {
|
||||
'site': (Site, 'name')
|
||||
}
|
||||
|
||||
required_assocs = {
|
||||
'site': (Site, 'name')
|
||||
}
|
||||
optional_assocs = {
|
||||
'role': (RackRole, 'name'),
|
||||
'tenant': (Tenant, 'name'),
|
||||
'group': (RackGroup, 'name')
|
||||
}
|
||||
|
||||
optional_assocs = {
|
||||
'role': (RackRole, 'name'),
|
||||
'tenant': (Tenant, 'name'),
|
||||
'group': (RackGroup, 'name')
|
||||
}
|
||||
for params in racks:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
|
||||
if racks is not None:
|
||||
for params in racks:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
rack, created = Rack.objects.get_or_create(**params)
|
||||
|
||||
rack, created = Rack.objects.get_or_create(**params)
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=rack,
|
||||
value=cf_value
|
||||
)
|
||||
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=rack,
|
||||
value=cf_value
|
||||
)
|
||||
rack.custom_field_values.add(custom_field_value)
|
||||
|
||||
rack.custom_field_values.add(custom_field_value)
|
||||
|
||||
print("🔳 Created rack", rack.site, rack.name)
|
||||
print("🔳 Created rack", rack.site, rack.name)
|
||||
|
@ -1,29 +1,24 @@
|
||||
from dcim.models import DeviceRole
|
||||
from ruamel.yaml import YAML
|
||||
from utilities.forms import COLOR_CHOICES
|
||||
from utilities.choices import ColorChoices
|
||||
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/device_roles.yml')
|
||||
if not file.is_file():
|
||||
device_roles = load_yaml('/opt/netbox/initializers/device_roles.yml')
|
||||
|
||||
if device_roles is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml=YAML(typ='safe')
|
||||
device_roles = yaml.load(stream)
|
||||
for params in device_roles:
|
||||
|
||||
if device_roles is not None:
|
||||
for params in device_roles:
|
||||
if 'color' in params:
|
||||
color = params.pop('color')
|
||||
|
||||
if 'color' in params:
|
||||
color = params.pop('color')
|
||||
for color_tpl in ColorChoices:
|
||||
if color in color_tpl:
|
||||
params['color'] = color_tpl[0]
|
||||
|
||||
for color_tpl in COLOR_CHOICES:
|
||||
if color in color_tpl:
|
||||
params['color'] = color_tpl[0]
|
||||
device_role, created = DeviceRole.objects.get_or_create(**params)
|
||||
|
||||
device_role, created = DeviceRole.objects.get_or_create(**params)
|
||||
|
||||
if created:
|
||||
print("🎨 Created device role", device_role.name)
|
||||
if created:
|
||||
print("🎨 Created device role", device_role.name)
|
||||
|
@ -1,32 +1,26 @@
|
||||
from dcim.models import Manufacturer, Platform
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/platforms.yml')
|
||||
if not file.is_file():
|
||||
platforms = load_yaml('/opt/netbox/initializers/platforms.yml')
|
||||
|
||||
if platforms is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
platforms = yaml.load(stream)
|
||||
optional_assocs = {
|
||||
'manufacturer': (Manufacturer, 'name'),
|
||||
}
|
||||
|
||||
optional_assocs = {
|
||||
'manufacturer': (Manufacturer, 'name'),
|
||||
}
|
||||
for params in platforms:
|
||||
|
||||
if platforms is not None:
|
||||
for params in platforms:
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
platform, created = Platform.objects.get_or_create(**params)
|
||||
|
||||
platform, created = Platform.objects.get_or_create(**params)
|
||||
|
||||
if created:
|
||||
print("💾 Created platform", platform.name)
|
||||
if created:
|
||||
print("💾 Created platform", platform.name)
|
||||
|
@ -1,19 +1,14 @@
|
||||
from tenancy.models import TenantGroup
|
||||
from ruamel.yaml import YAML
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/tenant_groups.yml')
|
||||
if not file.is_file():
|
||||
tenant_groups = load_yaml('/opt/netbox/initializers/tenant_groups.yml')
|
||||
|
||||
if tenant_groups is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
tenant_groups = yaml.load(stream)
|
||||
for params in tenant_groups:
|
||||
tenant_group, created = TenantGroup.objects.get_or_create(**params)
|
||||
|
||||
if tenant_groups is not None:
|
||||
for params in tenant_groups:
|
||||
tenant_group, created = TenantGroup.objects.get_or_create(**params)
|
||||
|
||||
if created:
|
||||
print("🔳 Created Tenant Group", tenant_group.name)
|
||||
if created:
|
||||
print("🔳 Created Tenant Group", tenant_group.name)
|
||||
|
@ -1,45 +1,39 @@
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/tenants.yml')
|
||||
if not file.is_file():
|
||||
tenants = load_yaml('/opt/netbox/initializers/tenants.yml')
|
||||
|
||||
if tenants is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
tenants = yaml.load(stream)
|
||||
optional_assocs = {
|
||||
'group': (TenantGroup, 'name')
|
||||
}
|
||||
|
||||
optional_assocs = {
|
||||
'group': (TenantGroup, 'name')
|
||||
}
|
||||
for params in tenants:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
|
||||
if tenants is not None:
|
||||
for params in tenants:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
tenant, created = Tenant.objects.get_or_create(**params)
|
||||
|
||||
tenant, created = Tenant.objects.get_or_create(**params)
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=tenant,
|
||||
value=cf_value
|
||||
)
|
||||
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=tenant,
|
||||
value=cf_value
|
||||
)
|
||||
tenant.custom_field_values.add(custom_field_value)
|
||||
|
||||
tenant.custom_field_values.add(custom_field_value)
|
||||
|
||||
print("👩💻 Created Tenant", tenant.name)
|
||||
print("👩💻 Created Tenant", tenant.name)
|
||||
|
@ -3,63 +3,57 @@ from ipam.models import IPAddress
|
||||
from virtualization.models import Cluster
|
||||
from tenancy.models import Tenant
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/devices.yml')
|
||||
if not file.is_file():
|
||||
devices = load_yaml('/opt/netbox/initializers/devices.yml')
|
||||
|
||||
if devices is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
devices = yaml.load(stream)
|
||||
required_assocs = {
|
||||
'device_role': (DeviceRole, 'name'),
|
||||
'device_type': (DeviceType, 'model'),
|
||||
'site': (Site, 'name')
|
||||
}
|
||||
|
||||
required_assocs = {
|
||||
'device_role': (DeviceRole, 'name'),
|
||||
'device_type': (DeviceType, 'model'),
|
||||
'site': (Site, 'name')
|
||||
}
|
||||
optional_assocs = {
|
||||
'tenant': (Tenant, 'name'),
|
||||
'platform': (Platform, 'name'),
|
||||
'rack': (Rack, 'name'),
|
||||
'cluster': (Cluster, 'name'),
|
||||
'primary_ip4': (IPAddress, 'address'),
|
||||
'primary_ip6': (IPAddress, 'address')
|
||||
}
|
||||
|
||||
optional_assocs = {
|
||||
'tenant': (Tenant, 'name'),
|
||||
'platform': (Platform, 'name'),
|
||||
'rack': (Rack, 'name'),
|
||||
'cluster': (Cluster, 'name'),
|
||||
'primary_ip4': (IPAddress, 'address'),
|
||||
'primary_ip6': (IPAddress, 'address')
|
||||
}
|
||||
for params in devices:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
|
||||
if devices is not None:
|
||||
for params in devices:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
device, created = Device.objects.get_or_create(**params)
|
||||
|
||||
device, created = Device.objects.get_or_create(**params)
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=device,
|
||||
value=cf_value
|
||||
)
|
||||
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=device,
|
||||
value=cf_value
|
||||
)
|
||||
device.custom_field_values.add(custom_field_value)
|
||||
|
||||
device.custom_field_values.add(custom_field_value)
|
||||
|
||||
print("🖥️ Created device", device.name)
|
||||
print("🖥️ Created device", device.name)
|
||||
|
@ -1,19 +1,14 @@
|
||||
from virtualization.models import ClusterType
|
||||
from ruamel.yaml import YAML
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/cluster_types.yml')
|
||||
if not file.is_file():
|
||||
cluster_types = load_yaml('/opt/netbox/initializers/cluster_types.yml')
|
||||
|
||||
if cluster_types is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
cluster_types = yaml.load(stream)
|
||||
for params in cluster_types:
|
||||
cluster_type, created = ClusterType.objects.get_or_create(**params)
|
||||
|
||||
if cluster_types is not None:
|
||||
for params in cluster_types:
|
||||
cluster_type, created = ClusterType.objects.get_or_create(**params)
|
||||
|
||||
if created:
|
||||
print("🧰 Created Cluster Type", cluster_type.name)
|
||||
if created:
|
||||
print("🧰 Created Cluster Type", cluster_type.name)
|
||||
|
@ -1,19 +1,14 @@
|
||||
from ipam.models import RIR
|
||||
from ruamel.yaml import YAML
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/rirs.yml')
|
||||
if not file.is_file():
|
||||
rirs = load_yaml('/opt/netbox/initializers/rirs.yml')
|
||||
|
||||
if rirs is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
rirs = yaml.load(stream)
|
||||
for params in rirs:
|
||||
rir, created = RIR.objects.get_or_create(**params)
|
||||
|
||||
if rirs is not None:
|
||||
for params in rirs:
|
||||
rir, created = RIR.objects.get_or_create(**params)
|
||||
|
||||
if created:
|
||||
print("🗺️ Created RIR", rir.name)
|
||||
if created:
|
||||
print("🗺️ Created RIR", rir.name)
|
||||
|
@ -1,46 +1,42 @@
|
||||
from ipam.models import Aggregate, RIR
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
|
||||
from netaddr import IPNetwork
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/aggregates.yml')
|
||||
if not file.is_file():
|
||||
aggregates = load_yaml('/opt/netbox/initializers/aggregates.yml')
|
||||
|
||||
if aggregates is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
aggregates = yaml.load(stream)
|
||||
required_assocs = {
|
||||
'rir': (RIR, 'name')
|
||||
}
|
||||
|
||||
required_assocs = {
|
||||
'rir': (RIR, 'name')
|
||||
}
|
||||
for params in aggregates:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
params['prefix'] = IPNetwork(params['prefix'])
|
||||
|
||||
if aggregates is not None:
|
||||
for params in aggregates:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
params['prefix'] = IPNetwork(params['prefix'])
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
aggregate, created = Aggregate.objects.get_or_create(**params)
|
||||
|
||||
aggregate, created = Aggregate.objects.get_or_create(**params)
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=aggregate,
|
||||
value=cf_value
|
||||
)
|
||||
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=aggregate,
|
||||
value=cf_value
|
||||
)
|
||||
aggregate.custom_field_values.add(custom_field_value)
|
||||
|
||||
aggregate.custom_field_values.add(custom_field_value)
|
||||
|
||||
print("🗞️ Created Aggregate", aggregate.prefix)
|
||||
print("🗞️ Created Aggregate", aggregate.prefix)
|
||||
|
@ -1,57 +1,51 @@
|
||||
from dcim.models import Site
|
||||
from virtualization.models import Cluster, ClusterType, ClusterGroup
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/clusters.yml')
|
||||
if not file.is_file():
|
||||
clusters = load_yaml('/opt/netbox/initializers/clusters.yml')
|
||||
|
||||
if clusters is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
clusters = yaml.load(stream)
|
||||
required_assocs = {
|
||||
'type': (ClusterType, 'name')
|
||||
}
|
||||
|
||||
required_assocs = {
|
||||
'type': (ClusterType, 'name')
|
||||
}
|
||||
optional_assocs = {
|
||||
'site': (Site, 'name'),
|
||||
'group': (ClusterGroup, 'name')
|
||||
}
|
||||
|
||||
optional_assocs = {
|
||||
'site': (Site, 'name'),
|
||||
'group': (ClusterGroup, 'name')
|
||||
}
|
||||
for params in clusters:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
|
||||
if clusters is not None:
|
||||
for params in clusters:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
cluster, created = Cluster.objects.get_or_create(**params)
|
||||
|
||||
cluster, created = Cluster.objects.get_or_create(**params)
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=cluster,
|
||||
value=cf_value
|
||||
)
|
||||
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=cluster,
|
||||
value=cf_value
|
||||
)
|
||||
cluster.custom_field_values.add(custom_field_value)
|
||||
|
||||
cluster.custom_field_values.add(custom_field_value)
|
||||
|
||||
print("🗄️ Created cluster", cluster.name)
|
||||
print("🗄️ Created cluster", cluster.name)
|
||||
|
@ -1,46 +1,42 @@
|
||||
from ipam.models import VRF
|
||||
from tenancy.models import Tenant
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/vrfs.yml')
|
||||
if not file.is_file():
|
||||
vrfs = load_yaml('/opt/netbox/initializers/vrfs.yml')
|
||||
|
||||
if vrfs is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
vrfs = yaml.load(stream)
|
||||
optional_assocs = {
|
||||
'tenant': (Tenant, 'name')
|
||||
}
|
||||
|
||||
optional_assocs = {
|
||||
'tenant': (Tenant, 'name')
|
||||
}
|
||||
for params in vrfs:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
|
||||
if vrfs is not None:
|
||||
for params in vrfs:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
vrf, created = VRF.objects.get_or_create(**params)
|
||||
|
||||
vrf, created = VRF.objects.get_or_create(**params)
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=vrf,
|
||||
value=cf_value
|
||||
)
|
||||
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=vrf,
|
||||
value=cf_value
|
||||
)
|
||||
vrf.custom_field_values.add(custom_field_value)
|
||||
|
||||
vrf.custom_field_values.add(custom_field_value)
|
||||
|
||||
print("📦 Created VRF", vrf.name)
|
||||
print("📦 Created VRF", vrf.name)
|
||||
|
@ -1,19 +1,14 @@
|
||||
from ipam.models import Role
|
||||
from ruamel.yaml import YAML
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/prefix_vlan_roles.yml')
|
||||
if not file.is_file():
|
||||
roles = load_yaml('/opt/netbox/initializers/prefix_vlan_roles.yml')
|
||||
|
||||
if roles is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
roles = yaml.load(stream)
|
||||
for params in roles:
|
||||
role, created = Role.objects.get_or_create(**params)
|
||||
|
||||
if roles is not None:
|
||||
for params in roles:
|
||||
role, created = Role.objects.get_or_create(**params)
|
||||
|
||||
if created:
|
||||
print("⛹️ Created Prefix/VLAN Role", role.name)
|
||||
if created:
|
||||
print("⛹️ Created Prefix/VLAN Role", role.name)
|
||||
|
@ -1,46 +1,40 @@
|
||||
from dcim.models import Site
|
||||
from ipam.models import VLANGroup
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/vlan_groups.yml')
|
||||
if not file.is_file():
|
||||
vlan_groups = load_yaml('/opt/netbox/initializers/vlan_groups.yml')
|
||||
|
||||
if vlan_groups is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
vlan_groups = yaml.load(stream)
|
||||
optional_assocs = {
|
||||
'site': (Site, 'name')
|
||||
}
|
||||
|
||||
optional_assocs = {
|
||||
'site': (Site, 'name')
|
||||
}
|
||||
for params in vlan_groups:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
|
||||
if vlan_groups is not None:
|
||||
for params in vlan_groups:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
vlan_group, created = VLANGroup.objects.get_or_create(**params)
|
||||
|
||||
vlan_group, created = VLANGroup.objects.get_or_create(**params)
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=vlan_group,
|
||||
value=cf_value
|
||||
)
|
||||
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=vlan_group,
|
||||
value=cf_value
|
||||
)
|
||||
vlan_group.custom_field_values.add(custom_field_value)
|
||||
|
||||
vlan_group.custom_field_values.add(custom_field_value)
|
||||
|
||||
print("🏘️ Created VLAN Group", vlan_group.name)
|
||||
print("🏘️ Created VLAN Group", vlan_group.name)
|
||||
|
@ -2,50 +2,44 @@ from dcim.models import Site
|
||||
from ipam.models import VLAN, VLANGroup, Role
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/vlans.yml')
|
||||
if not file.is_file():
|
||||
vlans = load_yaml('/opt/netbox/initializers/vlans.yml')
|
||||
|
||||
if vlans is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
vlans = yaml.load(stream)
|
||||
optional_assocs = {
|
||||
'site': (Site, 'name'),
|
||||
'tenant': (Tenant, 'name'),
|
||||
'tenant_group': (TenantGroup, 'name'),
|
||||
'group': (VLANGroup, 'name'),
|
||||
'role': (Role, 'name')
|
||||
}
|
||||
|
||||
optional_assocs = {
|
||||
'site': (Site, 'name'),
|
||||
'tenant': (Tenant, 'name'),
|
||||
'tenant_group': (TenantGroup, 'name'),
|
||||
'group': (VLANGroup, 'name'),
|
||||
'role': (Role, 'name')
|
||||
}
|
||||
for params in vlans:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
|
||||
if vlans is not None:
|
||||
for params in vlans:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
vlan, created = VLAN.objects.get_or_create(**params)
|
||||
|
||||
vlan, created = VLAN.objects.get_or_create(**params)
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=vlan,
|
||||
value=cf_value
|
||||
)
|
||||
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=vlan,
|
||||
value=cf_value
|
||||
)
|
||||
vlan.custom_field_values.add(custom_field_value)
|
||||
|
||||
vlan.custom_field_values.add(custom_field_value)
|
||||
|
||||
print("🏠 Created VLAN", vlan.name)
|
||||
print("🏠 Created VLAN", vlan.name)
|
||||
|
@ -2,53 +2,45 @@ from dcim.models import Site
|
||||
from ipam.models import Prefix, VLAN, Role, VRF
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from netaddr import IPNetwork
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/prefixes.yml')
|
||||
if not file.is_file():
|
||||
prefixes = load_yaml('/opt/netbox/initializers/prefixes.yml')
|
||||
|
||||
if prefixes is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
prefixes = yaml.load(stream)
|
||||
optional_assocs = {
|
||||
'site': (Site, 'name'),
|
||||
'tenant': (Tenant, 'name'),
|
||||
'tenant_group': (TenantGroup, 'name'),
|
||||
'vlan': (VLAN, 'name'),
|
||||
'role': (Role, 'name'),
|
||||
'vrf': (VRF, 'name')
|
||||
}
|
||||
|
||||
optional_assocs = {
|
||||
'site': (Site, 'name'),
|
||||
'tenant': (Tenant, 'name'),
|
||||
'tenant_group': (TenantGroup, 'name'),
|
||||
'vlan': (VLAN, 'name'),
|
||||
'role': (Role, 'name'),
|
||||
'vrf': (VRF, 'name')
|
||||
}
|
||||
for params in prefixes:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
params['prefix'] = IPNetwork(params['prefix'])
|
||||
|
||||
if prefixes is not None:
|
||||
for params in prefixes:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
params['prefix'] = IPNetwork(params['prefix'])
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
prefix, created = Prefix.objects.get_or_create(**params)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=prefix,
|
||||
value=cf_value
|
||||
)
|
||||
prefix.custom_field_values.add(custom_field_value)
|
||||
|
||||
prefix, created = Prefix.objects.get_or_create(**params)
|
||||
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=prefix,
|
||||
value=cf_value
|
||||
)
|
||||
|
||||
prefix.custom_field_values.add(custom_field_value)
|
||||
|
||||
print("📌 Created Prefix", prefix.prefix)
|
||||
print("📌 Created Prefix", prefix.prefix)
|
||||
|
@ -2,58 +2,52 @@ from dcim.models import Site, Platform, DeviceRole
|
||||
from virtualization.models import Cluster, VirtualMachine
|
||||
from tenancy.models import Tenant
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/virtual_machines.yml')
|
||||
if not file.is_file():
|
||||
virtual_machines = load_yaml('/opt/netbox/initializers/virtual_machines.yml')
|
||||
|
||||
if virtual_machines is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
virtual_machines = yaml.load(stream)
|
||||
required_assocs = {
|
||||
'cluster': (Cluster, 'name')
|
||||
}
|
||||
|
||||
required_assocs = {
|
||||
'cluster': (Cluster, 'name')
|
||||
}
|
||||
optional_assocs = {
|
||||
'tenant': (Tenant, 'name'),
|
||||
'platform': (Platform, 'name'),
|
||||
'role': (DeviceRole, 'name')
|
||||
}
|
||||
|
||||
optional_assocs = {
|
||||
'tenant': (Tenant, 'name'),
|
||||
'platform': (Platform, 'name'),
|
||||
'role': (DeviceRole, 'name')
|
||||
}
|
||||
for params in virtual_machines:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
|
||||
if virtual_machines is not None:
|
||||
for params in virtual_machines:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
virtual_machine, created = VirtualMachine.objects.get_or_create(**params)
|
||||
|
||||
virtual_machine, created = VirtualMachine.objects.get_or_create(**params)
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=virtual_machine,
|
||||
value=cf_value
|
||||
)
|
||||
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=virtual_machine,
|
||||
value=cf_value
|
||||
)
|
||||
virtual_machine.custom_field_values.add(custom_field_value)
|
||||
|
||||
virtual_machine.custom_field_values.add(custom_field_value)
|
||||
|
||||
print("🖥️ Created virtual machine", virtual_machine.name)
|
||||
print("🖥️ Created virtual machine", virtual_machine.name)
|
||||
|
@ -1,45 +1,38 @@
|
||||
from dcim.models import Interface
|
||||
from virtualization.models import VirtualMachine
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/virtualization_interfaces.yml')
|
||||
if not file.is_file():
|
||||
interfaces = load_yaml('/opt/netbox/initializers/virtualization_interfaces.yml')
|
||||
|
||||
if interfaces is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
interfaces = yaml.load(stream)
|
||||
required_assocs = {
|
||||
'virtual_machine': (VirtualMachine, 'name')
|
||||
}
|
||||
|
||||
required_assocs = {
|
||||
'virtual_machine': (VirtualMachine, 'name')
|
||||
}
|
||||
for params in interfaces:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
|
||||
if interfaces is not None:
|
||||
for params in interfaces:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
interface, created = VMInterface.objects.get_or_create(**params)
|
||||
|
||||
interface, created = Interface.objects.get_or_create(**params)
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=interface,
|
||||
value=cf_value
|
||||
)
|
||||
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=interface,
|
||||
value=cf_value
|
||||
)
|
||||
interface.custom_field_values.add(custom_field_value)
|
||||
|
||||
interface.custom_field_values.add(custom_field_value)
|
||||
|
||||
print("🧷 Created interface", interface.name, interface.virtual_machine.name)
|
||||
print("🧷 Created interface", interface.name, interface.virtual_machine.name)
|
||||
|
@ -1,44 +1,38 @@
|
||||
from dcim.models import Interface, Device
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from pathlib import Path
|
||||
from startup_script_utils import load_yaml
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/dcim_interfaces.yml')
|
||||
if not file.is_file():
|
||||
interfaces= load_yaml('/opt/netbox/initializers/dcim_interfaces.yml')
|
||||
|
||||
if interfaces is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
interfaces = yaml.load(stream)
|
||||
required_assocs = {
|
||||
'device': (Device, 'name')
|
||||
}
|
||||
|
||||
required_assocs = {
|
||||
'device': (Device, 'name')
|
||||
}
|
||||
for params in interfaces:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
|
||||
if interfaces is not None:
|
||||
for params in interfaces:
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
|
||||
for assoc, details in required_assocs.items():
|
||||
model, field = details
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
params[assoc] = model.objects.get(**query)
|
||||
interface, created = Interface.objects.get_or_create(**params)
|
||||
|
||||
interface, created = Interface.objects.get_or_create(**params)
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=interface,
|
||||
value=cf_value
|
||||
)
|
||||
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=interface,
|
||||
value=cf_value
|
||||
)
|
||||
interface.custom_field_values.add(custom_field_value)
|
||||
|
||||
interface.custom_field_values.add(custom_field_value)
|
||||
|
||||
print("🧷 Created interface", interface.name, interface.device.name)
|
||||
print("🧷 Created interface", interface.name, interface.device.name)
|
||||
|
@ -1,65 +1,69 @@
|
||||
from ipam.models import IPAddress, VRF
|
||||
from dcim.models import Device, Interface
|
||||
from virtualization.models import VirtualMachine
|
||||
from tenancy.models import Tenant
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from netaddr import IPNetwork
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
file = Path('/opt/netbox/initializers/ip_addresses.yml')
|
||||
if not file.is_file():
|
||||
from dcim.models import Device, Interface
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from extras.models import CustomField, CustomFieldValue
|
||||
from ipam.models import VRF, IPAddress
|
||||
from netaddr import IPNetwork
|
||||
from startup_script_utils import load_yaml
|
||||
from tenancy.models import Tenant
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
|
||||
ip_addresses = load_yaml('/opt/netbox/initializers/ip_addresses.yml')
|
||||
|
||||
if ip_addresses is None:
|
||||
sys.exit()
|
||||
|
||||
with file.open('r') as stream:
|
||||
yaml = YAML(typ='safe')
|
||||
ip_addresses = yaml.load(stream)
|
||||
optional_assocs = {
|
||||
'tenant': (Tenant, 'name'),
|
||||
'vrf': (VRF, 'name'),
|
||||
'interface': (None, None)
|
||||
}
|
||||
|
||||
optional_assocs = {
|
||||
'tenant': (Tenant, 'name'),
|
||||
'vrf': (VRF, 'name'),
|
||||
'interface': (Interface, 'name')
|
||||
}
|
||||
vm_interface_ct = ContentType.objects.filter(Q(app_label='virtualization', model='vminterface')).first()
|
||||
interface_ct = ContentType.objects.filter(Q(app_label='dcim', model='interface')).first()
|
||||
|
||||
if ip_addresses is not None:
|
||||
for params in ip_addresses:
|
||||
vm = params.pop('virtual_machine', None)
|
||||
device = params.pop('device', None)
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
params['address'] = IPNetwork(params['address'])
|
||||
for params in ip_addresses:
|
||||
vm = params.pop('virtual_machine', None)
|
||||
device = params.pop('device', None)
|
||||
custom_fields = params.pop('custom_fields', None)
|
||||
params['address'] = IPNetwork(params['address'])
|
||||
|
||||
if vm and device:
|
||||
print("IP Address can only specify one of the following: virtual_machine or device.")
|
||||
sys.exit()
|
||||
if vm and device:
|
||||
print("IP Address can only specify one of the following: virtual_machine or device.")
|
||||
sys.exit()
|
||||
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
if assoc == 'interface':
|
||||
if vm:
|
||||
vm_id = VirtualMachine.objects.get(name=vm).id
|
||||
query = { field: params.pop(assoc), "virtual_machine_id": vm_id }
|
||||
elif device:
|
||||
dev_id = Device.objects.get(name=device).id
|
||||
query = { field: params.pop(assoc), "device_id": dev_id }
|
||||
else:
|
||||
query = { field: params.pop(assoc) }
|
||||
for assoc, details in optional_assocs.items():
|
||||
if assoc in params:
|
||||
model, field = details
|
||||
if assoc == 'interface':
|
||||
if vm:
|
||||
vm_id = VirtualMachine.objects.get(name=vm).id
|
||||
query = { 'name': params.pop(assoc), "virtual_machine_id": vm_id }
|
||||
params['assigned_object_type'] = vm_interface_ct
|
||||
params['assigned_object_id'] = VMInterface.objects.get(**query).id
|
||||
elif device:
|
||||
dev_id = Device.objects.get(name=device).id
|
||||
query = { 'name': params.pop(assoc), "device_id": dev_id }
|
||||
params['assigned_object_type'] = interface_ct
|
||||
params['assigned_object_id'] = Interface.objects.get(**query).id
|
||||
else:
|
||||
query = { field: params.pop(assoc) }
|
||||
params[assoc] = model.objects.get(**query)
|
||||
|
||||
ip_address, created = IPAddress.objects.get_or_create(**params)
|
||||
ip_address, created = IPAddress.objects.get_or_create(**params)
|
||||
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=ip_address,
|
||||
value=cf_value
|
||||
)
|
||||
if created:
|
||||
if custom_fields is not None:
|
||||
for cf_name, cf_value in custom_fields.items():
|
||||
custom_field = CustomField.objects.get(name=cf_name)
|
||||
custom_field_value = CustomFieldValue.objects.create(
|
||||
field=custom_field,
|
||||
obj=ip_address,
|
||||
value=cf_value
|
||||
)
|
||||
|
||||
ip_address.custom_field_values.add(custom_field_value)
|
||||
ip_address.custom_field_values.add(custom_field_value)
|
||||
|
||||
print("🧬 Created IP Address", ip_address.address)
|
||||
print("🧬 Created IP Address", ip_address.address)
|
||||
|
@ -11,8 +11,19 @@ def filename(f):
|
||||
|
||||
with scandir(dirname(abspath(__file__))) as it:
|
||||
for f in sorted(it, key = filename):
|
||||
if f.name.startswith('__') or not f.is_file():
|
||||
if not f.is_file():
|
||||
continue
|
||||
|
||||
print(f"Running {f.path}")
|
||||
runpy.run_path(f.path)
|
||||
|
||||
if f.name.startswith('__'):
|
||||
continue
|
||||
|
||||
if not f.name.endswith('.py'):
|
||||
continue
|
||||
|
||||
print(f"▶️ Running the startup script {f.path}")
|
||||
try:
|
||||
runpy.run_path(f.path)
|
||||
except SystemExit as e:
|
||||
if e.code is not None and e.code != 0:
|
||||
print(f"‼️ The startup script {f.path} returned with code {e.code}, exiting.")
|
||||
raise
|
||||
|
2
startup_scripts/startup_script_utils/__init__.py
Normal file
2
startup_scripts/startup_script_utils/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .load_yaml import load_yaml
|
||||
from .permissions import set_permissions
|
10
startup_scripts/startup_script_utils/load_yaml.py
Normal file
10
startup_scripts/startup_script_utils/load_yaml.py
Normal file
@ -0,0 +1,10 @@
|
||||
from ruamel.yaml import YAML
|
||||
from pathlib import Path
|
||||
|
||||
def load_yaml(yaml_file: str):
|
||||
yf = Path(yaml_file)
|
||||
if not yf.is_file():
|
||||
return None
|
||||
with yf.open("r") as stream:
|
||||
yaml = YAML(typ="safe")
|
||||
return yaml.load(stream)
|
18
startup_scripts/startup_script_utils/permissions.py
Normal file
18
startup_scripts/startup_script_utils/permissions.py
Normal file
@ -0,0 +1,18 @@
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
|
||||
def set_permissions(subject, permission_filters):
|
||||
if subject is None or permission_filters is None:
|
||||
return
|
||||
subject.clear()
|
||||
for permission_filter in permission_filters:
|
||||
if "*" in permission_filter:
|
||||
permission_filter_regex = "^" + permission_filter.replace("*", ".*") + "$"
|
||||
permissions = Permission.objects.filter(codename__iregex=permission_filter_regex)
|
||||
print(" ⚿ Granting", permissions.count(), "permissions matching '" + permission_filter + "'")
|
||||
else:
|
||||
permissions = Permission.objects.filter(codename=permission_filter)
|
||||
print(" ⚿ Granting permission", permission_filter)
|
||||
|
||||
for permission in permissions:
|
||||
subject.add(permission)
|
Reference in New Issue
Block a user