فهرست منبع

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

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

* Update references to settings.VERSION
Jeremy Stretch 1 سال پیش
والد
کامیت
c6bd714a04

+ 1 - 1
netbox/core/management/commands/nbshell.py

@@ -18,7 +18,7 @@ BANNER_TEXT = """### NetBox interactive shell ({node})
     node=platform.node(),
     python=platform.python_version(),
     django=get_version(),
-    netbox=settings.VERSION
+    netbox=settings.RELEASE.name
 )
 
 

+ 1 - 1
netbox/core/views.py

@@ -539,7 +539,7 @@ class SystemView(UserPassesTestMixin, View):
         except (ProgrammingError, IndexError):
             pass
         stats = {
-            'netbox_version': settings.VERSION,
+            'netbox_release': settings.RELEASE,
             'django_version': DJANGO_VERSION,
             'python_version': platform.python_version(),
             'postgresql_version': psql_version,

+ 1 - 1
netbox/extras/dashboard/widgets.py

@@ -329,7 +329,7 @@ class RSSFeedWidget(DashboardWidget):
         try:
             response = requests.get(
                 url=self.config['feed_url'],
-                headers={'User-Agent': f'NetBox/{settings.VERSION}'},
+                headers={'User-Agent': f'NetBox/{settings.RELEASE.version}'},
                 proxies=settings.HTTP_PROXIES,
                 timeout=3
             )

+ 1 - 1
netbox/netbox/api/views.py

@@ -66,7 +66,7 @@ class StatusView(APIView):
         return Response({
             'django-version': DJANGO_VERSION,
             'installed-apps': installed_apps,
-            'netbox-version': settings.VERSION,
+            'netbox-version': settings.RELEASE.full_version,
             'plugins': get_installed_plugins(),
             'python-version': platform.python_version(),
             'rq-workers-running': Worker.count(get_connection('default')),

+ 8 - 6
netbox/netbox/settings.py

@@ -18,6 +18,7 @@ from django.utils.translation import gettext_lazy as _
 from netbox.config import PARAMS as CONFIG_PARAMS
 from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
 from netbox.plugins import PluginConfig
+from utilities.release import load_release_data
 from utilities.string import trailing_slash
 
 
@@ -25,7 +26,8 @@ from utilities.string import trailing_slash
 # Environment setup
 #
 
-VERSION = '4.0.6-dev'
+RELEASE = load_release_data()
+VERSION = RELEASE.full_version  # Retained for backward compatibility
 HOSTNAME = platform.node()
 # Set the base directory two levels up
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -533,7 +535,7 @@ if SENTRY_ENABLED:
     # Initialize the SDK
     sentry_sdk.init(
         dsn=SENTRY_DSN,
-        release=VERSION,
+        release=RELEASE.full_version,
         sample_rate=SENTRY_SAMPLE_RATE,
         traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
         send_default_pii=True,
@@ -553,7 +555,7 @@ if SENTRY_ENABLED:
 DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16]
 CENSUS_URL = 'https://census.netbox.dev/api/v1/'
 CENSUS_PARAMS = {
-    'version': VERSION,
+    'version': RELEASE.full_version,
     'python_version': sys.version.split()[0],
     'deployment_id': DEPLOYMENT_ID,
 }
@@ -611,7 +613,7 @@ FILTERS_NULL_CHOICE_VALUE = 'null'
 # 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 = {
     'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION],
     'COERCE_DECIMAL_TO_STRING': False,
@@ -656,7 +658,7 @@ REST_FRAMEWORK = {
 SPECTACULAR_SETTINGS = {
     'TITLE': 'NetBox REST API',
     'LICENSE': {'name': 'Apache v2 License'},
-    'VERSION': VERSION,
+    'VERSION': RELEASE.full_version,
     'COMPONENT_SPLIT_REQUEST': True,
     'REDOC_DIST': 'SIDECAR',
     'SERVERS': [{
@@ -802,7 +804,7 @@ for plugin_name in PLUGINS:
     # Validate user-provided configuration settings and assign defaults
     if plugin_name not in PLUGINS_CONFIG:
         PLUGINS_CONFIG[plugin_name] = {}
-    plugin_config.validate(PLUGINS_CONFIG[plugin_name], VERSION)
+    plugin_config.validate(PLUGINS_CONFIG[plugin_name], RELEASE.version)
 
     # Add middleware
     plugin_middleware = plugin_config.middleware

+ 4 - 4
netbox/netbox/tests/test_plugins.py

@@ -166,11 +166,11 @@ class PluginTest(TestCase):
             required_settings = ['foo']
 
         # 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
         with self.assertRaises(ImproperlyConfigured):
-            DummyConfigWithRequiredSettings.validate({}, settings.VERSION)
+            DummyConfigWithRequiredSettings.validate({}, settings.RELEASE.version)
 
     def test_default_settings(self):
         """
