Skip to content

Commit

Permalink
fix: Tox-compliant EDA code, and Tox checks in CI (#453)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesholland-uk authored Jun 23, 2023
1 parent 13aedcc commit 9a50c9b
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 50 deletions.
21 changes: 20 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,25 @@ jobs:
timeout-minutes: 8
run: poetry run make ${{ matrix.sanity }}

# Tox is used to execute linters required for Event-Driven Ansible (EDA) code:
# github.com/ansible/eda-partner-testing/blob/main/README.md
# Tox should only execute over <root>/extensions/eda/plugins.
# Tox utilises the tox.ini file found in the local directory.
# This action is taken from Ansible Partner Engineering's example:
# github.com/ansible/eda-partner-testing/blob/main/.github/workflows/tox.yml
# Tox is planned by Ansible Partner Engineering to cover other code in future.
tox:
name: Tox Checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install deps
run: python -m pip install tox
- name: Move to tox conf file and run tox
run: |
cd .github/workflows
python -m tox -- ../..
format:
name: Code Format Check
runs-on: ubuntu-latest
Expand Down Expand Up @@ -105,7 +124,7 @@ jobs:
release:
name: release
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [sanity]
needs: [sanity, tox]
runs-on: ubuntu-latest

steps:
Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Recommended usage of this file is detailed in https://github.com/ansible/eda-partner-testing/blob/main/README.md.
# The linter paths can be changed, but may result in false passes.
# {posargs} in this case would be the path to collection root relative from the .github/workflows dir (`../..`)

[tox]
envlist = ruff, darglint, pylint-event-source, pylint-event-filter
requires =
ruff
darglint
pylint

[testenv:ruff]
deps = ruff
commands = ruff check --select ALL --ignore INP001 -q {posargs}/extensions/eda/plugins


[testenv:darglint]
deps = darglint
commands = darglint -s numpy -z full {posargs}/extensions/eda/plugins


# If you dont have any event_source or event_filter plugins, remove the corresponding testenv
[testenv:pylint-event-source]
deps = pylint
commands = pylint {posargs}/extensions/eda/plugins/event_source/*.py --output-format=parseable -sn --disable R0801 --disable E0401

; [testenv:pylint-event-filter]
; deps = pylint
; commands = pylint {posargs}/extensions/eda/plugins/event_filter/*.py --output-format=parseable -sn --disable R0801
145 changes: 97 additions & 48 deletions extensions/eda/plugins/event_source/logs.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
"""An ansible-rulebook event source module.
An ansible-rulebook event source module for receiving events via a webhook from
PAN-OS firewall or Panorama appliance.
Arguments:
---------
host: The webserver hostname to listen to. Set to 0.0.0.0 to listen on all
interfaces. Defaults to 127.0.0.1
port: The TCP port to listen to. Defaults to 5000
Example:
-------
- paloaltonetworks.panos.logs:
host: 0.0.0.0
port: 5000
type: decryption
"""

# Copyright 2023 Palo Alto Networks, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -12,61 +32,57 @@
# See the License for the specific language governing permissions and
# limitations under the License.

# ruff: noqa: UP001, UP010
from __future__ import absolute_import, division, print_function

# pylint: disable-next=invalid-name
__metaclass__ = type

import asyncio
import logging
from typing import Any, Dict
from json import JSONDecodeError
from typing import Any

from aiohttp import web
from dpath import util


DOCUMENTATION = """
logs.py
An ansible-rulebook event source module for receiving events via a webhook from
PAN-OS firewall or Panorama appliance.
Arguments:
host: The webserver hostname to listen to. Set to 0.0.0.0 to listen on all
interfaces. Defaults to 127.0.0.1
port: The TCP port to listen to. Defaults to 5000
Example:
- paloaltonetworks.panos.logs:
host: 0.0.0.0
port: 5000
type: decryption
"""

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

routes = web.RouteTableDef()


@routes.get("/")
async def status(request: web.Request):
"""Return a simple status response."""
async def status() -> web.Response:
"""Return a simple status response.
Returns
-------
A web.Response object with status 200 and the text "up" returned by the function.
"""
return web.Response(status=200, text="up")


@routes.post("/{endpoint}")
async def webhook(request: web.Request):
"""
Handle webhook requests.
async def webhook(request: web.Request) -> web.Response:
"""Handle webhook requests.
Process the incoming JSON payload and forward it to the event queue
if it matches the configured log type.
Parameters
----------
request
The incoming webhook request.
Returns
-------
A web.Response object with status 200 and the status.
"""
try:
payload = await request.json()
except Exception as e:
logger.error(f"Failed to parse JSON payload: {e}")
except JSONDecodeError:
logger.exception("Failed to parse JSON payload")
return web.Response(status=400, text="Invalid JSON payload")

if request.app["type"] == "decryption":
Expand All @@ -78,18 +94,31 @@ async def webhook(request: web.Request):
await request.app["queue"].put(data)

return web.Response(
status=202, text=str({"status": "received", "payload": "happy"})
status=202,
text=str({"status": "received", "payload": "happy"}),
)


def process_payload(request, payload, log_type):
"""
Process the payload and extract the necessary information.
def process_payload(
request: web.Request,
payload: dict[str, Any],
log_type: str,
) -> dict[str, Any]:
"""Process the payload and extract the necessary information.
Parameters
----------
request
The incoming webhook request.
payload
The JSON payload from the request.
log_type : str
The log type to filter events.
Returns
-------
A dictionary containing the processed payload and metadata.
:param request: The incoming webhook request.
:param payload: The JSON payload from the request.
:param log_type: The log type to filter events.
:return: A dictionary containing the processed payload and metadata.
"""
try:
device_name = util.get(payload, "details.device_name", separator=".")
Expand All @@ -114,16 +143,21 @@ def process_payload(request, payload, log_type):
return data


async def main(queue: asyncio.Queue, args: Dict[str, Any], logger=None):
"""
Main function to run the plugin as a standalone application.
async def main(queue: asyncio.Queue, args: dict[str, Any], custom_logger: None) -> None:
"""Run the plugin as a standalone application.
Parameters
----------
queue
The event queue to forward incoming events to.
args
A dictionary containing configuration arguments.
custom_logger
An optional custom logger.
:param queue: The event queue to forward incoming events to.
:param args: A dictionary containing configuration arguments.
:param logger: An optional custom logger.
"""
if logger is None:
logger = logging.getLogger(__name__)
if custom_logger is None:
custom_logger = logging.getLogger(__name__)

app = web.Application()
app["queue"] = queue
Expand All @@ -143,15 +177,30 @@ async def main(queue: asyncio.Queue, args: Dict[str, Any], logger=None):
try:
await asyncio.Future()
except asyncio.CancelledError:
logger.info("Plugin Task Cancelled")
custom_logger.info("Plugin Task Cancelled")
finally:
await runner.cleanup()


if __name__ == "__main__":

class MockQueue:
async def put(self, event):
print(event)
"""A mock queue for handling events asynchronously."""

async def put(self: "MockQueue", event: str) -> None:
"""Put an event into the queue.
Parameters
----------
event: str
The event to be added to the queue.
"""
the_logger.info(event)

async def get(self: "MockQueue") -> None:
"""Get an event from the queue."""
the_logger.info("Getting event from the queue.")

asyncio.run(main(MockQueue(), {}))
the_logger = logging.getLogger()
asyncio.run(main(MockQueue(), {}, the_logger))
2 changes: 1 addition & 1 deletion tests/sanity/ignore-2.13.txt
Original file line number Diff line number Diff line change
Expand Up @@ -536,4 +536,4 @@ plugins/modules/panos_zone_facts.py import-3.5
plugins/httpapi/panos.py import-3.5
plugins/modules/panos_dag.py no-get-exception
plugins/modules/panos_dag_tags.py no-get-exception
plugins/modules/panos_sag.py no-get-exception
plugins/modules/panos_sag.py no-get-exception

0 comments on commit 9a50c9b

Please sign in to comment.