Closes #15908: Establish canonical & local sources for release info (#16420)

* Closes #15908: Establish canonical & local sources for release info

* Update references to settings.VERSION
This commit is contained in:
Jeremy Stretch 2024-06-07 13:41:48 -04:00 committed by GitHub
parent 1952d3e63a
commit c6bd714a04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 93 additions and 30 deletions

View File

@ -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
) )

View File

@ -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,

View File

@ -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
) )

View File

@ -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')),

View File

@ -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

View File

@ -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):

View File

@ -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",

View File

@ -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(),
})) }))

View File

@ -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
View File

@ -0,0 +1,2 @@
version: "4.1.0"
designation: "dev"

View File

@ -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 %}

View File

@ -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 #}

View File

@ -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>

View File

@ -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)

View 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)