@@ -183,12 +183,12 @@ class PluginTest(TestCase):
 
         # Populate the default value if setting has not been specified
         user_config = {}
-        DummyConfigWithDefaultSettings.validate(user_config, settings.VERSION)
+        DummyConfigWithDefaultSettings.validate(user_config, settings.RELEASE.version)
         self.assertEqual(user_config['bar'], 123)
 
         # Don't overwrite specified values
         user_config = {'bar': 456}
-        DummyConfigWithDefaultSettings.validate(user_config, settings.VERSION)
+        DummyConfigWithDefaultSettings.validate(user_config, settings.RELEASE.version)
         self.assertEqual(user_config['bar'], 456)
 
     def test_graphql(self):

+ 1 - 1
netbox/netbox/urls.py

@@ -57,7 +57,7 @@ _patterns = [
 
     path(
         "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()
         ),
         name="schema",

+ 1 - 1
netbox/netbox/views/errors.py

@@ -54,7 +54,7 @@ def handler_500(request, template_name=ERROR_500_TEMPLATE_NAME):
     return HttpResponseServerError(template.render({
         'error': error,
         'exception': str(type_),
-        'netbox_version': settings.VERSION,
+        'netbox_version': settings.RELEASE.full_version,
         'python_version': platform.python_version(),
         'plugins': get_installed_plugins(),
     }))

+ 1 - 1
netbox/netbox/views/misc.py

@@ -50,7 +50,7 @@ class HomeView(View):
             latest_release = cache.get('latest_release')
             if 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 = {
                         'version': str(release_version),
                         'url': release_url,

+ 2 - 0
netbox/release.yaml

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

+ 4 - 4
netbox/templates/base/base.html

@@ -20,7 +20,7 @@
     {# Initialize color mode #}
     <script
       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'">
     </script>
     <script type="text/javascript">
@@ -33,12 +33,12 @@
     {# Static resources #}
     <link
       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'"
     />
     <link
       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'"
     />
     <link rel="icon" type="image/png" href="{% static 'netbox.ico' %}" />
@@ -47,7 +47,7 @@
     {# Javascript #}
     <script
       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'">
     </script>
     {% django_htmx_script %}

+ 3 - 6
netbox/templates/base/layout.html

@@ -188,12 +188,9 @@ Blocks:
 
             {# Footer text #}
             <ul class="list-inline list-inline-dots fs-5 mb-0" id="footer-stamp" hx-swap-oob="true">
-              <li class="list-inline-item">
-                {% now 'Y-m-d H:i:s T' %}
-              </li>
-              <li class="list-inline-item">
-                {{ settings.HOSTNAME }} (v{{ settings.VERSION }})
-              </li>
+              <li class="list-inline-item">{% now 'Y-m-d H:i:s T' %}</li>
+              <li class="list-inline-item">{{ settings.HOSTNAME }}</li>
+              <li class="list-inline-item">{{ settings.RELEASE.name }}</li>
             </ul>
             {# /Footer text #}
 

+ 7 - 2
netbox/templates/core/system.html

@@ -28,8 +28,13 @@
         <h5 class="card-header">{% trans "System Status" %}</h5>
         <table class="table table-hover attr-table">
           <tr>
-            <th scope="row">{% trans "NetBox version" %}</th>
-            <td>{{ stats.netbox_version }}</td>
+            <th scope="row">{% trans "NetBox release" %}</th>
+            <td>
+              {{ stats.netbox_release.name }}
+              {% if stats.netbox_release.published %}
+               ({{ stats.netbox_release.published|isodate }})
+              {% endif %}
+            </td>
           </tr>
           <tr>
             <th scope="row">{% trans "Python version" %}</th>

+ 1 - 1
netbox/utilities/error_handlers.py

@@ -53,7 +53,7 @@ def handle_rest_api_exception(request, *args, **kwargs):
     data = {
         'error': str(error),
         'exception': type_.__name__,
-        'netbox_version': settings.VERSION,
+        'netbox_version': settings.RELEASE.full_version,
         'python_version': platform.python_version(),
     }
     return JsonResponse(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

+ 57 - 0
netbox/utilities/release.py

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