Skip to content
This repository has been archived by the owner on Oct 27, 2023. It is now read-only.

Commit

Permalink
Fix tests and add CI pipeline
Browse files Browse the repository at this point in the history
  • Loading branch information
schlomo committed Oct 26, 2023
1 parent ed1d2b4 commit 603a759
Show file tree
Hide file tree
Showing 26 changed files with 968 additions and 834 deletions.
34 changes: 34 additions & 0 deletions .github/actions/setup-and-run-tests/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Setup Python and run Test
description: Build and run tests

inputs:
python-version:
description: "Python version to use"
required: true
default: "3.12"

runs:
using: composite
steps:
- name: Setup Python ${{ inputs.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python-version }}

- name: Install poetry
shell: bash
run: |
set -e -x -o pipefail
pip install poetry
- name: Install dependencies
shell: bash
run: |
set -e -x -o pipefail
poetry install
- name: Test
shell: bash
run: |
poetry run black *.py --check --verbose --diff
poetry run pytest
53 changes: 53 additions & 0 deletions .github/workflows/test-and-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Validate and Publish

on:
push:
branches:
- main

concurrency:
group: ${{ github.workflow }}

jobs:
build-branch:
name: Build, branch, publish
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup and run tests
uses: ./.github/actions/setup-and-run-tests

- name: Get next version
uses: reecetech/[email protected]
id: version
with:
scheme: calver

- name: Set version
shell: bash
run: |
set -e -x -o pipefail
poetry version ${{ steps.version.outputs.version }}
- name: Create release branch
uses: EndBug/[email protected]
with:
default_author: github_actor
new_branch: release/${{ steps.version.outputs.version }}
push: true

