Coverage for core/checks.py: 63%
67 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1import logging
3from django.apps import apps
4from django.core.cache import cache
5from django.core.checks import Error, Tags, register
6from django.core.exceptions import ImproperlyConfigured
7from django.db import DatabaseError, InterfaceError, NotSupportedError, OperationalError, connections
8from django.db.models import Index, UniqueConstraint
10__all__ = (
11 'check_duplicate_indexes',
12 'check_postgresql_version',
13 'check_redis_version',
14)
16# The minimum major version of PostgreSQL required by NetBox. `SHOW server_version_num` reports the
17# server version as a single integer of the form MMmmmm (e.g. 150004 for PostgreSQL 15.4), so the
18# major version is scaled by 10000 when comparing against it.
19POSTGRESQL_MIN_VERSION = 15
20POSTGRESQL_VERSION_MULTIPLIER = 10000
22logger = logging.getLogger('netbox.core.checks')
25@register(Tags.models)
26def check_duplicate_indexes(app_configs, **kwargs):
27 """
28 Check for an index which is redundant to a declared unique constraint.
29 """
30 errors = []
32 for model in apps.get_models():
33 if not (meta := getattr(model, "_meta", None)): 33 ↛ 34line 33 didn't jump to line 34 because the condition on line 33 was never true
34 continue
36 index_fields = {
37 tuple(index.fields) for index in getattr(meta, 'indexes', [])
38 if isinstance(index, Index)
39 }
40 constraint_fields = {
41 tuple(constraint.fields) for constraint in getattr(meta, 'constraints', [])
42 if isinstance(constraint, UniqueConstraint)
43 }
45 # Find overlapping definitions
46 if duplicated := index_fields & constraint_fields: 46 ↛ 47line 46 didn't jump to line 47 because the condition on line 46 was never true
47 for fields in duplicated:
48 errors.append(
49 Error(
50 f"Model '{model.__name__}' defines the same field set {fields} in both `Meta.indexes` and "
51 f"`Meta.constraints`.",
52 obj=model,
53 )
54 )
56 return errors
59@register(Tags.database)
60def check_postgresql_version(app_configs, databases=None, **kwargs):
61 """
62 Report an error if the PostgreSQL version is less than POSTGRESQL_MIN_VERSION.
63 """
64 errors = []
66 # Validate only those database aliases which Django has asked us to check. A value of None means
67 # that no database may be touched during this run, so there is nothing to validate: commands which
68 # do intend to use a connection declare its alias (e.g. `migrate` passes the value of --database).
69 # This mirrors Django's own database checks; see checks.database.check_database_backends().
70 if databases is None: 70 ↛ 71line 70 didn't jump to line 71 because the condition on line 70 was never true
71 return errors
73 for alias in databases:
74 connection = connections[alias]
76 # A plugin may register a connection to some other type of database; only PostgreSQL
77 # connections are subject to NetBox's minimum version requirement.
78 if connection.vendor != 'postgresql': 78 ↛ 79line 78 didn't jump to line 79 because the condition on line 78 was never true
79 continue
81 try:
82 with connection.cursor() as cursor:
83 cursor.execute('SHOW server_version_num')
84 row = cursor.fetchone()
85 except NotSupportedError:
86 # Django refuses to use the connection at all when the server predates the minimum version
87 # which Django itself supports (BaseDatabaseWrapper.check_database_version_supported()), so
88 # the query above never runs. The PostgreSQL backend registers no validation checks of its
89 # own, so report the requirement here rather than letting the raw exception surface as a
90 # traceback the first time something touches the database.
91 errors.append(
92 Error(
93 f"Database '{alias}': The configured PostgreSQL version is not supported. NetBox "
94 f"requires PostgreSQL {POSTGRESQL_MIN_VERSION} or later.",
95 hint=f'Please upgrade to PostgreSQL {POSTGRESQL_MIN_VERSION} or later.',
96 id='netbox.E001',
97 )
98 )
99 continue
100 except (ImproperlyConfigured, InterfaceError, OperationalError):
101 # The database is unreachable, has yet to be provisioned, or the connection is no longer
102 # usable. (InterfaceError is a sibling of DatabaseError, not a subclass, so it must be
103 # named explicitly.) Leave the version unverified rather than reporting a spurious error.
104 continue
105 except DatabaseError:
106 # The server is reachable but rejected the query (e.g. a connection pooler which intercepts
107 # SHOW). Record why the version could not be determined rather than failing silently.
108 logger.warning(f"Database '{alias}': Failed to determine the PostgreSQL version.", exc_info=True)
109 continue
111 if not row: 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true
112 logger.warning(f"Database '{alias}': `SHOW server_version_num` returned no result.")
113 continue
115 try:
116 pg_version = int(row[0])
117 except (TypeError, ValueError):
118 # `SHOW server_version_num` reports an integer, but a pooler which answers the statement
119 # itself may report a dotted version instead. Leave the version unverified rather than
120 # raising out of the check and aborting the calling command.
121 logger.warning(f"Database '{alias}': Unable to parse the PostgreSQL version from {row[0]!r}.")
122 continue
124 if pg_version < POSTGRESQL_MIN_VERSION * POSTGRESQL_VERSION_MULTIPLIER: 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true
125 major_version = pg_version // POSTGRESQL_VERSION_MULTIPLIER
126 errors.append(
127 Error(
128 f"Database '{alias}': PostgreSQL {major_version} is not supported. NetBox requires "
129 f"PostgreSQL {POSTGRESQL_MIN_VERSION} or later.",
130 hint=f'Please upgrade to PostgreSQL {POSTGRESQL_MIN_VERSION} or later.',
131 id='netbox.E001',
132 )
133 )
135 return errors
138@register(Tags.caches)
139def check_redis_version(app_configs, **kwargs):
140 """
141 Report an error if the Redis version is less than 6.0.
142 """
143 errors = []
144 try:
145 client = cache.client.get_client()
146 redis_version = tuple(int(x) for x in client.info()['redis_version'].split('.'))
147 if redis_version < (6, 0): 147 ↛ 148line 147 didn't jump to line 148 because the condition on line 147 was never true
148 errors.append(
149 Error(
150 f'Redis {".".join(str(x) for x in redis_version)} is not supported. NetBox requires Redis 6.0 '
151 f'or later.',
152 hint='Please upgrade to Redis 6.0 or later.',
153 id='netbox.E002',
154 )
155 )
156 except Exception:
157 pass
158 return errors