mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-18 04:56:29 -06:00
* Closes #15908: Establish canonical & local sources for release info * Update references to settings.VERSION
This commit is contained in:
parent
1952d3e63a
commit
c6bd714a04
@ -18,7 +18,7 @@ BANNER_TEXT = """### NetBox interactive shell ({node})
|
|||||||
node=platform.node(),
|
node=platform.node(),
|
||||||
python=platform.python_version(),
|
python=platform.python_version(),
|
||||||
django=get_version(),
|
django=get_version(),
|
||||||
netbox=settings.VERSION
|
netbox=settings.RELEASE.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -539,7 +539,7 @@ class SystemView(UserPassesTestMixin, View):
|
|||||||
except (ProgrammingError, IndexError):
|
except (ProgrammingError, IndexError):
|
||||||
pass
|
pass
|
||||||
stats = {
|
stats = {
|
||||||
'netbox_version': settings.VERSION,
|
'netbox_release': settings.RELEASE,
|
||||||
'django_version': DJANGO_VERSION,
|
'django_version': DJANGO_VERSION,
|
||||||
'python_version': platform.python_version(),
|
'python_version': platform.python_version(),
|
||||||
'postgresql_version': psql_version,
|
'postgresql_version': psql_version,
|
||||||
|
@ -329,7 +329,7 @@ class RSSFeedWidget(DashboardWidget):
|
|||||||
try:
|
try:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
url=self.config['feed_url'],
|
url=self.config['feed_url'],
|
||||||
headers={'User-Agent': f'NetBox/{settings.VERSION}'},
|
headers={'User-Agent': f'NetBox/{settings.RELEASE.version}'},
|
||||||
proxies=settings.HTTP_PROXIES,
|
proxies=settings.HTTP_PROXIES,
|
||||||
timeout=3
|
timeout=3
|
||||||
)
|
)
|
||||||
|
@ -66,7 +66,7 @@ class StatusView(APIView):
|
|||||||
return Response({
|
return Response({
|
||||||
'django-version': DJANGO_VERSION,
|
'django-version': DJANGO_VERSION,
|
||||||
'installed-apps': installed_apps,
|
'installed-apps': installed_apps,
|
||||||
'netbox-version': settings.VERSION,
|
'netbox-version': settings.RELEASE.full_version,
|
||||||
'plugins': get_installed_plugins(),
|
'plugins': get_installed_plugins(),
|
||||||
'python-version': platform.python_version(),
|
'python-version': platform.python_version(),
|
||||||
'rq-workers-running': Worker.count(get_connection('default')),
|
'rq-workers-running': Worker.count(get_connection('default')),
|
||||||
|
@ -18,6 +18,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from netbox.config import PARAMS as CONFIG_PARAMS
|
from netbox.config import PARAMS as CONFIG_PARAMS
|
||||||
from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
||||||
from netbox.plugins import PluginConfig
|
from netbox.plugins import PluginConfig
|
||||||
|
from utilities.release import load_release_data
|
||||||
from utilities.string import trailing_slash
|
from utilities.string import trailing_slash
|
||||||
|
|
||||||
|
|
||||||
@ -25,7 +26,8 @@ from utilities.string import trailing_slash
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '4.0.6-dev'
|
RELEASE = load_release_data()
|
||||||
|
VERSION = RELEASE.full_version # Retained for backward compatibility
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
# Set the base directory two levels up
|
# Set the base directory two levels up
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
@ -533,7 +535,7 @@ if SENTRY_ENABLED:
|
|||||||
# Initialize the SDK
|
# Initialize the SDK
|
||||||
sentry_sdk.init(
|
sentry_sdk.init(
|
||||||
dsn=SENTRY_DSN,
|
dsn=SENTRY_DSN,
|
||||||
release=VERSION,
|
release=RELEASE.full_version,
|
||||||
sample_rate=SENTRY_SAMPLE_RATE,
|
sample_rate=SENTRY_SAMPLE_RATE,
|
||||||
traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
|
traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
|
||||||
send_default_pii=True,
|
send_default_pii=True,
|
||||||
@ -553,7 +555,7 @@ if SENTRY_ENABLED:
|
|||||||
DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16]
|
DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16]
|
||||||
CENSUS_URL = 'https://census.netbox.dev/api/v1/'
|
CENSUS_URL = 'https://census.netbox.dev/api/v1/'
|
||||||
CENSUS_PARAMS = {
|
CENSUS_PARAMS = {
|
||||||
'version': VERSION,
|
'version': RELEASE.full_version,
|
||||||
'python_version': sys.version.split()[0],
|
'python_version': sys.version.split()[0],
|
||||||
'deployment_id': DEPLOYMENT_ID,
|
'deployment_id': DEPLOYMENT_ID,
|
||||||
}
|
}
|
||||||
@ -611,7 +613,7 @@ FILTERS_NULL_CHOICE_VALUE = 'null'
|
|||||||
# Django REST framework (API)
|
# Django REST framework (API)
|
||||||
#
|
#
|
||||||
|
|
||||||
REST_FRAMEWORK_VERSION = '.'.join(VERSION.split('-')[0].split('.')[:2]) # Use major.minor as API version
|
REST_FRAMEWORK_VERSION = '.'.join(RELEASE.version.split('-')[0].split('.')[:2]) # Use major.minor as API version
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION],
|
'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION],
|
||||||
'COERCE_DECIMAL_TO_STRING': False,
|
'COERCE_DECIMAL_TO_STRING': False,
|
||||||
@ -656,7 +658,7 @@ REST_FRAMEWORK = {
|
|||||||
SPECTACULAR_SETTINGS = {
|
SPECTACULAR_SETTINGS = {
|
||||||
'TITLE': 'NetBox REST API',
|
'TITLE': 'NetBox REST API',
|
||||||
'LICENSE': {'name': 'Apache v2 License'},
|
'LICENSE': {'name': 'Apache v2 License'},
|
||||||
'VERSION': VERSION,
|
'VERSION': RELEASE.full_version,
|
||||||
'COMPONENT_SPLIT_REQUEST': True,
|
'COMPONENT_SPLIT_REQUEST': True,
|
||||||
'REDOC_DIST': 'SIDECAR',
|
'REDOC_DIST': 'SIDECAR',
|
||||||
'SERVERS': [{
|
'SERVERS': [{
|
||||||
@ -802,7 +804,7 @@ for plugin_name in PLUGINS:
|
|||||||
# Validate user-provided configuration settings and assign defaults
|
# Validate user-provided configuration settings and assign defaults
|
||||||
if plugin_name not in PLUGINS_CONFIG:
|
if plugin_name not in PLUGINS_CONFIG:
|
||||||
PLUGINS_CONFIG[plugin_name] = {}
|
PLUGINS_CONFIG[plugin_name] = {}
|
||||||
plugin_config.validate(PLUGINS_CONFIG[plugin_name], VERSION)
|
plugin_config.validate(PLUGINS_CONFIG[plugin_name], RELEASE.version)
|
||||||
|
|
||||||
# Add middleware
|
# Add middleware
|
||||||
plugin_middleware = plugin_config.middleware
|
plugin_middleware = plugin_config.middleware
|
||||||
|
@ -166,11 +166,11 @@ class PluginTest(TestCase):
|
|||||||
required_settings = ['foo']
|
required_settings = ['foo']
|
||||||
|
|
||||||
# Validation should pass when all required settings are present
|
# Validation should pass when all required settings are present
|
||||||
DummyConfigWithRequiredSettings.validate({'foo': True}, settings.VERSION)
|
DummyConfigWithRequiredSettings.validate({'foo': True}, settings.RELEASE.version)
|
||||||
|
|
||||||
# Validation should fail when a required setting is missing
|
# Validation should fail when a required setting is missing
|
||||||
with self.assertRaises(ImproperlyConfigured):
|
with self.assertRaises(ImproperlyConfigured):
|
||||||
DummyConfigWithRequiredSettings.validate({}, settings.VERSION)
|
DummyConfigWithRequiredSettings.validate({}, settings.RELEASE.version)
|
||||||
|
|
||||||
def test_default_settings(self):
|
def test_default_settings(self):
|
||||||
"""
|
"""
|
||||||
@ -183,12 +183,12 @@ class PluginTest(TestCase):
|
|||||||
|
|
||||||
# Populate the default value if setting has not been specified
|
# Populate the default value if setting has not been specified
|
||||||
user_config = {}
|
user_config = {}
|
||||||
DummyConfigWithDefaultSettings.validate(user_config, settings.VERSION)
|
DummyConfigWithDefaultSettings.validate(user_config, settings.RELEASE.version)
|
||||||
self.assertEqual(user_config['bar'], 123)
|
self.assertEqual(user_config['bar'], 123)
|
||||||
|
|
||||||
# Don't overwrite specified values
|
# Don't overwrite specified values
|
||||||
user_config = {'bar': 456}
|
user_config = {'bar': 456}
|
||||||
DummyConfigWithDefaultSettings.validate(user_config, settings.VERSION)
|
DummyConfigWithDefaultSettings.validate(user_config, settings.RELEASE.version)
|
||||||
self.assertEqual(user_config['bar'], 456)
|
self.assertEqual(user_config['bar'], 456)
|
||||||
|
|
||||||
def test_graphql(self):
|
def test_graphql(self):
|
||||||
|
@ -57,7 +57,7 @@ _patterns = [
|
|||||||
|
|
||||||
path(
|
path(
|
||||||
"api/schema/",
|
"api/schema/",
|
||||||
cache_page(timeout=86400, key_prefix=f"api_schema_{settings.VERSION}")(
|
cache_page(timeout=86400, key_prefix=f"api_schema_{settings.RELEASE.version}")(
|
||||||
SpectacularAPIView.as_view()
|
SpectacularAPIView.as_view()
|
||||||
),
|
),
|
||||||
name="schema",
|
name="schema",
|
||||||
|
@ -54,7 +54,7 @@ def handler_500(request, template_name=ERROR_500_TEMPLATE_NAME):
|
|||||||
return HttpResponseServerError(template.render({
|
return HttpResponseServerError(template.render({
|
||||||
'error': error,
|
'error': error,
|
||||||
'exception': str(type_),
|
'exception': str(type_),
|
||||||
'netbox_version': settings.VERSION,
|
'netbox_version': settings.RELEASE.full_version,
|
||||||
'python_version': platform.python_version(),
|
'python_version': platform.python_version(),
|
||||||
'plugins': get_installed_plugins(),
|
'plugins': get_installed_plugins(),
|
||||||
}))
|
}))
|
||||||
|
@ -50,7 +50,7 @@ class HomeView(View):
|
|||||||
latest_release = cache.get('latest_release')
|
latest_release = cache.get('latest_release')
|
||||||
if latest_release:
|
if latest_release:
|
||||||
release_version, release_url = latest_release
|
release_version, release_url = latest_release
|
||||||
if release_version > version.parse(settings.VERSION):
|
if release_version > version.parse(settings.RELEASE.version):
|
||||||
new_release = {
|
new_release = {
|
||||||
'version': str(release_version),
|
'version': str(release_version),
|
||||||
'url': release_url,
|
'url': release_url,
|
||||||
|
2
netbox/release.yaml
Normal file
2
netbox/release.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
version: "4.1.0"
|
||||||
|
designation: "dev"
|
@ -20,7 +20,7 @@
|
|||||||
{# Initialize color mode #}
|
{# Initialize color mode #}
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="{% static 'setmode.js' %}?v={{ settings.VERSION }}"
|
src="{% static 'setmode.js' %}?v={{ settings.RELEASE.version }}"
|
||||||
onerror="window.location='{% url 'media_failure' %}?filename=setmode.js'">
|
onerror="window.location='{% url 'media_failure' %}?filename=setmode.js'">
|
||||||
</script>
|
</script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
@ -33,12 +33,12 @@
|
|||||||
{# Static resources #}
|
{# Static resources #}
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="{% static 'netbox-external.css'%}?v={{ settings.VERSION }}"
|
href="{% static 'netbox-external.css'%}?v={{ settings.RELEASE.version }}"
|
||||||
onerror="window.location='{% url 'media_failure' %}?filename=netbox-external.css'"
|
onerror="window.location='{% url 'media_failure' %}?filename=netbox-external.css'"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="{% static 'netbox.css'%}?v={{ settings.VERSION }}"
|
href="{% static 'netbox.css'%}?v={{ settings.RELEASE.version }}"
|
||||||
onerror="window.location='{% url 'media_failure' %}?filename=netbox.css'"
|
onerror="window.location='{% url 'media_failure' %}?filename=netbox.css'"
|
||||||
/>
|
/>
|
||||||
<link rel="icon" type="image/png" href="{% static 'netbox.ico' %}" />
|
<link rel="icon" type="image/png" href="{% static 'netbox.ico' %}" />
|
||||||
@ -47,7 +47,7 @@
|
|||||||
{# Javascript #}
|
{# Javascript #}
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="{% static 'netbox.js' %}?v={{ settings.VERSION }}"
|
src="{% static 'netbox.js' %}?v={{ settings.RELEASE.version }}"
|
||||||
onerror="window.location='{% url 'media_failure' %}?filename=netbox.js'">
|
onerror="window.location='{% url 'media_failure' %}?filename=netbox.js'">
|
||||||
</script>
|
</script>
|
||||||
{% django_htmx_script %}
|
{% django_htmx_script %}
|
||||||
|
@ -188,12 +188,9 @@ Blocks:
|
|||||||
|
|
||||||
{# Footer text #}
|
{# Footer text #}
|
||||||
<ul class="list-inline list-inline-dots fs-5 mb-0" id="footer-stamp" hx-swap-oob="true">
|
<ul class="list-inline list-inline-dots fs-5 mb-0" id="footer-stamp" hx-swap-oob="true">
|
||||||
<li class="list-inline-item">
|
<li class="list-inline-item">{% now 'Y-m-d H:i:s T' %}</li>
|
||||||
{% now 'Y-m-d H:i:s T' %}
|
<li class="list-inline-item">{{ settings.HOSTNAME }}</li>
|
||||||
</li>
|
<li class="list-inline-item">{{ settings.RELEASE.name }}</li>
|
||||||
<li class="list-inline-item">
|
|
||||||
{{ settings.HOSTNAME }} (v{{ settings.VERSION }})
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
{# /Footer text #}
|
{# /Footer text #}
|
||||||
|
|
||||||
|
@ -28,8 +28,13 @@
|
|||||||
<h5 class="card-header">{% trans "System Status" %}</h5>
|
<h5 class="card-header">{% trans "System Status" %}</h5>
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "NetBox version" %}</th>
|
<th scope="row">{% trans "NetBox release" %}</th>
|
||||||
<td>{{ stats.netbox_version }}</td>
|
<td>
|
||||||
|
{{ stats.netbox_release.name }}
|
||||||
|
{% if stats.netbox_release.published %}
|
||||||
|
({{ stats.netbox_release.published|isodate }})
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Python version" %}</th>
|
<th scope="row">{% trans "Python version" %}</th>
|
||||||
|
@ -53,7 +53,7 @@ def handle_rest_api_exception(request, *args, **kwargs):
|
|||||||
data = {
|
data = {
|
||||||
'error': str(error),
|
'error': str(error),
|
||||||
'exception': type_.__name__,
|
'exception': type_.__name__,
|
||||||
'netbox_version': settings.VERSION,
|
'netbox_version': settings.RELEASE.full_version,
|
||||||
'python_version': platform.python_version(),
|
'python_version': platform.python_version(),
|
||||||
}
|
}
|
||||||
return JsonResponse(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return JsonResponse(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
57
netbox/utilities/release.py
Normal file
57
netbox/utilities/release.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
RELEASE_PATH = 'release.yaml'
|
||||||
|
LOCAL_RELEASE_PATH = 'local/release.yaml'
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ReleaseInfo:
|
||||||
|
version: str
|
||||||
|
edition: str = 'Community'
|
||||||
|
published: Union[datetime.date, None] = None
|
||||||
|
designation: Union[str, None] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_version(self):
|
||||||
|
if self.designation:
|
||||||
|
return f"{self.version}-{self.designation}"
|
||||||
|
return self.version
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return f"NetBox {self.edition} v{self.full_version}"
|
||||||
|
|
||||||
|
|
||||||
|
def load_release_data():
|
||||||
|
"""
|
||||||
|
Load any locally-defined release attributes and return a ReleaseInfo instance.
|
||||||
|
"""
|
||||||
|
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
# Load canonical release attributes
|
||||||
|
with open(os.path.join(base_path, RELEASE_PATH), 'r') as release_file:
|
||||||
|
data = yaml.safe_load(release_file)
|
||||||
|
|
||||||
|
# Overlay any local release date (if defined)
|
||||||
|
try:
|
||||||
|
with open(os.path.join(base_path, LOCAL_RELEASE_PATH), 'r') as release_file:
|
||||||
|
local_data = yaml.safe_load(release_file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
local_data = {}
|
||||||
|
if type(local_data) is not dict:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
f"{LOCAL_RELEASE_PATH}: Local release data must be defined as a dictionary."
|
||||||
|
)
|
||||||
|
data.update(local_data)
|
||||||
|
|
||||||
|
# Convert the published date to a date object
|
||||||
|
if 'published' in data:
|
||||||
|
data['published'] = datetime.date.fromisoformat(data['published'])
|
||||||
|
|
||||||
|
return ReleaseInfo(**data)
|
Loading…
Reference in New Issue
Block a user