Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: builtin actions #537

Merged
merged 2 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
54 changes: 54 additions & 0 deletions ansible_rulebook/action/control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright 2023 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import asyncio
from dataclasses import dataclass
from typing import List


@dataclass(frozen=True)
class Control:
Alex-Izquierdo marked this conversation as resolved.
Show resolved Hide resolved
"""Control information when running an action
Attributes:
queue: asyncio.Queue
This is the queue on which we would be sending action status
periodically when the action is running
inventory: str
This is the inventory information from the command line
It currently is the data that is read from a file, in the future
it could be a directory or an inventory name from the controller
hosts: list[str]
The list of servers passed into ansible-playbook or controller
variables: dict
The variables passed in from the command line plus the matching event
data with event or events key.
project_data_file: str
This is the directory where the collection data is sent from the
AAP server over the websocket is untarred to. The collection could
contain the playbook that is used in the run_playbook action.
"""

__slots__ = [
"queue",
"inventory",
"hosts",
"variables",
"project_data_file",
]
queue: asyncio.Queue
inventory: str
hosts: List[str]
variables: dict
project_data_file: str
83 changes: 83 additions & 0 deletions ansible_rulebook/action/debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Copyright 2023 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import sys
from dataclasses import asdict
from pprint import pprint

import dpath
from drools import ruleset as lang

from ansible_rulebook.util import get_horizontal_rule

from .control import Control
from .helper import Helper
from .metadata import Metadata

logger = logging.getLogger(__name__)


class Debug:
"""The debug action tries to mimic the ansible debug task with optional
msg: Prints a message
var: Prints a variable
default: print the metadata, control information and facts from the
rule engine
At the end we send back the action status
"""

def __init__(self, metadata: Metadata, control: Control, **action_args):
self.helper = Helper(metadata, control, "debug")
self.action_args = action_args

async def __call__(self):
if "msg" in self.action_args:
messages = self.action_args.get("msg")
if not isinstance(messages, list):
messages = [messages]
for msg in messages:
print(msg)
elif "var" in self.action_args:
key = self.action_args.get("var")
try:
print(
dpath.get(
self.helper.control.variables, key, separator="."
)
)
except KeyError:
logger.error("Key %s not found in variable pool", key)
raise
else:
print(get_horizontal_rule("="))
print("kwargs:")
args = asdict(self.helper.metadata)
project_data_file = self.helper.control.project_data_file
args.update(
{
"inventory": self.helper.control.inventory,
"hosts": self.helper.control.hosts,
"variables": self.helper.control.variables,
"project_data_file": project_data_file,
}
)
pprint(args)
print(get_horizontal_rule("="))
print("facts:")
pprint(lang.get_facts(self.helper.metadata.rule_set))
print(get_horizontal_rule("="))

sys.stdout.flush()
await self.helper.send_default_status()
143 changes: 143 additions & 0 deletions ansible_rulebook/action/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Copyright 2023 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import uuid
from typing import Dict

from ansible_rulebook.conf import settings
from ansible_rulebook.event_filter.insert_meta_info import main as insert_meta
from ansible_rulebook.util import run_at

from .control import Control
from .metadata import Metadata

KEY_EDA_VARS = "ansible_eda"
INTERNAL_ACTION_STATUS = "successful"


class Helper:
"""
Helper class stores the metadata, the control attributes and has
methods to send data to the Queue.
Attributes
----------
metadata : Metadata
a data class that stores rule specific data
control : Control
a control dataclass that stores the runtime information about
the queue on which we send the status for the action, the inventory
information, the hosts data and the variables that we would like
to pass into the action
uuid : str
each action has a uuid that is generated to track it
action : str
the name of the action, set by the sub classe
Methods
-------
send_status(data={}, obj_type:"action")
Sends the action status information on the queue
send_default_status()
Sends the default action status, used mostly with internal
actions like debug, print_event, set_fact, retract_fact,
noop, post_event
get_events()
Fetches the matching events from the variables
collect_extra_vars()
Create extra_vars to be sent to playbook and job template which
includes rule and matching events.
embellish_internal_event()
Add internal sources for facts and events posted from inside of
a rulebook
"""

