Skip to content

Commit dc81d10

Browse files
authored
Redis ha/aap 28152 (#1012)
1 parent 0028e72 commit dc81d10

File tree

3 files changed

+179
-14
lines changed

3 files changed

+179
-14
lines changed

src/aap_eda/api/views/activation.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
stop_rulebook_process,
4141
)
4242

43+
from .mixins import RedisDependencyMixin
44+
4345
logger = logging.getLogger(__name__)
4446

4547
resource_name = "RulebookActivation"
@@ -48,6 +50,7 @@
4850
class ActivationViewSet(
4951
mixins.DestroyModelMixin,
5052
viewsets.GenericViewSet,
53+
RedisDependencyMixin,
5154
):
5255
queryset = models.Activation.objects.all()
5356
serializer_class = serializers.ActivationSerializer
@@ -70,7 +73,8 @@ def filter_queryset(self, queryset):
7073
status.HTTP_400_BAD_REQUEST: OpenApiResponse(
7174
description="Invalid data to create activation."
7275
),
73-
},
76+
}
77+
| RedisDependencyMixin.redis_unavailable_response(),
7478
)
7579
def create(self, request):
7680
context = {"request": request}
@@ -79,6 +83,14 @@ def create(self, request):
7983
)
8084
serializer.is_valid(raise_exception=True)
8185

86+
# If we're expected to run this activation we need redis
87+
# to be available.
88+
if serializer.validated_data.get(
89+
"is_enabled",
90+
models.activation.DEFAULT_ENABLED,
91+
):
92+
self.redis_is_available()
93+
8294
with transaction.atomic():
8395
response = serializer.create(serializer.validated_data)
8496
check_related_permissions(
@@ -119,7 +131,8 @@ def create(self, request):
119131
None,
120132
description="The Activation has been deleted.",
121133
),
122-
},
134+
}
135+
| RedisDependencyMixin.redis_unavailable_response(),
123136
)
124137
def destroy(self, request, *args, **kwargs):
125138
activation = self.get_object()
@@ -130,6 +143,9 @@ def destroy(self, request, *args, **kwargs):
130143
"deleted.",
131144
)
132145

146+
# Redis must be available in order to perform the delete.
147+
self.redis_is_available()
148+
133149
audit_log = logging_utils.generate_simple_audit_log(
134150
"Delete",
135151
resource_name,
@@ -255,9 +271,7 @@ def instances(self, request, id):
255271
description="Activation not enabled.",
256272
),
257273
status.HTTP_409_CONFLICT: OpenApiResponse(
258-
None,
259-
description="Activation not enabled do to current activation "
260-
"status",
274+
description="Processing of enable is disallowed."
261275
),
262276
},
263277
)
@@ -275,7 +289,10 @@ def enable(self, request, pk):
275289
ActivationStatus.RUNNING,
276290
ActivationStatus.UNRESPONSIVE,
277291
]:
278-
return Response(status=status.HTTP_409_CONFLICT)
292+
return Response(
293+
data="Activation not enabled due to current activation status",
294+
status=status.HTTP_409_CONFLICT,
295+
)
279296

280297
valid, error = is_activation_valid(activation)
281298
if not valid:
@@ -288,6 +305,9 @@ def enable(self, request, pk):
288305
{"errors": error}, status=status.HTTP_400_BAD_REQUEST
289306
)
290307

308+
# Redis must be available in order to perform the enable.
309+
self.redis_is_available()
310+
291311
logger.info(f"Now enabling {activation.name} ...")
292312

293313
activation.is_enabled = True
@@ -326,7 +346,8 @@ def enable(self, request, pk):
326346
None,
327347
description="Activation has been disabled.",
328348
),
329-
},
349+
}
350+
| RedisDependencyMixin.redis_unavailable_response(),
330351
)
331352
@action(methods=["post"], detail=True, rbac_action=Action.DISABLE)
332353
def disable(self, request, pk):
@@ -335,6 +356,9 @@ def disable(self, request, pk):
335356
self._check_deleting(activation)
336357

337358
if activation.is_enabled:
359+
# Redis must be available in order to perform the delete.
360+
self.redis_is_available()
361+
338362
activation.status = ActivationStatus.STOPPING
339363
activation.is_enabled = False
340364
activation.save(
@@ -368,7 +392,8 @@ def disable(self, request, pk):
368392
None,
369393
description="Activation not enabled.",
370394
),
371-
},
395+
}
396+
| RedisDependencyMixin.redis_unavailable_response(),
372397
)
373398
@action(methods=["post"], detail=True, rbac_action=Action.RESTART)
374399
def restart(self, request, pk):
@@ -392,6 +417,9 @@ def restart(self, request, pk):
392417
{"errors": error}, status=status.HTTP_400_BAD_REQUEST
393418
)
394419

