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.
@@ -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 PurgeParameters
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
- content_paths = ["/*"]
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.endpoints.begin_purge_content(
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
- content_file_paths=PurgeParameters(content_paths=content_paths),
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 db_token_will_expire, get_db_password, get_redis_credentials, redis_token_will_expire
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
- # Send HUP signal to gunicorn main thread,
22
- # which will trigger new workers to start.
23
- os.kill(os.getppid(), 1)
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
- if db_token_will_expire():
31
- logger.debug("Database token will expire, fetching new credentials")
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
- # Re-authenticate the Redis connection
51
- redis_connection = get_redis_connection("default")
52
- redis_connection.execute_command("AUTH", redis_credentials.username, redis_credentials.password)
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
- settings.CACHES["default"]["OPTIONS"]["PASSWORD"] = redis_credentials.password
46
+ # Close existing connections to force reconnect with new password
47
+ connection.close()
55
48
  else:
56
- logger.debug("Redis token is still valid, skipping credentials update")
49
+ logger.debug("Database password unchanged, keeping existing credentials")
57
50
  except Exception as e:
58
- logger.error("Failed to update Redis credentials: %s", str(e))
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)