Coverage for core/checks.py: 63%

67 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1import logging 

2 

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 

9 

10__all__ = ( 

11 'check_duplicate_indexes', 

12 'check_postgresql_version', 

13 'check_redis_version', 

14) 

15 

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 

21 

22logger = logging.getLogger('netbox.core.checks') 

23 

24 

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 = [] 

31 

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 

35 

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 } 

44 

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 ) 

55 

56 return errors 

57 

58 

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 = [] 

65 

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 

72 

73 for alias in databases: 

74 connection = connections[alias] 

75 

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 

80 

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 

110 

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 

114 

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 

123 

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 ) 

134 

135 return errors 

136 

137 

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