420+
# Redis must be available in order to perform the restart.
421+
self.redis_is_available()
422+
395423
restart_rulebook_process(
396424
process_parent_type=ProcessParentType.ACTIVATION,
397425
process_parent_id=activation.id,

src/aap_eda/core/models/activation.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030

3131
__all__ = ("Activation",)
3232

33+
DEFAULT_ENABLED = True
34+
3335

3436
class Activation(
3537
StatusHandlerModelMixin,
@@ -53,7 +55,7 @@ class Meta:
5355
default="",
5456
blank=True,
5557
)
56-
is_enabled = models.BooleanField(default=True)
58+
is_enabled = models.BooleanField(default=DEFAULT_ENABLED)
5759
git_hash = models.TextField(null=False, default="")
5860
# TODO(alex) Since local activations are no longer supported
5961
# this field should be mandatory.

tests/integration/api/test_activation.py

Lines changed: 140 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
from typing import Any, Dict, List
15-
from unittest.mock import patch
15+
from unittest import mock
1616

1717
import pytest
1818
import yaml
@@ -36,7 +36,7 @@ def converted_extra_var(var: str) -> str:
3636

3737

3838
@pytest.mark.django_db
39-
@patch.object(settings, "RULEBOOK_WORKER_QUEUES", [])
39+
@mock.patch.object(settings, "RULEBOOK_WORKER_QUEUES", [])
4040
def test_create_activation(
4141
admin_awx_token: models.AwxToken,
4242
activation_payload: Dict[str, Any],
@@ -74,7 +74,7 @@ def test_create_activation(
7474

7575

7676
@pytest.mark.django_db
77-
@patch.object(settings, "RULEBOOK_WORKER_QUEUES", [])
77+
@mock.patch.object(settings, "RULEBOOK_WORKER_QUEUES", [])
7878
def test_create_activation_blank_text(
7979
admin_awx_token: models.AwxToken,
8080
activation_payload_blank_text: Dict[str, Any],
@@ -163,6 +163,35 @@ def test_create_activation_bad_entity(admin_client: APIClient):
163163
assert response.status_code == status.HTTP_400_BAD_REQUEST
164164

165165

166+
@pytest.mark.parametrize(
167+
"enabled",
168+
[True, False],
169+
)
170+
@pytest.mark.django_db
171+
@mock.patch("aap_eda.core.tasking.is_redis_failed", return_value=True)
172+
def test_create_activation_redis_unavailable(
173+
is_redis_failed: mock.Mock,
174+
activation_payload: Dict[str, Any],
175+
admin_awx_token: models.AwxToken,
176+
default_rulebook: models.Rulebook,
177+
admin_client: APIClient,
178+
enabled: bool,
179+
preseed_credential_types,
180+
):
181+
activation_payload["is_enabled"] = enabled
182+
response = admin_client.post(
183+
f"{api_url_v1}/activations/", data=activation_payload
184+
)
185+
186+
if not enabled:
187+
assert response.status_code == status.HTTP_201_CREATED
188+
else:
189+
assert response.status_code == status.HTTP_409_CONFLICT
190+
assert response.json() == {
191+
"detail": "Redis is required but unavailable."
192+
}
193+
194+
166195
@pytest.mark.parametrize(
167196
"dependent_object",
168197
[
@@ -442,6 +471,21 @@ def test_delete_activation(
442471
assert response.status_code == expected_response
443472

444473

474+
@pytest.mark.django_db
475+
@mock.patch("aap_eda.core.tasking.is_redis_failed", return_value=True)
476+
def test_delete_activation_redis_unavailable(
477+
is_redis_failed: mock.Mock,
478+
default_activation: models.Activation,
479+
admin_client: APIClient,
480+
preseed_credential_types,
481+
):
482+
response = admin_client.delete(
483+
f"{api_url_v1}/activations/{default_activation.id}/"
484+
)
485+
assert response.status_code == status.HTTP_409_CONFLICT
486+
assert response.json() == {"detail": "Redis is required but unavailable."}
487+
488+
445489
@pytest.mark.django_db
446490
def test_restart_activation(
447491
default_activation: models.Activation,
@@ -455,6 +499,38 @@ def test_restart_activation(
455499
assert response.status_code == status.HTTP_204_NO_CONTENT
456500

457501

502+
@pytest.mark.parametrize(
503+
"enabled",
504+
[True, False],
505+
)
506+
@pytest.mark.django_db
507+
@mock.patch("aap_eda.core.tasking.is_redis_failed", return_value=True)
508+
def test_restart_activation_redis_unavailable(
509+
is_redis_failed: mock.Mock,
510+
default_activation: models.Activation,
511+
admin_client: APIClient,
512+
enabled: bool,
513+
preseed_credential_types,
514+
):
515+
default_activation.is_enabled = enabled
516+
default_activation.save(update_fields=["is_enabled"])
517+
518+
response = admin_client.post(
519+
f"{api_url_v1}/activations/{default_activation.id}/restart/"
520+
)
521+
522+
if not enabled:
523+
assert response.status_code == status.HTTP_403_FORBIDDEN
524+
assert response.json() == {
525+
"detail": "Activation is disabled and cannot be run."
526+
}
527+
else:
528+
assert response.status_code == status.HTTP_409_CONFLICT
529+
assert response.json() == {
530+
"detail": "Redis is required but unavailable."
531+
}
532+
533+
458534
@pytest.mark.django_db
459535
@pytest.mark.parametrize("action", [enums.Action.RESTART, enums.Action.ENABLE])
460536
def test_restart_activation_without_de(
@@ -489,7 +565,7 @@ def test_restart_activation_without_de(
489565

490566

491567
@pytest.mark.django_db
492-
@patch.object(settings, "RULEBOOK_WORKER_QUEUES", [])
568+
@mock.patch.object(settings, "RULEBOOK_WORKER_QUEUES", [])
493569
def test_enable_activation(
494570
default_activation: models.Activation,
495571
admin_client: APIClient,
@@ -537,6 +613,36 @@ def test_enable_activation(
537613
)
538614

539615

616+
@pytest.mark.parametrize(
617+
"enabled",
618+
[True, False],
619+
)
620+
@pytest.mark.django_db
621+
@mock.patch.object(settings, "RULEBOOK_WORKER_QUEUES", [])
622+
@mock.patch("aap_eda.core.tasking.is_redis_failed", return_value=True)
623+
def test_enable_activation_redis_unavailable(
624+
is_redis_failed: mock.Mock,
625+
default_activation: models.Activation,
626+
admin_client: APIClient,
627+
enabled: bool,
628+
preseed_credential_types,
629+
):
630+
default_activation.is_enabled = enabled
631+
default_activation.save(update_fields=["is_enabled"])
632+
633+
response = admin_client.post(
634+
f"{api_url_v1}/activations/{default_activation.id}/enable/"
635+
)
636+
637+
if enabled:
638+
assert response.status_code == status.HTTP_204_NO_CONTENT
639+
else:
640+
assert response.status_code == status.HTTP_409_CONFLICT
641+
assert response.json() == {
642+
"detail": "Redis is required but unavailable."
643+
}
644+
645+
540646
@pytest.mark.django_db
541647
def test_disable_activation(
542648
default_activation: models.Activation,
@@ -550,6 +656,35 @@ def test_disable_activation(
550656
assert response.status_code == status.HTTP_204_NO_CONTENT
551657

552658

659+
@pytest.mark.parametrize(
660+
"enabled",
661+
[True, False],
662+
)
663+
@pytest.mark.django_db
664+
@mock.patch("aap_eda.core.tasking.is_redis_failed", return_value=True)
665+
def test_disable_activation_redis_unavailable(
666+
is_redis_failed: mock.Mock,
667+
default_activation: models.Activation,
668+
admin_client: APIClient,
669+
enabled: bool,
670+
preseed_credential_types,
671+
):
672+
default_activation.is_enabled = enabled
673+
default_activation.save(update_fields=["is_enabled"])
674+
675+
response = admin_client.post(
676+
f"{api_url_v1}/activations/{default_activation.id}/disable/"
677+
)
678+
679+
if not enabled:
680+
assert response.status_code == status.HTTP_204_NO_CONTENT
681+
else:
682+
assert response.status_code == status.HTTP_409_CONFLICT
683+
assert response.json() == {
684+
"detail": "Redis is required but unavailable."
685+
}
686+
687+
553688
@pytest.mark.django_db
554689
def test_list_activation_instances(
555690
default_activation: models.Activation,
@@ -812,7 +947,7 @@ def test_create_activation_with_awx_token(
812947

813948

814949
@pytest.mark.django_db
815-
@patch.object(settings, "RULEBOOK_WORKER_QUEUES", [])
950+
@mock.patch.object(settings, "RULEBOOK_WORKER_QUEUES", [])
816951
def test_create_activation_with_skip_audit_events(
817952
admin_awx_token: models.AwxToken,
818953
activation_payload_skip_audit_events: Dict[str, Any],

0 commit comments

Comments
 (0)