- name: Build release
shell: bash
run: |
set -e -x -o pipefail
poetry build --format wheel
- name: Upload release as artifact
uses: actions/upload-artifact@v2
with:
name: wheel
path: dist/*.whl
19 changes: 19 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Validation

on:
pull_request:
branches:
- main
workflow_call:

jobs:
validate:
name: Validate
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Validate
uses: ./.github/actions/setup-and-run-tests

1 change: 1 addition & 0 deletions connectivity_check/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from __future__ import annotations
from box import Box


class ConnectivityChecks(object):
"""
Run various network and HTTP connectivity checks
Expand Down
8 changes: 5 additions & 3 deletions connectivity_check/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ def cli():
MissingSchema,
SSLError,
TimeoutError,
FileNotFoundError
FileNotFoundError,
) as e:
raise SystemExit("ERROR: " + str(e))
except Exception as e:
raise SystemExit(
f"BUG ERROR (unexpected {e.__class__} exception):\n" + " \n".join(traceback.format_exception(e)))
f"BUG ERROR (unexpected {e.__class__} exception):\n"
+ " \n".join(traceback.format_exception(e))
)


if __name__ == "__main__":
cli()
cli() # pragma: no cover
12 changes: 6 additions & 6 deletions connectivity_check/check_cert.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from .exceptions import ConnectivityCheckException


class CertDetails:
def __init__(self, cert: x509.Certificate) -> None:
self.issuer = cert.issuer.rfc4514_string()
Expand Down Expand Up @@ -79,19 +80,18 @@ def check_cert(self, target: str, issuer: str = None, validity: int = 5, timeout
cert_details = CertDetails(cert)
if issuer:
issuer = str(issuer)
check_result_issuer = re.search(
issuer, cert_details.issuer, re.IGNORECASE) is not None
check_result_issuer = (
re.search(issuer, cert_details.issuer, re.IGNORECASE) is not None
)
check_result_validity = cert_details.validity >= validity
check_result = check_result_issuer and check_result_validity
self._datadog(target=host, check="cert", values=check_result)
if check_result:
return (
f"Certificate from {host} matches issuer »{issuer}« ({cert_details.issuer}) and expiration in more than {validity} ({cert_details.validity}) days"
)
return f"Certificate from {host} matches issuer »{issuer}« ({cert_details.issuer}) and expiration in more than {validity} ({cert_details.validity}) days"
else:
raise ConnectivityCheckException(
f"Certificate from {host} fails issuer match »{issuer}« or expires before {validity} days\n"
+ str(cert_details)
)
else:
return (f"Fetched X.509 certificate from {host}:{port}\n" + str(cert_details))
return f"Fetched X.509 certificate from {host}:{port}\n" + str(cert_details)
34 changes: 18 additions & 16 deletions connectivity_check/check_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@

from .exceptions import ConnectivityCheckException

def check_content(self, target: str, content: str = None, ca: str = None, timeout=10) -> str:

def check_content(
self, target: str, content: str = None, ca: str = None, timeout=10
) -> str:
"""
Check the content of remote location
Expand All @@ -32,19 +35,18 @@ def check_content(self, target: str, content: str = None, ca: str = None, timeou
certificates directory to lookup certificates by hash.
"""
response = requests.get(target, timeout=timeout, verify=ca if ca else True)
if response is not None:
request_dump = dump.dump_all(response).decode(response.encoding)
if content:
content = str(content)
check_result = re.search(
content, response.text, re.IGNORECASE) is not None
self._datadog(target=target, check="content", values=check_result)
if check_result:
return (f"Content from {target} matches »{content}«")
else:
raise ConnectivityCheckException(
f"Content from {target} fails content match »{content}«\n\n"
+ response.text
)

request_dump = dump.dump_all(response).decode(response.encoding)
if content:
content = str(content)
check_result = re.search(content, response.text, re.IGNORECASE) is not None
self._datadog(target=target, check="content", values=check_result)
if check_result:
return f"Content from {target} matches »{content}«"
else:
return (request_dump)
raise ConnectivityCheckException(
f"Content from {target} fails content match »{content}«\n\n"
+ response.text
)
else:
return request_dump
28 changes: 18 additions & 10 deletions connectivity_check/check_latency.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@
from urllib.parse import urlparse


def check_latency(self, target: str, latency: int, timeout: float = 5, runs: int = 3, wait: float = 1, verbose: bool = False) -> str:
def check_latency(
self,
target: str,
latency: int,
timeout: float = 5,
runs: int = 3,
wait: float = 1,
verbose: bool = False,
) -> str:
"""
Check latency based on TCP connections
Expand All @@ -40,24 +48,24 @@ def check_latency(self, target: str, latency: int, timeout: float = 5, runs: int

display_dest = f"{host}:{port}"

measures = tcp_latency.measure_latency(
host, port, timeout, runs, wait, verbose)
measures = tcp_latency.measure_latency(host, port, timeout, runs, wait, verbose)

if len(measures) > 0:
average = int(tcp_latency.mean(measures))
minimum = int(min(measures))
maximum = int(max(measures))

check_result = average <= latency
self._datadog(target=target, check="latency",
values={"average": average,
"minimum": minimum,
"maximum": maximum})
self._datadog(
target=target,
check="latency",
values={"average": average, "minimum": minimum, "maximum": maximum},
)
if check_result:
return f"TCP connection latency to {display_dest} is {average} (limit {latency})"
else:
raise ConnectivityCheckException(
f"TCP connection latency to {display_dest} is {average} exceeding the limit of {latency}")
f"TCP connection latency to {display_dest} is {average} exceeding the limit of {latency}"
)
else:
raise ConnectivityCheckException(
f"TCP connection to {display_dest} failed")
raise ConnectivityCheckException(f"TCP connection to {display_dest} failed")
23 changes: 16 additions & 7 deletions connectivity_check/check_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Route:
"Determine and IPv4 route to a destination"

def __init__(self, dest: str) -> Route:
if re.fullmatch('[\d.]+', dest):
if re.fullmatch("[\d.]+", dest):
# got IP
self.ip = dest
self.destination = dest
Expand All @@ -36,18 +36,21 @@ def __init__(self, dest: str) -> Route:
self.ip = gethostbyname(dest)
except gaierror as e:
raise ConnectivityCheckException(
f"Could not convert {dest} to IP address: {e}")
f"Could not convert {dest} to IP address: {e}"
)
self.destination = f"{dest} ({self.ip})"

if platform.system() == "Linux":
# Scapy doesn't support policy routing, see https://github.com/secdev/scapy/issues/836
# we therefore use Linux-only pyroute2 here
from pyroute2 import IPRoute

ipr = IPRoute()

route_attrs = dict(ipr.route("get", dst=self.ip)[0]["attrs"])
assert route_attrs[
"RTA_DST"] == self.ip, f"Route lookup destination mismatch {self.ip} != {route_attrs['RTA_DST']}"
assert (
route_attrs["RTA_DST"] == self.ip
), f"Route lookup destination mismatch {self.ip} != {route_attrs['RTA_DST']}"
self.local_ip = route_attrs["RTA_PREFSRC"]
self.gateway_ip = route_attrs.get("RTA_GATEWAY", "direct")
device_id = route_attrs["RTA_OIF"]
Expand All @@ -56,6 +59,7 @@ def __init__(self, dest: str) -> Route:
# use scapy on all non-Linux OS
# lazy-load scapy as it does a lot upon initialisation and can also print warnings if not disabled via logging
import logging

logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
from scapy.all import conf as scapy_conf

Expand All @@ -68,7 +72,11 @@ def compare(self, other: Route, same: bool = True) -> bool:
return self == other if same else self != other

def __eq__(self, other: Route) -> bool:
return self.device == other.device and self.local_ip == other.local_ip and self.gateway_ip == other.gateway_ip
return (
self.device == other.device
and self.local_ip == other.local_ip
and self.gateway_ip == other.gateway_ip
)

def __ne__(self, other: Route) -> bool:
return not self == other
Expand Down Expand Up @@ -97,8 +105,9 @@ def check_routing(self, target: str, reference: str, same: bool = False):
reference_route = Route(reference)

check_result = reference_route.compare(target_route, same)
self._datadog(target=target, check="routing",
values=check_result, device=target_route.device)
self._datadog(
target=target, check="routing", values=check_result, device=target_route.device
)
if check_result:
return f"Routing to {target} via {target_route.gateway_ip}/{target_route.device} {'is same as' if same else 'differs from'} route to {reference}"
else:
Expand Down
40 changes: 27 additions & 13 deletions connectivity_check/check_speed_cloudflare.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
from .exceptions import ConnectivityCheckException


def check_speed_cloudflare(self, latency: int = 50, download: int = 20, upload: int = 5, progress: bool = False):
def check_speed_cloudflare(
self, latency: int = 50, download: int = 20, upload: int = 5, progress: bool = False
):
"""
Check the network speed via Cloudflare speed.cloudflare.com
Expand All @@ -28,21 +30,33 @@ def check_speed_cloudflare(self, latency: int = 50, download: int = 20, upload:
results = cloudflareclass.cloudflare(printit=progress).runalltests()
except Exception as e:
raise ConnectivityCheckException(str(e))

# import json, pathlib ; pathlib.Path("./out.json").write_text(json.dumps(results, indent=2))

results = {key: compound["value"] for key, compound in results.items()}
result_download = results['90th_percentile_download_speed']
result_upload = results['90th_percentile_upload_speed']
result_latency = results['latency_ms']
result_server_description = "speed.cloudflare.com in {test_location_city} ({test_location_region})".format(
**results)
check_result = result_download >= download and result_upload >= upload and result_latency <= latency
self._datadog(target="speed.cloudflare.com", check="speed_cloudflare",
values={"download": result_download,
"upload": result_upload,
"latency": result_latency},
host=result_server_description)
result_download = results["90th_percentile_download_speed"]
result_upload = results["90th_percentile_upload_speed"]
result_latency = results["latency_ms"]
result_server_description = (
"speed.cloudflare.com in {test_location_city} ({test_location_region})".format(
**results
)
)
check_result = (
result_download >= download
and result_upload >= upload
and result_latency <= latency
)
self._datadog(
target="speed.cloudflare.com",
check="speed_cloudflare",
values={
"download": result_download,
"upload": result_upload,
"latency": result_latency,
},
host=result_server_description,
)
if check_result:
return f"Speedtest to {result_server_description} D:{result_download} U:{result_upload} Mb/s at {result_latency} ms"
else:
Expand Down
Loading

0 comments on commit 603a759

Please sign in to comment.