Skip to content

Commit

Permalink
refactor: [AAP-20044] implement interface for "containerable" entities (
Browse files Browse the repository at this point in the history
#669)

Co-authored-by: Bill Wei <bilwei@redhat.com>
  • Loading branch information
jshimkus-rh and bzwei authored Feb 17, 2024
1 parent e98d6a6 commit 4abb207
Show file tree
Hide file tree
Showing 12 changed files with 483 additions and 144 deletions.
3 changes: 2 additions & 1 deletion src/aap_eda/core/models/activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
from django.db import models

from aap_eda.core.enums import ActivationStatus, RestartPolicy
from aap_eda.services.activation.engine.common import ContainerableMixin

from .mixins import StatusHandlerModelMixin
from .user import AwxToken, User

__all__ = ("Activation",)


class Activation(StatusHandlerModelMixin, models.Model):
class Activation(StatusHandlerModelMixin, ContainerableMixin, models.Model):
class Meta:
db_table = "core_activation"
indexes = [models.Index(fields=["name"], name="ix_activation_name")]
Expand Down
12 changes: 11 additions & 1 deletion src/aap_eda/core/models/event_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import typing as tp

from django.db import models

from aap_eda.core.enums import ActivationStatus, RestartPolicy
from aap_eda.services.activation.engine.common import ContainerableMixin

from .mixins import StatusHandlerModelMixin


class EventStream(StatusHandlerModelMixin, models.Model):
class EventStream(StatusHandlerModelMixin, ContainerableMixin, models.Model):
"""Model representing an event stream."""

name = models.TextField(null=False, unique=True)
Expand Down Expand Up @@ -97,3 +100,10 @@ class Meta:

def __str__(self) -> str:
return f"EventStream {self.name} ({self.id})"

# Implementation of the ContainerableMixin.
def get_command_line_parameters(self) -> dict[str, tp.Any]:
params = super().get_command_line_parameters()
return params | {
"skip_audit_events": True,
}
168 changes: 166 additions & 2 deletions src/aap_eda/services/activation/engine/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,19 @@


import typing as tp
import uuid
from abc import ABC, abstractmethod
from datetime import datetime

import yaml
from django.conf import settings
from pydantic import BaseModel, validator

import aap_eda.services.activation.engine.exceptions as exceptions
from aap_eda.core.enums import ActivationStatus
from aap_eda.services.auth import create_jwt_token

from .ports import find_ports


class LogHandler(ABC):
Expand Down Expand Up @@ -56,6 +61,7 @@ class AnsibleRulebookCmdLine(BaseModel):
heartbeat: int
id: tp.Union[str, int] # casted to str
log_level: tp.Optional[str] = None # -v or -vv or None
skip_audit_events: bool = False

@validator("id", pre=True)
def cast_to_str(cls, v):
Expand All @@ -82,6 +88,8 @@ def get_args(self, sanitized=False) -> list[str]:
"--heartbeat",
str(self.heartbeat),
]
if self.skip_audit_events:
args.append("--skip-audit-events")
if self.log_level:
args.append(self.log_level)

Expand All @@ -102,8 +110,8 @@ class ContainerRequest(BaseModel):
name: str # f"eda-{activation_instance.id}-{uuid.uuid4()}"
image_url: str # quay.io/ansible/ansible-rulebook:main
cmdline: AnsibleRulebookCmdLine
activation_instance_id: str
activation_id: str
rulebook_process_id: int
process_parent_id: int
credential: tp.Optional[Credential] = None
ports: tp.Optional[list[tuple]] = None
pull_policy: str = settings.DEFAULT_PULL_POLICY # Always by default
Expand All @@ -113,6 +121,162 @@ class ContainerRequest(BaseModel):
extra_args: tp.Optional[dict] = None


class ContainerableMixinError(Exception):
"""Base class for exceptions from implementers of ContainerableMixin."""

pass


class ContainerableInvalidError(ContainerableMixinError):
pass


class ContainerableNoLatestInstanceError(ContainerableMixinError):
pass


# To use ContainerableMixin the model class adding the mixin is required to
# have the following attributes (or property getters):
#
# Attribute Type
# --------- ----
# decision_environment DecisionEnvironment
# extra_var ExtraVar
# latest_instance RulebookProcess
# restart_policy str
# rulebook_rulesets str
#
class ContainerableMixin:
def get_command_line_parameters(self) -> dict[str, tp.Any]:
"""Return parameters for running ansible-rulebook."""
self.validate()

