pulumi-django-azure 1.0.28__py3-none-any.whl → 1.0.59__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pulumi_django_azure/__init__.py +0 -2
- pulumi_django_azure/azure_helper.py +34 -40
- pulumi_django_azure/context_processors.py +42 -0
- pulumi_django_azure/django_deployment.py +153 -244
- pulumi_django_azure/management/commands/fix_cache_control.py +119 -0
- pulumi_django_azure/management/commands/purge_cache.py +10 -2
- pulumi_django_azure/management/commands/purge_cdn.py +10 -4
- pulumi_django_azure/management/commands/test_redis.py +248 -0
- pulumi_django_azure/middleware.py +26 -41
- pulumi_django_azure/settings.py +62 -34
- {pulumi_django_azure-1.0.28.dist-info → pulumi_django_azure-1.0.59.dist-info}/METADATA +251 -278
- pulumi_django_azure-1.0.59.dist-info/RECORD +14 -0
- {pulumi_django_azure-1.0.28.dist-info → pulumi_django_azure-1.0.59.dist-info}/WHEEL +1 -2
- pulumi_django_azure-1.0.28.dist-info/RECORD +0 -12
- pulumi_django_azure-1.0.28.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from azure.storage.blob import BlobServiceClient
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.core.management.base import BaseCommand
|
|
6
|
+
|
|
7
|
+
from pulumi_django_azure.azure_helper import AZURE_CREDENTIAL
|
|
8
|
+
|
|
9
|
+
DEFAULT_CACHE_CONTROL = "public,max-age=31536000,immutable"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Command(BaseCommand):
|
|
13
|
+
help = "Loops over all files in the static and media containers and applies the cache-control setting if it is not set yet."
|
|
14
|
+
|
|
15
|
+
def add_arguments(self, parser):
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--cache-control",
|
|
18
|
+
action="store",
|
|
19
|
+
default=DEFAULT_CACHE_CONTROL,
|
|
20
|
+
help="The cache-control setting to apply to the blobs",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--dry-run",
|
|
25
|
+
action="store_true",
|
|
26
|
+
help="Show what would be updated without actually updating the blobs",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def handle(self, *args, **options):
|
|
30
|
+
dry_run = options["dry_run"]
|
|
31
|
+
cache_control = options["cache_control"]
|
|
32
|
+
# Get storage account name and containers from settings
|
|
33
|
+
storage_account_name = getattr(settings, "AZURE_ACCOUNT_NAME", None)
|
|
34
|
+
if not storage_account_name:
|
|
35
|
+
self.stderr.write(self.style.ERROR("AZURE_ACCOUNT_NAME is not set in settings."))
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
# Get containers from environment variables
|
|
39
|
+
media_container = os.getenv("AZURE_STORAGE_CONTAINER_MEDIA")
|
|
40
|
+
static_container = os.getenv("AZURE_STORAGE_CONTAINER_STATICFILES")
|
|
41
|
+
|
|
42
|
+
containers = []
|
|
43
|
+
if media_container:
|
|
44
|
+
containers.append(("media", media_container))
|
|
45
|
+
if static_container:
|
|
46
|
+
containers.append(("static", static_container))
|
|
47
|
+
|
|
48
|
+
if not containers:
|
|
49
|
+
self.stderr.write(
|
|
50
|
+
self.style.ERROR("No containers configured (AZURE_STORAGE_CONTAINER_MEDIA or AZURE_STORAGE_CONTAINER_STATICFILES).")
|
|
51
|
+
)
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
# Get cache-control setting
|
|
55
|
+
self.stdout.write(f"Using cache-control: {cache_control}")
|
|
56
|
+
|
|
57
|
+
# Create BlobServiceClient
|
|
58
|
+
account_url = f"https://{storage_account_name}.blob.core.windows.net"
|
|
59
|
+
blob_service_client = BlobServiceClient(account_url=account_url, credential=AZURE_CREDENTIAL)
|
|
60
|
+
|
|
61
|
+
total_updated = 0
|
|
62
|
+
total_skipped = 0
|
|
63
|
+
total_errors = 0
|
|
64
|
+
|
|
65
|
+
for container_type, container_name in containers:
|
|
66
|
+
self.stdout.write(f"\nProcessing {container_type} container: {container_name}")
|
|
67
|
+
try:
|
|
68
|
+
container_client = blob_service_client.get_container_client(container_name)
|
|
69
|
+
|
|
70
|
+
# List all blobs in the container
|
|
71
|
+
blobs = container_client.list_blobs()
|
|
72
|
+
blob_count = 0
|
|
73
|
+
|
|
74
|
+
for blob in blobs:
|
|
75
|
+
blob_count += 1
|
|
76
|
+
blob_client = blob_service_client.get_blob_client(container=container_name, blob=blob.name)
|
|
77
|
+
|
|
78
|
+
# Get current blob properties
|
|
79
|
+
try:
|
|
80
|
+
properties = blob_client.get_blob_properties()
|
|
81
|
+
content_settings = properties.content_settings
|
|
82
|
+
current_cache_control = content_settings.cache_control if content_settings else None
|
|
83
|
+
|
|
84
|
+
# Check if cache-control is already set
|
|
85
|
+
if current_cache_control:
|
|
86
|
+
if dry_run:
|
|
87
|
+
self.stdout.write(f" [SKIP] {blob.name} (already has cache-control: {current_cache_control})")
|
|
88
|
+
total_skipped += 1
|
|
89
|
+
else:
|
|
90
|
+
# Set cache-control if not set
|
|
91
|
+
if dry_run:
|
|
92
|
+
self.stdout.write(f" [WOULD UPDATE] {blob.name}")
|
|
93
|
+
else:
|
|
94
|
+
# Create new content settings with the new cache-control
|
|
95
|
+
content_settings.cache_control = cache_control
|
|
96
|
+
blob_client.set_http_headers(content_settings=content_settings)
|
|
97
|
+
self.stdout.write(f" [UPDATED] {blob.name}")
|
|
98
|
+
total_updated += 1
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
self.stderr.write(self.style.ERROR(f" [ERROR] {blob.name}: {str(e)}"))
|
|
102
|
+
total_errors += 1
|
|
103
|
+
|
|
104
|
+
self.stdout.write(f"Processed {blob_count} blobs in {container_name}")
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
self.stderr.write(self.style.ERROR(f"Error processing container {container_name}: {str(e)}"))
|
|
108
|
+
total_errors += 1
|
|
109
|
+
|
|
110
|
+
# Summary
|
|
111
|
+
self.stdout.write("\n" + "=" * 60)
|
|
112
|
+
self.stdout.write("Summary:")
|
|
113
|
+
if dry_run:
|
|
114
|
+
self.stdout.write(f" Would update: {total_updated} blobs")
|
|
115
|
+
else:
|
|
116
|
+
self.stdout.write(f" Updated: {total_updated} blobs")
|
|
117
|
+
self.stdout.write(f" Skipped (already set): {total_skipped} blobs")
|
|
118
|
+
self.stdout.write(f" Errors: {total_errors} blobs")
|
|
119
|
+
self.stdout.write("=" * 60)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from django.core.cache import cache
|
|
2
2
|
from django.core.management.base import BaseCommand
|
|
3
|
+
from django_redis import get_redis_connection
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class Command(BaseCommand):
|
|
@@ -10,6 +11,13 @@ class Command(BaseCommand):
|
|
|
10
11
|
|
|
11
12
|
try:
|
|
12
13
|
cache.clear()
|
|
13
|
-
self.stdout.write(self.style.SUCCESS("Successfully purged cache."))
|
|
14
|
+
self.stdout.write(self.style.SUCCESS("Successfully purged cache using cache.clear()."))
|
|
14
15
|
except Exception as e:
|
|
15
|
-
self.stdout.write(self.style.ERROR(f"Failed to purge cache: {e}"))
|
|
16
|
+
self.stdout.write(self.style.ERROR(f"Failed to purge cache using cache.clear(): {e}"))
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
redis_conn = get_redis_connection("default")
|
|
20
|
+
redis_conn.flushall()
|
|
21
|
+
self.stdout.write(self.style.SUCCESS("Successfully purged cache using redis flushall()."))
|
|
22
|
+
except Exception as e:
|
|
23
|
+
self.stdout.write(self.style.ERROR(f"Failed to purge cache using redis flushall(): {e}"))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
3
|
from azure.mgmt.cdn import CdnManagementClient
|
|
4
|
-
from azure.mgmt.cdn.models import
|
|
4
|
+
from azure.mgmt.cdn.models import AfdPurgeParameters
|
|
5
5
|
from django.conf import settings
|
|
6
6
|
from django.core.management.base import BaseCommand
|
|
7
7
|
|
|
@@ -23,7 +23,10 @@ class Command(BaseCommand):
|
|
|
23
23
|
resource_group = os.getenv("WEBSITE_RESOURCE_GROUP")
|
|
24
24
|
profile_name = os.getenv("CDN_PROFILE")
|
|
25
25
|
endpoint_name = os.getenv("CDN_ENDPOINT")
|
|
26
|
-
|
|
26
|
+
cdn_host = os.getenv("CDN_HOST")
|
|
27
|
+
media_container = os.getenv("AZURE_STORAGE_CONTAINER_MEDIA")
|
|
28
|
+
static_container = os.getenv("AZURE_STORAGE_CONTAINER_STATICFILES")
|
|
29
|
+
content_paths = [f"/{media_container}/*", f"/{static_container}/*"]
|
|
27
30
|
|
|
28
31
|
# Ensure all required environment variables are set
|
|
29
32
|
if not all([resource_group, profile_name, endpoint_name]):
|
|
@@ -35,11 +38,14 @@ class Command(BaseCommand):
|
|
|
35
38
|
|
|
36
39
|
try:
|
|
37
40
|
# Purge the CDN endpoint
|
|
38
|
-
purge_operation = cdn_client.
|
|
41
|
+
purge_operation = cdn_client.afd_endpoints.begin_purge_content(
|
|
39
42
|
resource_group_name=resource_group,
|
|
40
43
|
profile_name=profile_name,
|
|
41
44
|
endpoint_name=endpoint_name,
|
|
42
|
-
|
|
45
|
+
contents=AfdPurgeParameters(
|
|
46
|
+
domains=[cdn_host],
|
|
47
|
+
content_paths=content_paths,
|
|
48
|
+
),
|
|
43
49
|
)
|
|
44
50
|
|
|
45
51
|
# Check if the --wait argument was provided
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Management command to test Redis connectivity and functionality.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from django.core.cache import cache
|
|
9
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Command(BaseCommand):
|
|
13
|
+
help = "Test Redis connectivity and basic functionality"
|
|
14
|
+
|
|
15
|
+
def add_arguments(self, parser):
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--detailed",
|
|
18
|
+
action="store_true",
|
|
19
|
+
help="Show detailed Redis information and run comprehensive tests",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--quiet",
|
|
23
|
+
action="store_true",
|
|
24
|
+
help="Only show errors and final status",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def handle(self, *args, **options):
|
|
28
|
+
verbosity = 0 if options["quiet"] else (2 if options["detailed"] else 1)
|
|
29
|
+
|
|
30
|
+
if verbosity >= 1:
|
|
31
|
+
self.stdout.write("Testing Redis connectivity...")
|
|
32
|
+
self.stdout.write("-" * 50)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
# Test basic connectivity
|
|
36
|
+
self._test_basic_connectivity(verbosity)
|
|
37
|
+
|
|
38
|
+
# Test basic cache operations
|
|
39
|
+
self._test_cache_operations(verbosity)
|
|
40
|
+
|
|
41
|
+
# If detailed mode, run additional tests
|
|
42
|
+
if options["detailed"]:
|
|
43
|
+
self._test_detailed_operations(verbosity)
|
|
44
|
+
self._show_cache_info(verbosity)
|
|
45
|
+
|
|
46
|
+
if verbosity >= 1:
|
|
47
|
+
self.stdout.write(self.style.SUCCESS("✓ All Redis tests passed successfully!"))
|
|
48
|
+
|
|
49
|
+
except Exception as e:
|
|
50
|
+
if verbosity >= 1:
|
|
51
|
+
self.stdout.write(self.style.ERROR(f"✗ Redis test failed: {str(e)}"))
|
|
52
|
+
raise CommandError(f"Redis connectivity test failed: {str(e)}") from e
|
|
53
|
+
|
|
54
|
+
def _test_basic_connectivity(self, verbosity):
|
|
55
|
+
"""Test basic Redis connectivity."""
|
|
56
|
+
if verbosity >= 2:
|
|
57
|
+
self.stdout.write("Testing basic connectivity...")
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# Try to set and get a simple value
|
|
61
|
+
cache.set("redis_test_key", "test_value", 30)
|
|
62
|
+
value = cache.get("redis_test_key")
|
|
63
|
+
|
|
64
|
+
if value != "test_value":
|
|
65
|
+
raise Exception("Failed to retrieve test value from cache")
|
|
66
|
+
|
|
67
|
+
# Clean up
|
|
68
|
+
cache.delete("redis_test_key")
|
|
69
|
+
|
|
70
|
+
if verbosity >= 2:
|
|
71
|
+
self.stdout.write(self.style.SUCCESS(" ✓ Basic connectivity test passed"))
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
raise Exception(f"Basic connectivity test failed: {str(e)}") from e
|
|
75
|
+
|
|
76
|
+
def _test_cache_operations(self, verbosity):
|
|
77
|
+
"""Test various cache operations."""
|
|
78
|
+
if verbosity >= 2:
|
|
79
|
+
self.stdout.write("Testing cache operations...")
|
|
80
|
+
|
|
81
|
+
test_data = {
|
|
82
|
+
"string_test": "Hello Redis!",
|
|
83
|
+
"number_test": 42,
|
|
84
|
+
"dict_test": {"key": "value", "nested": {"data": True}},
|
|
85
|
+
"list_test": [1, 2, 3, "four", 5.0],
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
# Test different data types
|
|
90
|
+
for key, value in test_data.items():
|
|
91
|
+
cache.set(f"test_{key}", value, 60)
|
|
92
|
+
retrieved_value = cache.get(f"test_{key}")
|
|
93
|
+
|
|
94
|
+
if retrieved_value != value:
|
|
95
|
+
raise Exception(f"Data mismatch for {key}: expected {value}, got {retrieved_value}")
|
|
96
|
+
|
|
97
|
+
if verbosity >= 2:
|
|
98
|
+
self.stdout.write(f" ✓ {key}: {type(value).__name__} data test passed")
|
|
99
|
+
|
|
100
|
+
# Test cache.get_or_set
|
|
101
|
+
default_value = "default_from_function"
|
|
102
|
+
result = cache.get_or_set("test_get_or_set", default_value, 60)
|
|
103
|
+
if result != default_value:
|
|
104
|
+
raise Exception("get_or_set test failed")
|
|
105
|
+
|
|
106
|
+
if verbosity >= 2:
|
|
107
|
+
self.stdout.write(" ✓ get_or_set operation test passed")
|
|
108
|
+
|
|
109
|
+
# Test cache.add (should fail on existing key)
|
|
110
|
+
if cache.add("test_string_test", "should_not_override"):
|
|
111
|
+
raise Exception("add() should have failed on existing key")
|
|
112
|
+
|
|
113
|
+
if verbosity >= 2:
|
|
114
|
+
self.stdout.write(" ✓ add operation test passed")
|
|
115
|
+
|
|
116
|
+
# Test cache expiration
|
|
117
|
+
cache.set("test_expiration", "will_expire", 1)
|
|
118
|
+
time.sleep(1.1) # Wait for expiration
|
|
119
|
+
expired_value = cache.get("test_expiration")
|
|
120
|
+
if expired_value is not None:
|
|
121
|
+
raise Exception("Cache expiration test failed - value should have expired")
|
|
122
|
+
|
|
123
|
+
if verbosity >= 2:
|
|
124
|
+
self.stdout.write(" ✓ Cache expiration test passed")
|
|
125
|
+
|
|
126
|
+
# Clean up test keys
|
|
127
|
+
for key in test_data:
|
|
128
|
+
cache.delete(f"test_{key}")
|
|
129
|
+
cache.delete("test_get_or_set")
|
|
130
|
+
|
|
131
|
+
if verbosity >= 2:
|
|
132
|
+
self.stdout.write(self.style.SUCCESS(" ✓ All cache operations tests passed"))
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
raise Exception(f"Cache operations test failed: {str(e)}") from e
|
|
136
|
+
|
|
137
|
+
def _test_detailed_operations(self, verbosity):
|
|
138
|
+
"""Run detailed Redis operations tests."""
|
|
139
|
+
if verbosity >= 2:
|
|
140
|
+
self.stdout.write("Running detailed operations...")
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
# Test cache.get_many and set_many
|
|
144
|
+
test_data = {
|
|
145
|
+
"bulk_key_1": "value_1",
|
|
146
|
+
"bulk_key_2": "value_2",
|
|
147
|
+
"bulk_key_3": "value_3",
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
cache.set_many(test_data, 60)
|
|
151
|
+
retrieved_data = cache.get_many(test_data.keys())
|
|
152
|
+
|
|
153
|
+
for key, expected_value in test_data.items():
|
|
154
|
+
if retrieved_data.get(key) != expected_value:
|
|
155
|
+
raise Exception(f"Bulk operation failed for key {key}")
|
|
156
|
+
|
|
157
|
+
if verbosity >= 2:
|
|
158
|
+
self.stdout.write(" ✓ Bulk operations (set_many/get_many) test passed")
|
|
159
|
+
|
|
160
|
+
# Test cache.delete_many
|
|
161
|
+
cache.delete_many(test_data.keys())
|
|
162
|
+
remaining_data = cache.get_many(test_data.keys())
|
|
163
|
+
if any(remaining_data.values()):
|
|
164
|
+
raise Exception("delete_many operation failed")
|
|
165
|
+
|
|
166
|
+
if verbosity >= 2:
|
|
167
|
+
self.stdout.write(" ✓ Bulk delete (delete_many) test passed")
|
|
168
|
+
|
|
169
|
+
# Test cache versioning if supported
|
|
170
|
+
try:
|
|
171
|
+
cache.set("version_test", "version_1", 60, version=1)
|
|
172
|
+
cache.set("version_test", "version_2", 60, version=2)
|
|
173
|
+
|
|
174
|
+
v1_value = cache.get("version_test", version=1)
|
|
175
|
+
v2_value = cache.get("version_test", version=2)
|
|
176
|
+
|
|
177
|
+
if v1_value == "version_1" and v2_value == "version_2":
|
|
178
|
+
if verbosity >= 2:
|
|
179
|
+
self.stdout.write(" ✓ Cache versioning test passed")
|
|
180
|
+
else:
|
|
181
|
+
if verbosity >= 2:
|
|
182
|
+
self.stdout.write(" ! Cache versioning not supported or failed")
|
|
183
|
+
|
|
184
|
+
cache.delete("version_test", version=1)
|
|
185
|
+
cache.delete("version_test", version=2)
|
|
186
|
+
|
|
187
|
+
except Exception:
|
|
188
|
+
if verbosity >= 2:
|
|
189
|
+
self.stdout.write(" ! Cache versioning not supported")
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
raise Exception(f"Detailed operations test failed: {str(e)}") from e
|
|
193
|
+
|
|
194
|
+
def _show_cache_info(self, verbosity):
|
|
195
|
+
"""Show information about the cache configuration."""
|
|
196
|
+
if verbosity >= 2:
|
|
197
|
+
self.stdout.write("\nCache Configuration Information:")
|
|
198
|
+
self.stdout.write("-" * 35)
|
|
199
|
+
|
|
200
|
+
# Show cache backend information
|
|
201
|
+
cache_config = getattr(settings, "CACHES", {}).get("default", {})
|
|
202
|
+
backend = cache_config.get("BACKEND", "Unknown")
|
|
203
|
+
location = cache_config.get("LOCATION", "Not specified")
|
|
204
|
+
|
|
205
|
+
self.stdout.write(f"Backend: {backend}")
|
|
206
|
+
self.stdout.write(f"Location: {location}")
|
|
207
|
+
|
|
208
|
+
# Show cache options if any
|
|
209
|
+
options = cache_config.get("OPTIONS", {})
|
|
210
|
+
if options:
|
|
211
|
+
self.stdout.write("Options:")
|
|
212
|
+
for key, value in options.items():
|
|
213
|
+
# Don't show sensitive information like passwords
|
|
214
|
+
if "password" in key.lower() or "secret" in key.lower():
|
|
215
|
+
value = "***hidden***"
|
|
216
|
+
self.stdout.write(f" {key}: {value}")
|
|
217
|
+
|
|
218
|
+
# Try to get cache stats if available
|
|
219
|
+
try:
|
|
220
|
+
if hasattr(cache, "_cache") and hasattr(cache._cache, "get_stats"):
|
|
221
|
+
stats = cache._cache.get_stats()
|
|
222
|
+
if stats:
|
|
223
|
+
self.stdout.write("\nCache Statistics:")
|
|
224
|
+
for stat_key, stat_value in stats.items():
|
|
225
|
+
self.stdout.write(f" {stat_key}: {stat_value}")
|
|
226
|
+
except Exception:
|
|
227
|
+
pass # Stats not available
|
|
228
|
+
|
|
229
|
+
# Test if we can determine Redis info
|
|
230
|
+
try:
|
|
231
|
+
# Try to access Redis client directly if using django-redis
|
|
232
|
+
if hasattr(cache, "_cache") and hasattr(cache._cache, "_client"):
|
|
233
|
+
client = cache._cache._client
|
|
234
|
+
if hasattr(client, "connection_pool"):
|
|
235
|
+
pool = client.connection_pool
|
|
236
|
+
self.stdout.write("\nConnection Pool Info:")
|
|
237
|
+
self.stdout.write(f" Max connections: {getattr(pool, 'max_connections', 'Unknown')}")
|
|
238
|
+
self.stdout.write(f" Connection class: {getattr(pool, 'connection_class', 'Unknown')}")
|
|
239
|
+
|
|
240
|
+
connection_kwargs = getattr(pool, "connection_kwargs", {})
|
|
241
|
+
if connection_kwargs:
|
|
242
|
+
self.stdout.write(" Connection parameters:")
|
|
243
|
+
for key, value in connection_kwargs.items():
|
|
244
|
+
if "password" in key.lower():
|
|
245
|
+
value = "***hidden***"
|
|
246
|
+
self.stdout.write(f" {key}: {value}")
|
|
247
|
+
except Exception:
|
|
248
|
+
pass # Redis-specific info not available
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
+
import signal
|
|
3
4
|
|
|
4
5
|
from django.conf import settings
|
|
5
|
-
from django.core.cache import cache
|
|
6
6
|
from django.db import connection
|
|
7
|
-
from django.db.utils import OperationalError
|
|
8
7
|
from django.http import HttpResponse
|
|
9
|
-
from django_redis import get_redis_connection
|
|
10
8
|
|
|
11
|
-
from .azure_helper import
|
|
9
|
+
from .azure_helper import get_db_password
|
|
12
10
|
|
|
13
11
|
logger = logging.getLogger("pulumi_django_azure.health_check")
|
|
14
12
|
|
|
@@ -18,47 +16,40 @@ class HealthCheckMiddleware:
|
|
|
18
16
|
self.get_response = get_response
|
|
19
17
|
|
|
20
18
|
def _self_heal(self):
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
os.
|
|
19
|
+
logger.warning("Self-healing by gracefully restarting Gunicorn.")
|
|
20
|
+
|
|
21
|
+
master_pid = os.getppid()
|
|
22
|
+
|
|
23
|
+
logger.debug("Master PID: %d", master_pid)
|
|
24
|
+
|
|
25
|
+
# https://docs.gunicorn.org/en/latest/signals.html
|
|
26
|
+
|
|
27
|
+
# Reload a new master with new workers,
|
|
28
|
+
# since the application is preloaded this is the only safe way for now.
|
|
29
|
+
os.kill(master_pid, signal.SIGUSR2)
|
|
30
|
+
|
|
31
|
+
# Gracefully shutdown the current workers
|
|
32
|
+
os.kill(master_pid, signal.SIGTERM)
|
|
24
33
|
|
|
25
34
|
def __call__(self, request):
|
|
26
35
|
if request.path == settings.HEALTH_CHECK_PATH:
|
|
27
36
|
# Update the database credentials if needed
|
|
28
37
|
if settings.AZURE_DB_PASSWORD:
|
|
29
38
|
try:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
settings.DATABASES["default"]["PASSWORD"] = get_db_password()
|
|
33
|
-
else:
|
|
34
|
-
logger.debug("Database token is still valid, skipping credentials update")
|
|
35
|
-
except Exception as e:
|
|
36
|
-
logger.error("Failed to update database credentials: %s", str(e))
|
|
37
|
-
|
|
38
|
-
self._self_heal()
|
|
39
|
-
|
|
40
|
-
return HttpResponse(status=503)
|
|
41
|
-
|
|
42
|
-
# Update the Redis credentials if needed
|
|
43
|
-
if settings.AZURE_REDIS_CREDENTIALS:
|
|
44
|
-
try:
|
|
45
|
-
if redis_token_will_expire():
|
|
46
|
-
logger.debug("Redis token will expire, fetching new credentials")
|
|
47
|
-
|
|
48
|
-
redis_credentials = get_redis_credentials()
|
|
39
|
+
current_db_password = settings.DATABASES["default"]["PASSWORD"]
|
|
40
|
+
new_db_password = get_db_password()
|
|
49
41
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
if new_db_password != current_db_password:
|
|
43
|
+
logger.debug("Database password has changed, updating credentials")
|
|
44
|
+
settings.DATABASES["default"]["PASSWORD"] = new_db_password
|
|
53
45
|
|
|
54
|
-
|
|
46
|
+
# Close existing connections to force reconnect with new password
|
|
47
|
+
connection.close()
|
|
55
48
|
else:
|
|
56
|
-
logger.debug("
|
|
49
|
+
logger.debug("Database password unchanged, keeping existing credentials")
|
|
57
50
|
except Exception as e:
|
|
58
|
-
logger.error("Failed to update
|
|
59
|
-
|
|
51
|
+
logger.error("Failed to update database credentials: %s", str(e))
|
|
60
52
|
self._self_heal()
|
|
61
|
-
|
|
62
53
|
return HttpResponse(status=503)
|
|
63
54
|
|
|
64
55
|
try:
|
|
@@ -66,17 +57,11 @@ class HealthCheckMiddleware:
|
|
|
66
57
|
connection.ensure_connection()
|
|
67
58
|
logger.debug("Database connection check passed")
|
|
68
59
|
|
|
69
|
-
# Test the Redis connection
|
|
70
|
-
cache.set("health_check", "test")
|
|
71
|
-
logger.debug("Redis connection check passed")
|
|
72
|
-
|
|
73
60
|
return HttpResponse("OK")
|
|
74
61
|
|
|
75
|
-
except OperationalError as e:
|
|
76
|
-
logger.error("Database connection failed: %s", str(e))
|
|
77
|
-
return HttpResponse(status=503)
|
|
78
62
|
except Exception as e:
|
|
79
63
|
logger.error("Health check failed with unexpected error: %s", str(e))
|
|
64
|
+
self._self_heal()
|
|
80
65
|
return HttpResponse(status=503)
|
|
81
66
|
|
|
82
67
|
return self.get_response(request)
|