Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c512e0e9b | ||
|
|
5b96496b46 | ||
|
|
88edcde8d6 | ||
|
|
7f18a3ad6f | ||
|
|
1925be271a | ||
|
|
a53486ca23 | ||
|
|
4bc418a5d0 |
@@ -0,0 +1,3 @@
|
||||
COMPOSE_PATH_SEPARATOR=:
|
||||
COMPOSE_FILE=ci/docker-compose.yml:ci/docker-compose.ci.yml
|
||||
COMPOSE_PROJECT_NAME=netbox-docker
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build the Docker image
|
||||
run: docker compose build --no-cache
|
||||
|
||||
- name: Verify the Docker image
|
||||
# clearcache is provided by Django extras, which is not loaded if netbox-slm cannot be loaded
|
||||
run: docker compose run netbox sh -c "/opt/netbox/venv/bin/python manage.py clearcache"
|
||||
|
||||
- name: Run Django tests with coverage for netbox_slm
|
||||
run: |
|
||||
chmod go+w ci/reports
|
||||
docker compose run netbox sh -c " \
|
||||
/opt/netbox/venv/bin/coverage run --source='netbox_slm' manage.py test netbox_slm && \
|
||||
/opt/netbox/venv/bin/coverage report --fail-under=0 && \
|
||||
/opt/netbox/venv/bin/coverage xml -o /ci/reports/coverage.xml"
|
||||
|
||||
- name: Sonarcloud scan
|
||||
if: env.SONAR_TOKEN != null
|
||||
uses: sonarsource/sonarcloud-github-action@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
@@ -1,3 +1,4 @@
|
||||
**/netbox
|
||||
**/netbox-docker
|
||||
**/*.pyc
|
||||
**/__pycache__
|
||||
@@ -5,3 +6,7 @@
|
||||
**/.vscode
|
||||
**/dist
|
||||
**/netbox_slm.egg-info
|
||||
ci/docker-compose.override.yml
|
||||
ci/reports
|
||||
out/production
|
||||
*.iml
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
include ../LICENSE
|
||||
include ../README.rst
|
||||
include ../README.md
|
||||
include ../SECURITY.md
|
||||
recursive-include netbox_slm/templates *
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
## Known Issues
|
||||
|
||||
- WARNING: This plugin is only tested with Netbox 3.2 at this time.
|
||||
- WARNING: This plugin is only tested with a single Netbox version at this time.
|
||||
- CSV/Bulk imports for SoftwareProduct, Version and Installation are currently broken (WIP)
|
||||
|
||||
## Installation Guide
|
||||
@@ -28,12 +28,12 @@ to install the Netbox SLM plugin:
|
||||
|
||||
1. Add ``netbox_slm`` to the ``PLUGINS`` list in
|
||||
``configuration/extra.py``.
|
||||
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==1.1`` as
|
||||
2. Create a ``plugin_requirements.txt`` with ``netbox-slm==1.2`` as
|
||||
contents.
|
||||
3. Create a ``Dockerfile-SLM`` with contents:
|
||||
|
||||
```
|
||||
FROM netboxcommunity/netbox:v3.2.X
|
||||
FROM netboxcommunity/netbox:vX.Y.Z
|
||||
|
||||
COPY ./plugin_requirements.txt /
|
||||
RUN /opt/netbox/venv/bin/pip install --no-warn-script-location -r /plugin_requirements.txt
|
||||
@@ -42,7 +42,7 @@ to install the Netbox SLM plugin:
|
||||
4. Create a ``docker-compose.override.yml`` with contents:
|
||||
|
||||
```
|
||||
version: '3.4'
|
||||
version: '3.7'
|
||||
services:
|
||||
netbox:
|
||||
ports:
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
ARG PYTHON_VERSION=3.10
|
||||
FROM python:${PYTHON_VERSION}-alpine AS compile-image
|
||||
|
||||
WORKDIR /build
|
||||
RUN python -m venv /opt/netbox/venv
|
||||
ENV PATH="/opt/netbox/venv/bin:$PATH"
|
||||
|
||||
COPY ../ /build
|
||||
|
||||
RUN python setup.py sdist
|
||||
RUN pip install --no-index /build
|
||||
|
||||
FROM netboxcommunity/netbox:v3.4.7
|
||||
|
||||
ARG PYTHON_VERSION=3.10
|
||||
COPY --from=compile-image /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM netboxcommunity/netbox:v3.4.7
|
||||
|
||||
RUN mkdir /ci && chmod go+w /ci
|
||||
COPY ../ci/requirements_ci.txt /ci/
|
||||
RUN pip install -r /ci/requirements_ci.txt
|
||||
|
||||
COPY ../netbox_slm /opt/netbox/netbox/netbox_slm
|
||||
@@ -0,0 +1,257 @@
|
||||
####
|
||||
## We recommend to not edit this file.
|
||||
## Create separate files to overwrite the settings.
|
||||
## See `extra.py` as an example.
|
||||
####
|
||||
|
||||
import re
|
||||
from os import environ
|
||||
from os.path import abspath, dirname, join
|
||||
|
||||
# For reference see https://netbox.readthedocs.io/en/stable/configuration/
|
||||
# Based on https://github.com/netbox-community/netbox/blob/master/netbox/netbox/configuration.example.py
|
||||
|
||||
# Read secret from file
|
||||
def _read_secret(secret_name, default = None):
|
||||
try:
|
||||
f = open('/run/secrets/' + secret_name, 'r', encoding='utf-8')
|
||||
except EnvironmentError:
|
||||
return default
|
||||
else:
|
||||
with f:
|
||||
return f.readline().strip()
|
||||
|
||||
_BASE_DIR = dirname(dirname(abspath(__file__)))
|
||||
|
||||
#########################
|
||||
# #
|
||||
# Required settings #
|
||||
# #
|
||||
#########################
|
||||
|
||||
# This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write
|
||||
# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
|
||||
#
|
||||
# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
|
||||
ALLOWED_HOSTS = environ.get('ALLOWED_HOSTS', '*').split(' ')
|
||||
|
||||
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
||||
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
|
||||
DATABASE = {
|
||||
'NAME': environ.get('DB_NAME', 'netbox'), # Database name
|
||||
'USER': environ.get('DB_USER', ''), # PostgreSQL username
|
||||
'PASSWORD': _read_secret('db_password', environ.get('DB_PASSWORD', '')),
|
||||
# PostgreSQL password
|
||||
'HOST': environ.get('DB_HOST', 'localhost'), # Database server
|
||||
'PORT': environ.get('DB_PORT', ''), # Database port (leave blank for default)
|
||||
'OPTIONS': {'sslmode': environ.get('DB_SSLMODE', 'prefer')},
|
||||
# Database connection SSLMODE
|
||||
'CONN_MAX_AGE': int(environ.get('DB_CONN_MAX_AGE', '300')),
|
||||
# Max database connection age
|
||||
'DISABLE_SERVER_SIDE_CURSORS': environ.get('DB_DISABLE_SERVER_SIDE_CURSORS', 'False').lower() == 'true',
|
||||
# Disable the use of server-side cursors transaction pooling
|
||||
}
|
||||
|
||||
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
|
||||
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
|
||||
# to use two separate database IDs.
|
||||
REDIS = {
|
||||
'tasks': {
|
||||
'HOST': environ.get('REDIS_HOST', 'localhost'),
|
||||
'PORT': int(environ.get('REDIS_PORT', 6379)),
|
||||
'PASSWORD': _read_secret('redis_password', environ.get('REDIS_PASSWORD', '')),
|
||||
'DATABASE': int(environ.get('REDIS_DATABASE', 0)),
|
||||
'SSL': environ.get('REDIS_SSL', 'False').lower() == 'true',
|
||||
'INSECURE_SKIP_TLS_VERIFY': environ.get('REDIS_INSECURE_SKIP_TLS_VERIFY', 'False').lower() == 'true',
|
||||
},
|
||||
'caching': {
|
||||
'HOST': environ.get('REDIS_CACHE_HOST', environ.get('REDIS_HOST', 'localhost')),
|
||||
'PORT': int(environ.get('REDIS_CACHE_PORT', environ.get('REDIS_PORT', 6379))),
|
||||
'PASSWORD': _read_secret('redis_cache_password', environ.get('REDIS_CACHE_PASSWORD', environ.get('REDIS_PASSWORD', ''))),
|
||||
'DATABASE': int(environ.get('REDIS_CACHE_DATABASE', 1)),
|
||||
'SSL': environ.get('REDIS_CACHE_SSL', environ.get('REDIS_SSL', 'False')).lower() == 'true',
|
||||
'INSECURE_SKIP_TLS_VERIFY': environ.get('REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY', environ.get('REDIS_INSECURE_SKIP_TLS_VERIFY', 'False')).lower() == 'true',
|
||||
},
|
||||
}
|
||||
|
||||
# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file.
|
||||
# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and
|
||||
# symbols. NetBox will not run without this defined. For more information, see
|
||||
# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY
|
||||
SECRET_KEY = _read_secret('secret_key', environ.get('SECRET_KEY', ''))
|
||||
|
||||
|
||||
#########################
|
||||
# #
|
||||
# Optional settings #
|
||||
# #
|
||||
#########################
|
||||
|
||||
# Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of
|
||||
# application errors (assuming correct email settings are provided).
|
||||
ADMINS = [
|
||||
# ['John Doe', 'jdoe@example.com'],
|
||||
]
|
||||
|
||||
# URL schemes that are allowed within links in NetBox
|
||||
ALLOWED_URL_SCHEMES = (
|
||||
'file', 'ftp', 'ftps', 'http', 'https', 'irc', 'mailto', 'sftp', 'ssh', 'tel', 'telnet', 'tftp', 'vnc', 'xmpp',
|
||||
)
|
||||
|
||||
# Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same
|
||||
# content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
|
||||
BANNER_TOP = environ.get('BANNER_TOP', '')
|
||||
BANNER_BOTTOM = environ.get('BANNER_BOTTOM', '')
|
||||
|
||||
# Text to include on the login page above the login form. HTML is allowed.
|
||||
BANNER_LOGIN = environ.get('BANNER_LOGIN', '')
|
||||
|
||||
# Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set:
|
||||
# BASE_PATH = 'netbox/'
|
||||
BASE_PATH = environ.get('BASE_PATH', '')
|
||||
|
||||
# Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90)
|
||||
CHANGELOG_RETENTION = int(environ.get('CHANGELOG_RETENTION', 90))
|
||||
|
||||
# API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be
|
||||
# allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or
|
||||
# CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers
|
||||
CORS_ORIGIN_ALLOW_ALL = environ.get('CORS_ORIGIN_ALLOW_ALL', 'False').lower() == 'true'
|
||||
CORS_ORIGIN_WHITELIST = list(filter(None, environ.get('CORS_ORIGIN_WHITELIST', 'https://localhost').split(' ')))
|
||||
CORS_ORIGIN_REGEX_WHITELIST = [re.compile(r) for r in list(filter(None, environ.get('CORS_ORIGIN_REGEX_WHITELIST', '').split(' ')))]
|
||||
|
||||
# Cross-Site-Request-Forgery-Attack settings. If Netbox is sitting behind a reverse proxy, you might need to set the CSRF_TRUSTED_ORIGINS flag.
|
||||
# Django 4.0 requires to specify the URL Scheme in this setting. An example environment variable could be specified like:
|
||||
# CSRF_TRUSTED_ORIGINS=https://demo.netbox.dev http://demo.netbox.dev
|
||||
CSRF_TRUSTED_ORIGINS = list(filter(None, environ.get('CSRF_TRUSTED_ORIGINS', '').split(' ')))
|
||||
|
||||
# Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal
|
||||
# sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging
|
||||
# on a production system.
|
||||
DEBUG = environ.get('DEBUG', 'False').lower() == 'true'
|
||||
DEVELOPER = environ.get('DEVELOPER', 'False').lower() == 'true'
|
||||
|
||||
# Email settings
|
||||
EMAIL = {
|
||||
'SERVER': environ.get('EMAIL_SERVER', 'localhost'),
|
||||
'PORT': int(environ.get('EMAIL_PORT', 25)),
|
||||
'USERNAME': environ.get('EMAIL_USERNAME', ''),
|
||||
'PASSWORD': _read_secret('email_password', environ.get('EMAIL_PASSWORD', '')),
|
||||
'USE_SSL': environ.get('EMAIL_USE_SSL', 'False').lower() == 'true',
|
||||
'USE_TLS': environ.get('EMAIL_USE_TLS', 'False').lower() == 'true',
|
||||
'SSL_CERTFILE': environ.get('EMAIL_SSL_CERTFILE', ''),
|
||||
'SSL_KEYFILE': environ.get('EMAIL_SSL_KEYFILE', ''),
|
||||
'TIMEOUT': int(environ.get('EMAIL_TIMEOUT', 10)), # seconds
|
||||
'FROM_EMAIL': environ.get('EMAIL_FROM', ''),
|
||||
}
|
||||
|
||||
# Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table
|
||||
# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
|
||||
ENFORCE_GLOBAL_UNIQUE = environ.get('ENFORCE_GLOBAL_UNIQUE', 'False').lower() == 'true'
|
||||
|
||||
# Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and
|
||||
# by anonymous users. List models in the form `<app>.<model>`. Add '*' to this list to exempt all models.
|
||||
EXEMPT_VIEW_PERMISSIONS = list(filter(None, environ.get('EXEMPT_VIEW_PERMISSIONS', '').split(' ')))
|
||||
|
||||
# Enable GraphQL API.
|
||||
GRAPHQL_ENABLED = environ.get('GRAPHQL_ENABLED', 'True').lower() == 'true'
|
||||
|
||||
# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
|
||||
# https://docs.djangoproject.com/en/stable/topics/logging/
|
||||
LOGGING = {}
|
||||
|
||||
# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
|
||||
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
|
||||
LOGIN_REQUIRED = 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 = int(environ.get('LOGIN_TIMEOUT', 1209600))
|
||||
|
||||
# Setting this to True will display a "maintenance mode" banner at the top of every page.
|
||||
MAINTENANCE_MODE = environ.get('MAINTENANCE_MODE', 'False').lower() == 'true'
|
||||
|
||||
# Maps provider
|
||||
MAPS_URL = environ.get('MAPS_URL', None)
|
||||
|
||||
# An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g.
|
||||
# "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request
|
||||
# all objects by specifying "?limit=0".
|
||||
MAX_PAGE_SIZE = int(environ.get('MAX_PAGE_SIZE', 1000))
|
||||
|
||||
# The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that
|
||||
# the default value of this setting is derived from the installed location.
|
||||
MEDIA_ROOT = environ.get('MEDIA_ROOT', join(_BASE_DIR, 'media'))
|
||||
|
||||
# Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics'
|
||||
METRICS_ENABLED = environ.get('METRICS_ENABLED', 'False').lower() == 'true'
|
||||
|
||||
# Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM.
|
||||
NAPALM_USERNAME = environ.get('NAPALM_USERNAME', '')
|
||||
NAPALM_PASSWORD = _read_secret('napalm_password', environ.get('NAPALM_PASSWORD', ''))
|
||||
|
||||
# NAPALM timeout (in seconds). (Default: 30)
|
||||
NAPALM_TIMEOUT = int(environ.get('NAPALM_TIMEOUT', 30))
|
||||
|
||||
# NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must
|
||||
# be provided as a dictionary.
|
||||
NAPALM_ARGS = {}
|
||||
|
||||
# Determine how many objects to display per page within a list. (Default: 50)
|
||||
PAGINATE_COUNT = int(environ.get('PAGINATE_COUNT', 50))
|
||||
|
||||
# Enable installed plugins. Add the name of each plugin to the list.
|
||||
PLUGINS = ['netbox_slm']
|
||||
|
||||
# Plugins configuration settings. These settings are used by various plugins that the user may have installed.
|
||||
# Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings.
|
||||
PLUGINS_CONFIG = {
|
||||
}
|
||||
|
||||
# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to
|
||||
# prefer IPv4 instead.
|
||||
PREFER_IPV4 = environ.get('PREFER_IPV4', 'False').lower() == 'true'
|
||||
|
||||
# Rack elevation size defaults, in pixels. For best results, the ratio of width to height should be roughly 10:1.
|
||||
RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = int(environ.get('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 22))
|
||||
RACK_ELEVATION_DEFAULT_UNIT_WIDTH = int(environ.get('RACK_ELEVATION_DEFAULT_UNIT_WIDTH', 220))
|
||||
|
||||
# Remote authentication support
|
||||
REMOTE_AUTH_ENABLED = environ.get('REMOTE_AUTH_ENABLED', 'False').lower() == 'true'
|
||||
REMOTE_AUTH_BACKEND = environ.get('REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend')
|
||||
REMOTE_AUTH_HEADER = environ.get('REMOTE_AUTH_HEADER', 'HTTP_REMOTE_USER')
|
||||
REMOTE_AUTH_AUTO_CREATE_USER = environ.get('REMOTE_AUTH_AUTO_CREATE_USER', 'True').lower() == 'true'
|
||||
REMOTE_AUTH_DEFAULT_GROUPS = list(filter(None, environ.get('REMOTE_AUTH_DEFAULT_GROUPS', '').split(' ')))
|
||||
|
||||
# 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 = 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 = environ.get('REPORTS_ROOT', '/etc/netbox/reports')
|
||||
|
||||
# Maximum execution time for background tasks, in seconds.
|
||||
RQ_DEFAULT_TIMEOUT = int(environ.get('RQ_DEFAULT_TIMEOUT', 300))
|
||||
|
||||
# The file path where custom scripts will be stored. A trailing slash is not needed. Note that the default value of
|
||||
# this setting is derived from the installed location.
|
||||
SCRIPTS_ROOT = environ.get('SCRIPTS_ROOT', '/etc/netbox/scripts')
|
||||
|
||||
# By default, NetBox will store session data in the database. Alternatively, a file path can be specified here to use
|
||||
# local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only
|
||||
# database access.) Note that the user as which NetBox runs must have read and write permissions to this path.
|
||||
SESSION_FILE_PATH = environ.get('SESSIONS_ROOT', None)
|
||||
|
||||
# Time zone (default: UTC)
|
||||
TIME_ZONE = environ.get('TIME_ZONE', 'UTC')
|
||||
|
||||
# Date/time formatting. See the following link for supported formats:
|
||||
# https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date
|
||||
DATE_FORMAT = environ.get('DATE_FORMAT', 'N j, Y')
|
||||
SHORT_DATE_FORMAT = environ.get('SHORT_DATE_FORMAT', 'Y-m-d')
|
||||
TIME_FORMAT = environ.get('TIME_FORMAT', 'g:i a')
|
||||
SHORT_TIME_FORMAT = environ.get('SHORT_TIME_FORMAT', 'H:i:s')
|
||||
DATETIME_FORMAT = environ.get('DATETIME_FORMAT', 'N j, Y g:i a')
|
||||
SHORT_DATETIME_FORMAT = environ.get('SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||
@@ -0,0 +1,11 @@
|
||||
PLUGINS = ['netbox_slm']
|
||||
DEBUG = True
|
||||
SECRET_KEY = 'dummy'
|
||||
DEVELOPER = True
|
||||
PLUGINS_CONFIG = {
|
||||
'netbox_slm': dict(
|
||||
TEST_RUNNER='xmlrunner.extra.djangotestrunner.XMLTestRunner',
|
||||
TEST_OUTPUT_DIR='/ci/reports/',
|
||||
TEST_OUTPUT_FILE_NAME='junit.xml',
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
version: '3.7'
|
||||
services:
|
||||
netbox:
|
||||
ports:
|
||||
- "8001:8080"
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ci/Dockerfile-CI
|
||||
image: netbox:slm
|
||||
environment:
|
||||
- COVERAGE_FILE=/ci/.coverage
|
||||
volumes:
|
||||
- ./reports:/ci/reports
|
||||
netbox-worker:
|
||||
image: netbox:slm
|
||||
netbox-housekeeping:
|
||||
image: netbox:slm
|
||||
postgres:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
redis:
|
||||
ports:
|
||||
- "6379:6379"
|
||||
@@ -0,0 +1,63 @@
|
||||
version: '3.7'
|
||||
services:
|
||||
netbox: &netbox
|
||||
image: netboxcommunity/netbox:${VERSION-v3.3-2.2.0}
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- redis-cache
|
||||
- netbox-worker
|
||||
env_file: env/netbox.env
|
||||
user: 'unit:root'
|
||||
volumes:
|
||||
- ./configuration:/etc/netbox/config:z,ro
|
||||
- netbox-media-files:/opt/netbox/netbox/media:z
|
||||
netbox-worker:
|
||||
<<: *netbox
|
||||
depends_on:
|
||||
- redis
|
||||
- postgres
|
||||
command:
|
||||
- /opt/netbox/venv/bin/python
|
||||
- /opt/netbox/netbox/manage.py
|
||||
- rqworker
|
||||
netbox-housekeeping:
|
||||
<<: *netbox
|
||||
depends_on:
|
||||
- redis
|
||||
- postgres
|
||||
command:
|
||||
- /opt/netbox/housekeeping.sh
|
||||
|
||||
# postgres
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
env_file: env/postgres.env
|
||||
volumes:
|
||||
- netbox-postgres-data:/var/lib/postgresql/data
|
||||
|
||||
# redis
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c # this is to evaluate the $REDIS_PASSWORD from the env
|
||||
- redis-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
|
||||
env_file: env/redis.env
|
||||
volumes:
|
||||
- netbox-redis-data:/data
|
||||
redis-cache:
|
||||
image: redis:7-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-media-files:
|
||||
driver: local
|
||||
netbox-postgres-data:
|
||||
driver: local
|
||||
netbox-redis-data:
|
||||
driver: local
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
CORS_ORIGIN_ALLOW_ALL=True
|
||||
DB_HOST=postgres
|
||||
DB_NAME=netbox
|
||||
DB_PASSWORD=J5brHrAXFLQSif0K
|
||||
DB_USER=netbox
|
||||
EMAIL_FROM=netbox@bar.com
|
||||
EMAIL_PASSWORD=
|
||||
EMAIL_PORT=25
|
||||
EMAIL_SERVER=localhost
|
||||
EMAIL_SSL_CERTFILE=
|
||||
EMAIL_SSL_KEYFILE=
|
||||
EMAIL_TIMEOUT=5
|
||||
EMAIL_USERNAME=netbox
|
||||
# EMAIL_USE_SSL and EMAIL_USE_TLS are mutually exclusive, i.e. they can't both be `true`!
|
||||
EMAIL_USE_SSL=false
|
||||
EMAIL_USE_TLS=false
|
||||
GRAPHQL_ENABLED=true
|
||||
HOUSEKEEPING_INTERVAL=86400
|
||||
MAX_PAGE_SIZE=1000
|
||||
MEDIA_ROOT=/opt/netbox/netbox/media
|
||||
METRICS_ENABLED=false
|
||||
NAPALM_PASSWORD=
|
||||
NAPALM_TIMEOUT=10
|
||||
NAPALM_USERNAME=
|
||||
REDIS_CACHE_DATABASE=1
|
||||
REDIS_CACHE_HOST=redis-cache
|
||||
REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY=false
|
||||
REDIS_CACHE_PASSWORD=t4Ph722qJ5QHeQ1qfu36
|
||||
REDIS_CACHE_SSL=false
|
||||
REDIS_DATABASE=0
|
||||
REDIS_HOST=redis
|
||||
REDIS_INSECURE_SKIP_TLS_VERIFY=false
|
||||
REDIS_PASSWORD=H733Kdjndks81
|
||||
REDIS_SSL=false
|
||||
RELEASE_CHECK_URL=https://api.github.com/repos/netbox-community/netbox/releases
|
||||
SECRET_KEY=r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj
|
||||
SKIP_SUPERUSER=false
|
||||
SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567
|
||||
SUPERUSER_EMAIL=admin@example.com
|
||||
SUPERUSER_NAME=admin
|
||||
SUPERUSER_PASSWORD=admin
|
||||
WEBHOOKS_ENABLED=true
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
POSTGRES_DB=netbox
|
||||
POSTGRES_PASSWORD=J5brHrAXFLQSif0K
|
||||
POSTGRES_USER=netbox
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
REDIS_PASSWORD=t4Ph722qJ5QHeQ1qfu36
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
REDIS_PASSWORD=H733Kdjndks81
|
||||
@@ -0,0 +1,2 @@
|
||||
coverage==7.2.2
|
||||
unittest-xml-reporting==3.2.0
|
||||
@@ -4,14 +4,15 @@ from extras.plugins import PluginConfig
|
||||
class SLMConfig(PluginConfig):
|
||||
name = 'netbox_slm'
|
||||
verbose_name = 'Software Lifecycle Management'
|
||||
description = 'Software Lifecycle Management'
|
||||
version = '1.1'
|
||||
author = 'Hedde van der Heide'
|
||||
author_email = 'hedde.vanderheide@ictu.nl'
|
||||
description = 'Software Lifecycle Management Netbox Plugin.'
|
||||
version = '1.4'
|
||||
author = 'ICTU'
|
||||
author_email = 'open-source-projects@ictu.nl'
|
||||
base_url = 'slm'
|
||||
required_settings = []
|
||||
default_settings = {
|
||||
'version_info': False
|
||||
}
|
||||
|
||||
|
||||
config = SLMConfig
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
|
||||
if settings.DEBUG:
|
||||
for model in apps.get_app_config("netbox_slm").get_models():
|
||||
admin.site.register(model)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from netbox_slm.models import (
|
||||
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation,
|
||||
)
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
|
||||
class SoftwareProductSerializer(NetBoxModelSerializer):
|
||||
@@ -15,11 +13,12 @@ class SoftwareProductSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = [
|
||||
'id', 'display', 'url', 'name', 'tags', 'custom_field_data', 'created', 'last_updated',
|
||||
'id', 'display', 'url', 'name',
|
||||
'tags', 'custom_field_data', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def get_display(self, obj):
|
||||
return f"{obj.manufacturer.name} - {obj.name}"
|
||||
return f"{obj.manufacturer} - {obj}"
|
||||
|
||||
|
||||
class SoftwareProductVersionSerializer(NetBoxModelSerializer):
|
||||
@@ -31,11 +30,12 @@ class SoftwareProductVersionSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = [
|
||||
'id', 'display', 'url', 'name', 'software_product', 'tags', 'custom_field_data', 'created', 'last_updated',
|
||||
'id', 'display', 'url', 'name', 'software_product',
|
||||
'tags', 'custom_field_data', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def get_display(self, obj):
|
||||
return obj.name
|
||||
return f"{obj}"
|
||||
|
||||
|
||||
class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
|
||||
@@ -47,9 +47,27 @@ class SoftwareProductInstallationSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = [
|
||||
'id', 'display', 'url', 'device', 'virtualmachine', 'software_product', 'version', 'tags',
|
||||
'custom_field_data', 'created', 'last_updated',
|
||||
'id', 'display', 'url', 'device', 'virtualmachine', 'software_product', 'version',
|
||||
'tags', 'custom_field_data', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def get_display(self, obj):
|
||||
return obj
|
||||
return f"{obj}"
|
||||
|
||||
|
||||
class SoftwareLicenseSerializer(NetBoxModelSerializer):
|
||||
display = serializers.SerializerMethodField()
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="plugins-api:netbox_slm-api:softwarelicense-detail"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareLicense
|
||||
fields = [
|
||||
'id', 'display', 'url', 'name', 'description', 'type', 'stored_location',
|
||||
'start_date', 'expiration_date', 'software_product', 'version', 'installation',
|
||||
'tags', 'custom_field_data', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
def get_display(self, obj):
|
||||
return f"{obj}"
|
||||
|
||||
@@ -4,6 +4,7 @@ from netbox_slm.api.views import (
|
||||
SoftwareProductViewSet,
|
||||
SoftwareProductVersionViewSet,
|
||||
SoftwareProductInstallationViewSet,
|
||||
SoftwareLicenseViewSet,
|
||||
)
|
||||
|
||||
router = NetBoxRouter()
|
||||
@@ -12,4 +13,5 @@ router.APIRootView = NetboxSLMRootView
|
||||
router.register("softwareproducts", SoftwareProductViewSet)
|
||||
router.register("softwareproductversions", SoftwareProductVersionViewSet)
|
||||
router.register("softwareproductinstallations", SoftwareProductInstallationViewSet)
|
||||
router.register("softwarelicenses", SoftwareLicenseViewSet)
|
||||
urlpatterns = router.urls
|
||||
+16
-6
@@ -1,15 +1,19 @@
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
from rest_framework.routers import APIRootView
|
||||
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
from netbox_slm.api.serializers import (
|
||||
SoftwareProductSerializer, SoftwareProductVersionSerializer, SoftwareProductInstallationSerializer,
|
||||
SoftwareProductSerializer,
|
||||
SoftwareProductVersionSerializer,
|
||||
SoftwareProductInstallationSerializer,
|
||||
SoftwareLicenseSerializer,
|
||||
)
|
||||
from netbox_slm.filtersets import (
|
||||
SoftwareProductFilterSet, SoftwareProductVersionFilterSet, SoftwareProductInstallationFilterSet,
|
||||
)
|
||||
from netbox_slm.models import (
|
||||
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation,
|
||||
SoftwareProductFilterSet,
|
||||
SoftwareProductVersionFilterSet,
|
||||
SoftwareProductInstallationFilterSet,
|
||||
SoftwareLicenseFilterSet,
|
||||
)
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
|
||||
class NetboxSLMRootView(APIRootView):
|
||||
@@ -37,3 +41,9 @@ class SoftwareProductInstallationViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
serializer_class = SoftwareProductInstallationSerializer
|
||||
filterset_class = SoftwareProductInstallationFilterSet
|
||||
|
||||
|
||||
class SoftwareLicenseViewSet(NetBoxModelViewSet):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
serializer_class = SoftwareLicenseSerializer
|
||||
filterset_class = SoftwareLicenseFilterSet
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from django.db.models import Q
|
||||
|
||||
from netbox.filtersets import NetBoxModelFilterSet
|
||||
from netbox_slm.models import *
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
|
||||
class SoftwareProductFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareProduct instances."""
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = tuple()
|
||||
@@ -14,16 +15,18 @@ class SoftwareProductFilterSet(NetBoxModelFilterSet):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(name__icontains=value) | \
|
||||
Q(manufacturer__name__icontains=value)
|
||||
qs_filter = Q(name__icontains=value) | Q(manufacturer__name__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareProductVersion instances."""
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = tuple()
|
||||
fields = (
|
||||
"software_product",
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
@@ -37,9 +40,12 @@ class SoftwareProductVersionFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareProductInstallation instances."""
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = tuple()
|
||||
fields = (
|
||||
"software_product",
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
@@ -49,3 +55,22 @@ class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet):
|
||||
Q(software_product__manufacturer__name__icontains=value) | \
|
||||
Q(version__name__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class SoftwareLicenseFilterSet(NetBoxModelFilterSet):
|
||||
"""Filter capabilities for SoftwareLicense instances."""
|
||||
|
||||
class Meta:
|
||||
model = SoftwareLicense
|
||||
fields = tuple()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(name__icontains=value) | \
|
||||
Q(software_product__name__icontains=value) | \
|
||||
Q(version__name__icontains=value) | \
|
||||
Q(installation__device__name__icontains=value) | \
|
||||
Q(installation__virtualmachine__name__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from .software_license import *
|
||||
from .software_product import *
|
||||
from .software_product_installation import *
|
||||
from .software_product_version import *
|
||||
@@ -0,0 +1,76 @@
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
from utilities.forms import DynamicModelChoiceField, APISelect, TagFilterField
|
||||
|
||||
|
||||
class SoftwareLicenseForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareLicense object."""
|
||||
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=True,
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
|
||||
),
|
||||
)
|
||||
version = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}
|
||||
),
|
||||
query_params={
|
||||
'software_product': '$software_product',
|
||||
}
|
||||
)
|
||||
installation = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductInstallation.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductinstallation-list")}
|
||||
),
|
||||
query_params={
|
||||
'software_product': '$software_product',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareLicense
|
||||
fields = ("name", "description", "type", "stored_location", "start_date", "expiration_date",
|
||||
"software_product", "version", "installation", "tags")
|
||||
|
||||
|
||||
class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareLicense
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag',)),
|
||||
)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SoftwareLicenseImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = SoftwareLicense
|
||||
fields = ("name", "description", "type", "stored_location", "start_date", "expiration_date",)
|
||||
|
||||
|
||||
class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm):
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
required=False
|
||||
)
|
||||
version = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductVersion.objects.all(),
|
||||
required=False
|
||||
)
|
||||
installation = DynamicModelChoiceField(
|
||||
queryset=SoftwareProductInstallation.objects.all(),
|
||||
required=False
|
||||
)
|
||||
model = SoftwareLicense
|
||||
fieldsets = (
|
||||
(None, ('software_product', 'version', 'installation',)),
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.models import Manufacturer
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
|
||||
from netbox_slm.models import SoftwareProduct
|
||||
from utilities.forms import DynamicModelChoiceField, TagFilterField
|
||||
|
||||
|
||||
class SoftwareProductForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareProduct object."""
|
||||
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = ("name", "manufacturer", "description", "tags")
|
||||
|
||||
|
||||
class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareProduct
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SoftwareProductImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = ("name", "manufacturer",)
|
||||
|
||||
|
||||
class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=forms.MultipleHiddenInput(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
nullable_fields = []
|
||||
@@ -2,101 +2,13 @@ from django import forms
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.models import Manufacturer, Device
|
||||
from netbox.forms import (
|
||||
NetBoxModelForm,
|
||||
NetBoxModelCSVForm,
|
||||
NetBoxModelBulkEditForm,
|
||||
NetBoxModelFilterSetForm,
|
||||
)
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
from utilities.forms import (
|
||||
DynamicModelChoiceField, APISelect, TagFilterField, ChoiceField
|
||||
)
|
||||
from dcim.models import Device
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
|
||||
from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion
|
||||
from utilities.forms import DynamicModelChoiceField, APISelect, TagFilterField
|
||||
from virtualization.models import VirtualMachine
|
||||
|
||||
|
||||
class SoftwareProductForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareProduct object."""
|
||||
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = ("name", "manufacturer", "description", "tags")
|
||||
|
||||
|
||||
class SoftwareProductFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareProduct
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SoftwareProductCSVForm(NetBoxModelCSVForm):
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
fields = ("name", "manufacturer",)
|
||||
|
||||
|
||||
class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=forms.MultipleHiddenInput(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProduct
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class SoftwareProductVersionForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareProductVersion object."""
|
||||
name = forms.CharField(label=_("Version"))
|
||||
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("name", "software_product", "tags")
|
||||
|
||||
|
||||
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareProductVersion
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SoftwareProductVersionCSVForm(NetBoxModelCSVForm):
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("name",)
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkEditForm(NetBoxModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=forms.MultipleHiddenInput(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class SoftwareProductInstallationForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareProductInstallation object."""
|
||||
|
||||
@@ -153,7 +65,7 @@ class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SoftwareProductInstallationCSVForm(NetBoxModelCSVForm):
|
||||
class SoftwareProductInstallationImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = SoftwareProductInstallation
|
||||
fields = tuple()
|
||||
@@ -0,0 +1,49 @@
|
||||
from django import forms
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.forms import NetBoxModelForm, NetBoxModelImportForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion
|
||||
from utilities.forms import DynamicModelChoiceField, APISelect, TagFilterField
|
||||
|
||||
|
||||
class SoftwareProductVersionForm(NetBoxModelForm):
|
||||
"""Form for creating a new SoftwareProductVersion object."""
|
||||
name = forms.CharField(label=_("Version"))
|
||||
|
||||
software_product = DynamicModelChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=APISelect(
|
||||
attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("name", "software_product", "tags")
|
||||
|
||||
|
||||
class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm):
|
||||
model = SoftwareProductVersion
|
||||
fieldsets = (
|
||||
(None, ('q', 'tag')),
|
||||
)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class SoftwareProductVersionImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
fields = ("name",)
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkEditForm(NetBoxModelBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=SoftwareProduct.objects.all(),
|
||||
widget=forms.MultipleHiddenInput(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SoftwareProductVersion
|
||||
nullable_fields = []
|
||||
@@ -0,0 +1,54 @@
|
||||
# Generated by Django 4.1.8 on 2023-06-27 08:10
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0084_staging'),
|
||||
('netbox_slm', '0004_auto_20220415_0705'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='softwareproduct',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductinstallation',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='softwareproductversion',
|
||||
name='custom_field_data',
|
||||
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SoftwareLicense',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('description', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('type', models.CharField(max_length=128)),
|
||||
('stored_location', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('start_date', models.DateField(blank=True, null=True)),
|
||||
('expiration_date', models.DateField(blank=True, null=True)),
|
||||
('installation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductinstallation')),
|
||||
('software_product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
('version', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductversion')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
+41
-4
@@ -33,11 +33,12 @@ class SoftwareProduct(NetBoxModel):
|
||||
|
||||
|
||||
class SoftwareProductVersion(NetBoxModel):
|
||||
name = models.CharField(max_length=64)
|
||||
|
||||
software_product = models.ForeignKey(
|
||||
to='netbox_slm.SoftwareProduct',
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
name = models.CharField(max_length=64)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
@@ -80,13 +81,49 @@ class SoftwareProductInstallation(NetBoxModel):
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.pk}"
|
||||
return f"{self.pk} ({self.platform})"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk})
|
||||
|
||||
def get_platform(self):
|
||||
@property
|
||||
def platform(self):
|
||||
return self.device or self.virtualmachine
|
||||
|
||||
def render_type(self):
|
||||
return f"{'device' if self.device else 'virtualmachine'}"
|
||||
return 'device' if self.device else 'virtualmachine'
|
||||
|
||||
|
||||
class SoftwareLicense(NetBoxModel):
|
||||
name = models.CharField(max_length=128)
|
||||
description = models.CharField(max_length=255, null=True, blank=True)
|
||||
type = models.CharField(max_length=128)
|
||||
|
||||
stored_location = models.CharField(max_length=255, null=True, blank=True)
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
expiration_date = models.DateField(null=True, blank=True)
|
||||
|
||||
software_product = models.ForeignKey(
|
||||
to='netbox_slm.SoftwareProduct',
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
version = models.ForeignKey(
|
||||
to='netbox_slm.SoftwareProductVersion',
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
installation = models.ForeignKey(
|
||||
to='netbox_slm.SoftwareProductInstallation',
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("plugins:netbox_slm:softwarelicense", kwargs={"pk": self.pk})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from extras.plugins import PluginMenuButton, PluginMenuItem
|
||||
from utilities.choices import ButtonColorChoices
|
||||
|
||||
|
||||
menu_items = (
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_slm:softwareproduct_list',
|
||||
@@ -63,4 +62,24 @@ menu_items = (
|
||||
),
|
||||
)
|
||||
),
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_slm:softwarelicense_list',
|
||||
link_text='Licenses',
|
||||
buttons=(
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwarelicense_add",
|
||||
"Add",
|
||||
"mdi mdi-plus-thick",
|
||||
ButtonColorChoices.GREEN,
|
||||
permissions=["netbox_slm.add_softwarelicense"],
|
||||
),
|
||||
PluginMenuButton(
|
||||
"plugins:netbox_slm:softwarelicense_import",
|
||||
"Import",
|
||||
"mdi mdi-upload",
|
||||
ButtonColorChoices.CYAN,
|
||||
permissions=["netbox_slm.add_softwarelicense"],
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
+59
-4
@@ -1,9 +1,9 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from django.db.models import Count
|
||||
from django_tables2.utils import Accessor
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
from netbox.tables import NetBoxTable, ChoiceFieldColumn, ToggleColumn, columns
|
||||
|
||||
from netbox.tables import NetBoxTable, ToggleColumn, columns
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
|
||||
class SoftwareProductTable(NetBoxTable):
|
||||
@@ -118,7 +118,7 @@ class SoftwareProductInstallationTable(NetBoxTable):
|
||||
linkify=True
|
||||
)
|
||||
platform = tables.Column(
|
||||
accessor='get_platform',
|
||||
accessor=Accessor('platform'),
|
||||
linkify=True
|
||||
)
|
||||
type = tables.Column(accessor='render_type')
|
||||
@@ -165,3 +165,58 @@ class SoftwareProductInstallationTable(NetBoxTable):
|
||||
|
||||
def render_software_product(self, value, **kwargs):
|
||||
return f"{kwargs['record'].software_product.manufacturer.name} - {value}"
|
||||
|
||||
|
||||
class SoftwareLicenseTable(NetBoxTable):
|
||||
"""Table for displaying SoftwareLicense objects."""
|
||||
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
|
||||
type = tables.Column(accessor='render_type')
|
||||
software_product = tables.Column(
|
||||
accessor=Accessor('software_product'),
|
||||
linkify=True
|
||||
)
|
||||
version = tables.Column(
|
||||
accessor=Accessor('version'),
|
||||
linkify=True
|
||||
)
|
||||
installation = tables.Column(
|
||||
accessor=Accessor('installation'),
|
||||
linkify=True
|
||||
)
|
||||
|
||||
tags = columns.TagColumn(
|
||||
url_name="plugins:netbox_slm:softwarelicense_list",
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SoftwareLicense
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"description",
|
||||
"type",
|
||||
"stored_location",
|
||||
"start_date",
|
||||
"expiration_date",
|
||||
"software_product",
|
||||
"version",
|
||||
"installation",
|
||||
"tags",
|
||||
)
|
||||
default_columns = (
|
||||
"pk",
|
||||
"name",
|
||||
"expiration_date",
|
||||
"software_product",
|
||||
"installation",
|
||||
"tags",
|
||||
)
|
||||
|
||||
def render_software_product(self, value, **kwargs):
|
||||
return f"{kwargs['record'].software_product.manufacturer.name} - {value}"
|
||||
|
||||
def render_installation(self, **kwargs):
|
||||
return f"{kwargs['record'].installation.platform}"
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Software License
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Type</th>
|
||||
<td>{{ object.type }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Stored location</th>
|
||||
<td>{{ object.stored_location }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Start date</th>
|
||||
<td>{{ object.start_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Expiration date</th>
|
||||
<td>{{ object.expiration_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Software Product</th>
|
||||
<td>{{ object.software_product }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Version</th>
|
||||
<td>{{ object.version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Installation</th>
|
||||
<td>{{ object.installation }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -15,7 +15,15 @@
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object }}</td>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Manufacturer</th>
|
||||
<td>{{ object.manufacturer }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Versions</th>
|
||||
@@ -34,7 +42,6 @@
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{# {% include 'inc/panels/comments.html' %}#}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,17 +13,17 @@
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
{% if object.device %}
|
||||
{% if object.device %}
|
||||
<tr>
|
||||
<th scope="row">Device</th>
|
||||
<td>{{ object.device }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<th scope="row">Virtualmachine</th>
|
||||
<td>{{ object.virtualmachine }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">Software Product</th>
|
||||
<td>{{ object.software_product }}</td>
|
||||
@@ -37,7 +37,6 @@
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{# {% include 'inc/panels/comments.html' %}#}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{# {% include 'inc/panels/comments.html' %}#}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
|
||||
class ModelTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.p_name = "test product"
|
||||
self.v_name = "test version"
|
||||
self.l_name = "test license"
|
||||
|
||||
self.software_product = SoftwareProduct.objects.create(name=self.p_name)
|
||||
self.software_product_version = SoftwareProductVersion.objects.create(
|
||||
name=self.v_name, software_product=self.software_product
|
||||
)
|
||||
self.software_product_installation = SoftwareProductInstallation.objects.create(
|
||||
software_product=self.software_product, version=self.software_product_version
|
||||
)
|
||||
self.software_license = SoftwareLicense.objects.create(
|
||||
name=self.l_name, software_product=self.software_product,
|
||||
version=self.software_product_version, installation=self.software_product_installation
|
||||
)
|
||||
|
||||
def test_model_name(self):
|
||||
self.assertEqual(self.p_name, str(self.software_product))
|
||||
self.assertEqual(self.v_name, str(self.software_product_version))
|
||||
self.assertTrue(str(self.software_product_installation)[0].isdigit()) # starts with PK
|
||||
self.assertEqual(self.l_name, str(self.software_license))
|
||||
|
||||
def test_absolute_url(self):
|
||||
self.assertEqual("/plugins/slm/software-products/1/", self.software_product.get_absolute_url())
|
||||
self.assertEqual("/plugins/slm/versions/1/", self.software_product_version.get_absolute_url())
|
||||
self.assertEqual("/plugins/slm/installations/1/", self.software_product_installation.get_absolute_url())
|
||||
self.assertEqual("/plugins/slm/licenses/1/", self.software_license.get_absolute_url())
|
||||
|
||||
def test_get_installation_count(self):
|
||||
installation_ss = '<a href="/plugins/slm/installations/?q={}">1</a>'
|
||||
self.assertEqual(installation_ss.format(self.p_name), self.software_product.get_installation_count())
|
||||
self.assertEqual(installation_ss.format(self.v_name), self.software_product_version.get_installation_count())
|
||||
|
||||
def test_product_installation_methods(self):
|
||||
self.assertEqual("virtualmachine", self.software_product_installation.render_type())
|
||||
self.assertIsNone(self.software_product_installation.platform)
|
||||
+51
-16
@@ -1,21 +1,27 @@
|
||||
from django.urls import path
|
||||
from netbox.views.generic import ObjectChangeLogView
|
||||
|
||||
from netbox.views.generic import ObjectChangeLogView
|
||||
from netbox_slm import views
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense
|
||||
|
||||
urlpatterns = [
|
||||
# Software Products
|
||||
path("software-products/", views.SoftwareProductListView.as_view(), name='softwareproduct_list'),
|
||||
path("software-products/add/", views.SoftwareProductEditView.as_view(), name='softwareproduct_add'),
|
||||
path("software-products/import/", views.SoftwareProductBulkImportView.as_view(), name="softwareproduct_import"),
|
||||
path("software-products/edit/", views.SoftwareProductBulkEditView.as_view(), name="softwareproduct_bulk_edit"),
|
||||
# SoftwareProduct
|
||||
path("software-products/", views.SoftwareProductListView.as_view(),
|
||||
name='softwareproduct_list'),
|
||||
path("software-products/add/", views.SoftwareProductEditView.as_view(),
|
||||
name='softwareproduct_add'),
|
||||
path("software-products/import/", views.SoftwareProductBulkImportView.as_view(),
|
||||
name="softwareproduct_import"),
|
||||
path("software-products/edit/", views.SoftwareProductBulkEditView.as_view(),
|
||||
name="softwareproduct_bulk_edit"),
|
||||
path("software-products/delete/", views.SoftwareProductBulkDeleteView.as_view(),
|
||||
name="softwareproduct_bulk_delete"),
|
||||
path("software-products/<int:pk>/", views.SoftwareProductView.as_view(), name="softwareproduct"),
|
||||
path("software-products/<int:pk>/", views.SoftwareProductView.as_view(),
|
||||
name="softwareproduct"),
|
||||
path("software-products/<int:pk>/delete/", views.SoftwareProductDeleteView.as_view(),
|
||||
name="softwareproduct_delete"),
|
||||
path("software-products/<int:pk>/edit/", views.SoftwareProductEditView.as_view(), name="softwareproduct_edit"),
|
||||
path("software-products/<int:pk>/edit/", views.SoftwareProductEditView.as_view(),
|
||||
name="softwareproduct_edit"),
|
||||
path(
|
||||
"software-products/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
@@ -23,18 +29,23 @@ urlpatterns = [
|
||||
kwargs={"model": SoftwareProduct},
|
||||
),
|
||||
|
||||
# Software Product Versions
|
||||
path("versions/", views.SoftwareProductVersionListView.as_view(), name='softwareproductversion_list'),
|
||||
path("versions/add/", views.SoftwareProductVersionEditView.as_view(), name='softwareproductversion_add'),
|
||||
# SoftwareProductVersion
|
||||
path("versions/", views.SoftwareProductVersionListView.as_view(),
|
||||
name='softwareproductversion_list'),
|
||||
path("versions/add/", views.SoftwareProductVersionEditView.as_view(),
|
||||
name='softwareproductversion_add'),
|
||||
path("versions/import/", views.SoftwareProductVersionBulkImportView.as_view(),
|
||||
name="softwareproductversion_import"),
|
||||
path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(), name="softwareproductversion_bulk_edit"),
|
||||
path("versions/edit/", views.SoftwareProductVersionBulkEditView.as_view(),
|
||||
name="softwareproductversion_bulk_edit"),
|
||||
path("versions/delete/", views.SoftwareProductVersionBulkDeleteView.as_view(),
|
||||
name="softwareproductversion_bulk_delete"),
|
||||
path("versions/<int:pk>/", views.SoftwareProductVersionView.as_view(), name="softwareproductversion"),
|
||||
path("versions/<int:pk>/", views.SoftwareProductVersionView.as_view(),
|
||||
name="softwareproductversion"),
|
||||
path("versions/<int:pk>/delete/", views.SoftwareProductVersionDeleteView.as_view(),
|
||||
name="softwareproductversion_delete"),
|
||||
path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(), name="softwareproductversion_edit"),
|
||||
path("versions/<int:pk>/edit/", views.SoftwareProductVersionEditView.as_view(),
|
||||
name="softwareproductversion_edit"),
|
||||
path(
|
||||
"versions/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
@@ -42,7 +53,7 @@ urlpatterns = [
|
||||
kwargs={"model": SoftwareProductVersion},
|
||||
),
|
||||
|
||||
# Software Product Versions
|
||||
# SoftwareProductInstallation
|
||||
path("installations/", views.SoftwareProductInstallationListView.as_view(),
|
||||
name='softwareproductinstallation_list'),
|
||||
path("installations/add/", views.SoftwareProductInstallationEditView.as_view(),
|
||||
@@ -65,4 +76,28 @@ urlpatterns = [
|
||||
name="softwareproductinstallation_changelog",
|
||||
kwargs={"model": SoftwareProductInstallation},
|
||||
),
|
||||
|
||||
# SoftwareLicense
|
||||
path("licenses/", views.SoftwareLicenseListView.as_view(),
|
||||
name='softwarelicense_list'),
|
||||
path("licenses/add/", views.SoftwareLicenseEditView.as_view(),
|
||||
name='softwarelicense_add'),
|
||||
path("licenses/import/", views.SoftwareLicenseBulkImportView.as_view(),
|
||||
name="softwarelicense_import"),
|
||||
path("licenses/edit/", views.SoftwareLicenseBulkEditView.as_view(),
|
||||
name="softwarelicense_bulk_edit"),
|
||||
path("licenses/delete/", views.SoftwareLicenseBulkDeleteView.as_view(),
|
||||
name="softwarelicense_bulk_delete"),
|
||||
path("licenses/<int:pk>/", views.SoftwareLicenseView.as_view(),
|
||||
name="softwarelicense"),
|
||||
path("licenses/<int:pk>/delete/", views.SoftwareLicenseDeleteView.as_view(),
|
||||
name="softwarelicense_delete"),
|
||||
path("licenses/<int:pk>/edit/", views.SoftwareLicenseEditView.as_view(),
|
||||
name="softwarelicense_edit"),
|
||||
path(
|
||||
"licenses/<int:pk>/changelog/",
|
||||
ObjectChangeLogView.as_view(),
|
||||
name="softwarelicense_changelog",
|
||||
kwargs={"model": SoftwareLicense},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets
|
||||
from netbox_slm import forms
|
||||
from netbox_slm import tables
|
||||
from netbox_slm.models import (
|
||||
SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation
|
||||
)
|
||||
|
||||
|
||||
class SoftwareProductListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProducts."""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = filtersets.SoftwareProductFilterSet
|
||||
filterset_form = forms.SoftwareProductFilterForm
|
||||
table = tables.SoftwareProductTable
|
||||
|
||||
|
||||
class SoftwareProductView(generic.ObjectView):
|
||||
"""Display SoftwareProduct details"""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
versions = instance.softwareproductversion_set.all()
|
||||
return {"versions": versions}
|
||||
|
||||
|
||||
class SoftwareProductEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProduct instance."""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
form = forms.SoftwareProductForm
|
||||
|
||||
|
||||
class SoftwareProductDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProduct instance"""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
model_form = forms.SoftwareProductCSVForm
|
||||
table = tables.SoftwareProductTable
|
||||
|
||||
|
||||
class SoftwareProductBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = filtersets.SoftwareProductFilterSet
|
||||
filterset_form = forms.SoftwareProductFilterForm
|
||||
table = tables.SoftwareProductTable
|
||||
form = forms.SoftwareProductBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
table = tables.SoftwareProductTable
|
||||
|
||||
|
||||
class SoftwareProductVersionListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProductVersions."""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = filtersets.SoftwareProductVersionFilterSet
|
||||
filterset_form = forms.SoftwareProductVersionFilterForm
|
||||
table = tables.SoftwareProductVersionTable
|
||||
|
||||
|
||||
class SoftwareProductVersionView(generic.ObjectView):
|
||||
"""Display SoftwareProductVersion details"""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
installation_count = instance.get_installation_count()
|
||||
return {"installations": installation_count}
|
||||
|
||||
|
||||
class SoftwareProductVersionEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProductVersion instance."""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
form = forms.SoftwareProductVersionForm
|
||||
|
||||
|
||||
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProductVersion instance"""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
model_form = forms.SoftwareProductVersionCSVForm
|
||||
table = tables.SoftwareProductVersionTable
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = filtersets.SoftwareProductVersionFilterSet
|
||||
table = tables.SoftwareProductVersionTable
|
||||
form = forms.SoftwareProductVersionBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
table = tables.SoftwareProductVersionTable
|
||||
|
||||
|
||||
class SoftwareProductInstallationListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProductInstallations."""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = filtersets.SoftwareProductInstallationFilterSet
|
||||
filterset_form = forms.SoftwareProductInstallationFilterForm
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
|
||||
|
||||
class SoftwareProductInstallationView(generic.ObjectView):
|
||||
"""Display SoftwareProductInstallation details"""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductInstallationEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProductInstallation instance."""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
form = forms.SoftwareProductInstallationForm
|
||||
|
||||
|
||||
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProductInstallation instance"""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
model_form = forms.SoftwareProductInstallationCSVForm
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = filtersets.SoftwareProductInstallationFilterSet
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
form = forms.SoftwareProductInstallationBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
@@ -0,0 +1,4 @@
|
||||
from .software_license import *
|
||||
from .software_product import *
|
||||
from .software_product_installation import *
|
||||
from .software_product_version import *
|
||||
@@ -0,0 +1,49 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
from netbox_slm.models import SoftwareLicense
|
||||
|
||||
|
||||
class SoftwareLicenseListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareLicenses."""
|
||||
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
filterset = filtersets.SoftwareLicenseFilterSet
|
||||
filterset_form = forms.SoftwareLicenseFilterForm
|
||||
table = tables.SoftwareLicenseTable
|
||||
|
||||
|
||||
class SoftwareLicenseView(generic.ObjectView):
|
||||
"""Display SoftwareLicense details"""
|
||||
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
|
||||
|
||||
class SoftwareLicenseEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareLicense instance."""
|
||||
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
form = forms.SoftwareLicenseForm
|
||||
|
||||
|
||||
class SoftwareLicenseDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareLicense instance"""
|
||||
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
|
||||
|
||||
class SoftwareLicenseBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
model_form = forms.SoftwareLicenseImportForm
|
||||
table = tables.SoftwareLicenseTable
|
||||
|
||||
|
||||
class SoftwareLicenseBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
filterset = filtersets.SoftwareLicenseFilterSet
|
||||
table = tables.SoftwareLicenseTable
|
||||
form = forms.SoftwareLicenseBulkEditForm
|
||||
|
||||
|
||||
class SoftwareLicenseBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareLicense.objects.all()
|
||||
table = tables.SoftwareLicenseTable
|
||||
@@ -0,0 +1,54 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
from netbox_slm.models import SoftwareProduct
|
||||
|
||||
|
||||
class SoftwareProductListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProducts."""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = filtersets.SoftwareProductFilterSet
|
||||
filterset_form = forms.SoftwareProductFilterForm
|
||||
table = tables.SoftwareProductTable
|
||||
|
||||
|
||||
class SoftwareProductView(generic.ObjectView):
|
||||
"""Display SoftwareProduct details"""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
versions = instance.softwareproductversion_set.all()
|
||||
return {"versions": versions}
|
||||
|
||||
|
||||
class SoftwareProductEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProduct instance."""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
form = forms.SoftwareProductForm
|
||||
|
||||
|
||||
class SoftwareProductDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProduct instance"""
|
||||
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
model_form = forms.SoftwareProductImportForm
|
||||
table = tables.SoftwareProductTable
|
||||
|
||||
|
||||
class SoftwareProductBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
filterset = filtersets.SoftwareProductFilterSet
|
||||
filterset_form = forms.SoftwareProductFilterForm
|
||||
table = tables.SoftwareProductTable
|
||||
form = forms.SoftwareProductBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProduct.objects.all()
|
||||
table = tables.SoftwareProductTable
|
||||
@@ -0,0 +1,49 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
from netbox_slm.models import SoftwareProductInstallation
|
||||
|
||||
|
||||
class SoftwareProductInstallationListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProductInstallations."""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = filtersets.SoftwareProductInstallationFilterSet
|
||||
filterset_form = forms.SoftwareProductInstallationFilterForm
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
|
||||
|
||||
class SoftwareProductInstallationView(generic.ObjectView):
|
||||
"""Display SoftwareProductInstallation details"""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductInstallationEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProductInstallation instance."""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
form = forms.SoftwareProductInstallationForm
|
||||
|
||||
|
||||
class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProductInstallation instance"""
|
||||
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
model_form = forms.SoftwareProductInstallationImportForm
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
filterset = filtersets.SoftwareProductInstallationFilterSet
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
form = forms.SoftwareProductInstallationBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductInstallation.objects.all()
|
||||
table = tables.SoftwareProductInstallationTable
|
||||
@@ -0,0 +1,53 @@
|
||||
from netbox.views import generic
|
||||
from netbox_slm import filtersets, forms, tables
|
||||
from netbox_slm.models import SoftwareProductVersion
|
||||
|
||||
|
||||
class SoftwareProductVersionListView(generic.ObjectListView):
|
||||
"""View for listing all existing SoftwareProductVersions."""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = filtersets.SoftwareProductVersionFilterSet
|
||||
filterset_form = forms.SoftwareProductVersionFilterForm
|
||||
table = tables.SoftwareProductVersionTable
|
||||
|
||||
|
||||
class SoftwareProductVersionView(generic.ObjectView):
|
||||
"""Display SoftwareProductVersion details"""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
installation_count = instance.get_installation_count()
|
||||
return {"installations": installation_count}
|
||||
|
||||
|
||||
class SoftwareProductVersionEditView(generic.ObjectEditView):
|
||||
"""View for editing and creating a SoftwareProductVersion instance."""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
form = forms.SoftwareProductVersionForm
|
||||
|
||||
|
||||
class SoftwareProductVersionDeleteView(generic.ObjectDeleteView):
|
||||
"""View for deleting a SoftwareProductVersion instance"""
|
||||
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkImportView(generic.BulkImportView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
model_form = forms.SoftwareProductVersionImportForm
|
||||
table = tables.SoftwareProductVersionTable
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkEditView(generic.BulkEditView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
filterset = filtersets.SoftwareProductVersionFilterSet
|
||||
table = tables.SoftwareProductVersionTable
|
||||
form = forms.SoftwareProductVersionBulkEditForm
|
||||
|
||||
|
||||
class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = SoftwareProductVersion.objects.all()
|
||||
table = tables.SoftwareProductVersionTable
|
||||
@@ -1,11 +1,9 @@
|
||||
[metadata]
|
||||
name = netbox-slm
|
||||
version = 1.1
|
||||
version = 1.4
|
||||
description = Software Lifecycle Management Netbox Plugin.
|
||||
# long_description = file: README.rst
|
||||
# long_description_content_type='text/x-rst'
|
||||
author = Hedde van der Heide
|
||||
author_email = hedde.vanderheide@ictu.nl
|
||||
author = ICTU
|
||||
author_email = open-source-projects@ictu.nl
|
||||
license = Apache 2.0
|
||||
classifiers =
|
||||
Environment :: Web Environment
|
||||
@@ -16,13 +14,13 @@ classifiers =
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Topic :: Internet :: WWW/HTTP
|
||||
Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
|
||||
[options]
|
||||
include_package_data = true
|
||||
packages = find:
|
||||
python_requires = >=3.7
|
||||
python_requires = >=3.9
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from setuptools import setup, Extension
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
||||
@@ -0,0 +1,13 @@
|
||||
# Project metadata
|
||||
sonar.organization=ictu
|
||||
sonar.projectKey=ICTU_netbox_slm
|
||||
sonar.projectName=netbox-slm
|
||||
sonar.projectVersion=1.4
|
||||
|
||||
# Path is relative to the sonar-project.properties file
|
||||
sonar.sources=netbox_slm
|
||||
sonar.python.version=3.9, 3.10, 3.11
|
||||
|
||||
# Unit tests
|
||||
sonar.python.xunit.reportPath=ci/reports/junit.xml
|
||||
sonar.python.coverage.reportPaths=ci/reports/coverage.xml
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
git clone -b release https://github.com/netbox-community/netbox-docker.git
|
||||
cd netbox-docker
|
||||
tee docker-compose.override.yml <<EOF
|
||||
version: '3.4'
|
||||
version: '3.7'
|
||||
services:
|
||||
netbox:
|
||||
ports:
|
||||
|
||||
Reference in New Issue
Block a user