access_token, refresh_token = create_jwt_token()
return {
"id": str(self.latest_instance.id),
"ws_url": self._get_ws_url(),
"log_level": settings.ANSIBLE_RULEBOOK_LOG_LEVEL,
"ws_ssl_verify": settings.WEBSOCKET_SSL_VERIFY,
"ws_token_url": self._get_ws_token_url(),
"ws_access_token": access_token,
"ws_refresh_token": refresh_token,
"heartbeat": settings.RULEBOOK_LIVENESS_CHECK_SECONDS,
"skip_audit_events": False,
}

def get_container_parameters(self) -> dict[str, tp.Any]:
"""Return parameters used to create a ContainerRquest."""
self.validate()

return {
"credential": self._get_image_credential(),
"name": self._get_container_name(),
"image_url": self.decision_environment.image_url,
"ports": self._get_ports(),
"env_vars": settings.PODMAN_ENV_VARS,
"extra_args": settings.PODMAN_EXTRA_ARGS,
"mem_limit": settings.PODMAN_MEM_LIMIT,
"mounts": settings.PODMAN_MOUNTS,
"process_parent_id": self.id,
"rulebook_process_id": self.latest_instance.id,
"cmdline": self._build_cmdline(),
}

def get_container_request(self) -> ContainerRequest:
"""Return ContainerRequest used for creation."""
params = self.get_container_parameters()
return ContainerRequest(
credential=params["credential"],
name=params["name"],
image_url=params["image_url"],
ports=params["ports"],
process_parent_id=params["process_parent_id"],
rulebook_process_id=params["rulebook_process_id"],
env_vars=params["env_vars"],
extra_args=params["extra_args"],
mem_limit=params["mem_limit"],
mounts=params["mounts"],
cmdline=params["cmdline"],
)

def get_restart_policy(self) -> str:
"""Return the restart policy for the implementer.
We don't validate here as validation is for use to create a new
container and the value of the restart policy is not a determinate of
that.
"""
return self.restart_policy

def validate(self):
"""Validate the the implementer is appropriate to be containerized."""
try:
self._validate()
except ContainerableMixinError as e:
raise ContainerableInvalidError from e

def _build_cmdline(self) -> AnsibleRulebookCmdLine:
params = self.get_command_line_parameters()
return AnsibleRulebookCmdLine(
ws_url=params["ws_url"],
log_level=params["log_level"],
ws_ssl_verify=params["ws_ssl_verify"],
ws_access_token=params["ws_access_token"],
ws_refresh_token=params["ws_refresh_token"],
ws_token_url=params["ws_token_url"],
heartbeat=params["heartbeat"],
id=params["id"],
skip_audit_events=params["skip_audit_events"],
)

def _get_container_name(self) -> str:
"""Return the name to use for the ContainerRequest."""
return (
f"{settings.CONTAINER_NAME_PREFIX}-{self.latest_instance.id}"
f"-{uuid.uuid4()}"
)

def _get_context(self) -> dict[str, tp.Any]:
"""Return the context dictionary used to create a ContainerRquest."""
if self.extra_var:
context = yaml.safe_load(self.extra_var.extra_var)
else:
context = {}
return context

def _get_image_credential(self) -> tp.Optional[Credential]:
"""Return a decrypted Credential or None for the implementer."""
credential = self.decision_environment.credential
if credential:
return Credential(
username=credential.username,
secret=credential.secret.get_secret_value(),
)
return None

def _get_ports(self) -> list[tuple]:
return find_ports(self.rulebook_rulesets, self._get_context())

def _get_ws_url(self) -> str:
return f"{settings.WEBSOCKET_BASE_URL}{self._get_ws_url_subpath()}"

def _get_ws_url_subpath(self) -> str:
return f"/{settings.API_PREFIX}/ws/ansible-rulebook"

def _get_ws_token_url(self) -> str:
return (
f"{settings.WEBSOCKET_TOKEN_BASE_URL}"
f"{self._get_ws_token_url_subpath()}"
)

def _get_ws_token_url_subpath(self) -> str:
return f"/{settings.API_PREFIX}/v1/auth/token/refresh/"

def _validate(self):
if not self.latest_instance:
raise ContainerableNoLatestInstanceError


class ContainerStatus(BaseModel):
status: ActivationStatus
message: str = ""
Expand Down
8 changes: 4 additions & 4 deletions src/aap_eda/services/activation/engine/kubernetes.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,12 @@ def start(self, request: ContainerRequest, log_handler: LogHandler) -> str:
# TODO : Should this be compatible with the previous version
# Previous Version
self.job_name = (
f"{self.resource_prefix}-job-{request.activation_id}"
f"-{request.activation_instance_id}"
f"{self.resource_prefix}-job-{request.process_parent_id}"
f"-{request.rulebook_process_id}"
)
self.pod_name = (
f"{self.resource_prefix}-pod-{request.activation_id}"
f"-{request.activation_instance_id}"
f"{self.resource_prefix}-pod-{request.process_parent_id}"
f"-{request.rulebook_process_id}"
)