def __init__(self, metadata: Metadata, control: Control, action: str):
self.metadata = metadata
self.control = control
self.uuid = str(uuid.uuid4())
self.action = action

async def send_status(self, data: Dict, obj_type: str = "Action") -> None:
"""Send Action status information on the queue"""
payload = {
"type": obj_type,
"action": self.action,
"action_uuid": self.uuid,
"ruleset": self.metadata.rule_set,
"ruleset_uuid": self.metadata.rule_set_uuid,
"rule": self.metadata.rule,
"rule_uuid": self.metadata.rule_uuid,
"rule_run_at": self.metadata.rule_run_at,
"activation_id": settings.identifier,
"activation_instance_id": settings.identifier,
}
payload.update(data)
await self.control.queue.put(payload)

async def send_default_status(self):
"""Send default action status information on the queue"""
await self.send_status(
{
"run_at": run_at(),
"status": INTERNAL_ACTION_STATUS,
"matching_events": self.get_events(),
}
)

def get_events(self) -> Dict:
"""From the control variables, detect if its a single event
match or a multi event match and return a dictionary with
the event data with
m key for single event stored in the event key
m_0,m_1,.... for multiple matching events stored in
the events key
"""
if "event" in self.control.variables:
return {"m": self.control.variables["event"]}
if "events" in self.control.variables:
return self.control.variables["events"]
return {}

def embellish_internal_event(self, event: Dict) -> Dict:
"""Insert metadata for every internally generated event"""
return insert_meta(
event, **{"source_name": self.action, "source_type": "internal"}
)

def set_action(self, action) -> None:
self.action = action

def collect_extra_vars(self, user_extra_vars: Dict) -> Dict:
"""When we send information to ansible-playbook or job template
on AWX, we need the rule and event specific information to
be sent to this external process
the caller passes in the user_extra_vars from the action args
and then we append eda specific vars and return that as a
the updated dictionary that is sent to the external process
"""
extra_vars = user_extra_vars.copy() if user_extra_vars else {}

eda_vars = {
"ruleset": self.metadata.rule_set,
"rule": self.metadata.rule,
}
if "events" in self.control.variables:
eda_vars["events"] = self.control.variables["events"]
if "event" in self.control.variables:
eda_vars["event"] = self.control.variables["event"]

extra_vars[KEY_EDA_VARS] = eda_vars
return extra_vars
48 changes: 48 additions & 0 deletions ansible_rulebook/action/metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright 2023 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from dataclasses import dataclass


@dataclass(frozen=True)
class Metadata:
"""Metadata class stores the rule specific information
which is used when reporting stats for the action
Attributes
----------
rule: str
Rule name
rule_uuid: str
Rule uuid
rule_set: str
Rule set name
rule_set_uuid: str
Rule set uuid
rule_run_at: str
ISO 8601 date/time when the rule was triggered
"""

__slots__ = [
"rule",
"rule_uuid",
"rule_set",
"rule_set_uuid",
"rule_run_at",
]
rule: str
rule_uuid: str
rule_set: str
rule_set_uuid: str
rule_run_at: str
34 changes: 34 additions & 0 deletions ansible_rulebook/action/noop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright 2023 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging

from .control import Control
from .helper import Helper
from .metadata import Metadata

logger = logging.getLogger(__name__)


class Noop:
"""The No Op action usually used for debugging, doesn't do anything and
just sends the action status
"""

def __init__(self, metadata: Metadata, control: Control, **action_args):
self.helper = Helper(metadata, control, "noop")
self.action_args = action_args

async def __call__(self):
await self.helper.send_default_status()
Loading