# Should we switch to new format
Expand Down
70 changes: 9 additions & 61 deletions src/aap_eda/services/activation/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@
import contextlib
import logging
import typing as tp
import uuid
from datetime import timedelta
from functools import wraps

import yaml
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
Expand All @@ -37,24 +35,16 @@
)
from aap_eda.services.activation import exceptions
from aap_eda.services.activation.engine import exceptions as engine_exceptions
from aap_eda.services.activation.engine.common import (
AnsibleRulebookCmdLine,
ContainerRequest,
Credential,
)
from aap_eda.services.activation.engine.common import ContainerRequest
from aap_eda.services.activation.restart_helper import (
system_restart_activation,
)
from aap_eda.services.auth import create_jwt_token

from .db_log_handler import DBLogger
from .engine.common import ContainerEngine
from .engine.common import ContainerableInvalidError, ContainerEngine
from .engine.factory import new_container_engine
from .engine.ports import find_ports

LOGGER = logging.getLogger(__name__)
ACTIVATION_PATH = f"/{settings.API_PREFIX}/ws/ansible-rulebook"
TOKEN_RENEW_PATH = f"/{settings.API_PREFIX}/v1/auth/token/refresh/"


class HasDbInstance(tp.Protocol):
Expand Down Expand Up @@ -282,7 +272,7 @@ def _start_activation_instance(self):

# start the container
try:
container_request = self._build_container_request()
container_request = self._get_container_request()
container_id = self.container_engine.start(
container_request,
log_handler,
Expand Down Expand Up @@ -1060,55 +1050,13 @@ def _create_activation_instance(self):
self._error_activation(msg)
raise exceptions.ActivationStartError(msg) from exc

def _build_container_request(self) -> ContainerRequest:
if self.db_instance.extra_var:
context = yaml.safe_load(self.db_instance.extra_var.extra_var)
else:
context = {}

return ContainerRequest(
credential=self._build_credential(),
cmdline=self._build_cmdline(),
name=(
f"{settings.CONTAINER_NAME_PREFIX}-{self.latest_instance.id}"
f"-{uuid.uuid4()}"
),
image_url=self.db_instance.decision_environment.image_url,
ports=find_ports(self.db_instance.rulebook_rulesets, context),
activation_id=self.db_instance.id,
activation_instance_id=self.latest_instance.id,
env_vars=settings.PODMAN_ENV_VARS,
extra_args=settings.PODMAN_EXTRA_ARGS,
mem_limit=settings.PODMAN_MEM_LIMIT,
mounts=settings.PODMAN_MOUNTS,
)

def _build_credential(self) -> tp.Optional[Credential]:
credential = self.db_instance.decision_environment.credential
if credential:
return Credential(
username=credential.username,
secret=credential.secret.get_secret_value(),
)
return None

def _build_cmdline(self) -> AnsibleRulebookCmdLine:
if not self.latest_instance:
def _get_container_request(self) -> ContainerRequest:
try:
return self.db_instance.get_container_request()
except ContainerableInvalidError:
msg = (
f"Activation {self.db_instance.id} does not have an instance, "
"cmdline can not be built."
f"Activation {self.db_instance.id} not valid, "
"container request cannot be built."
)
LOGGER.exception(msg)
raise exceptions.ActivationManagerError(msg)

access_token, refresh_token = create_jwt_token()
return AnsibleRulebookCmdLine(
ws_url=settings.WEBSOCKET_BASE_URL + ACTIVATION_PATH,
log_level=settings.ANSIBLE_RULEBOOK_LOG_LEVEL,
ws_ssl_verify=settings.WEBSOCKET_SSL_VERIFY,
ws_access_token=access_token,
ws_refresh_token=refresh_token,
ws_token_url=settings.WEBSOCKET_TOKEN_BASE_URL + TOKEN_RENEW_PATH,
heartbeat=settings.RULEBOOK_LIVENESS_CHECK_SECONDS,
id=str(self.latest_instance.id),
)
2 changes: 1 addition & 1 deletion src/aap_eda/services/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from rest_framework_simplejwt.tokens import RefreshToken

from aap_eda.core.models import User
from aap_eda.core.models.user import User


def group_permission_resource(permission_data):
Expand Down
Loading

0 comments on commit 4abb207

Please sign in to comment.