diff --git a/PKG-INFO b/PKG-INFO
index 2024fea..75dba8a 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,25 +1,25 @@
Metadata-Version: 2.1
Name: swh.icinga_plugins
-Version: 0.2.3
+Version: 0.3.0
Summary: Icinga plugins for Software Heritage infrastructure monitoring
Home-page: https://forge.softwareheritage.org/diffusion/swh-icinga-plugins
Author: Software Heritage developers
Author-email: swh-devel@inria.fr
License: UNKNOWN
Project-URL: Bug Reports, https://forge.softwareheritage.org/maniphest
Project-URL: Funding, https://www.softwareheritage.org/donate
Project-URL: Source, https://forge.softwareheritage.org/source/swh-icinga-plugins
Description: swh-icinga-plugins
==================
Scripts for end-to-end monitoring of the SWH infrastructure
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Operating System :: OS Independent
Classifier: Development Status :: 3 - Alpha
Requires-Python: >=3.7
Description-Content-Type: text/markdown
Provides-Extra: testing
diff --git a/swh.icinga_plugins.egg-info/PKG-INFO b/swh.icinga_plugins.egg-info/PKG-INFO
index 86d19a9..c5f4061 100644
--- a/swh.icinga_plugins.egg-info/PKG-INFO
+++ b/swh.icinga_plugins.egg-info/PKG-INFO
@@ -1,25 +1,25 @@
Metadata-Version: 2.1
Name: swh.icinga-plugins
-Version: 0.2.3
+Version: 0.3.0
Summary: Icinga plugins for Software Heritage infrastructure monitoring
Home-page: https://forge.softwareheritage.org/diffusion/swh-icinga-plugins
Author: Software Heritage developers
Author-email: swh-devel@inria.fr
License: UNKNOWN
Project-URL: Bug Reports, https://forge.softwareheritage.org/maniphest
Project-URL: Funding, https://www.softwareheritage.org/donate
Project-URL: Source, https://forge.softwareheritage.org/source/swh-icinga-plugins
Description: swh-icinga-plugins
==================
Scripts for end-to-end monitoring of the SWH infrastructure
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Operating System :: OS Independent
Classifier: Development Status :: 3 - Alpha
Requires-Python: >=3.7
Description-Content-Type: text/markdown
Provides-Extra: testing
diff --git a/swh.icinga_plugins.egg-info/SOURCES.txt b/swh.icinga_plugins.egg-info/SOURCES.txt
index bb58e0c..e7829f9 100644
--- a/swh.icinga_plugins.egg-info/SOURCES.txt
+++ b/swh.icinga_plugins.egg-info/SOURCES.txt
@@ -1,44 +1,47 @@
.gitignore
.pre-commit-config.yaml
AUTHORS
CODE_OF_CONDUCT.md
CONTRIBUTORS
LICENSE
MANIFEST.in
Makefile
README.md
mypy.ini
pyproject.toml
pytest.ini
requirements-swh.txt
requirements-test.txt
requirements.txt
setup.cfg
setup.py
tox.ini
data/deposit/jesuisgpl.tgz
data/deposit/jesuisgpl.tgz.xml
docs/.gitignore
docs/Makefile
docs/conf.py
docs/index.rst
docs/_static/.placeholder
docs/_templates/.placeholder
swh/__init__.py
swh.icinga_plugins.egg-info/PKG-INFO
swh.icinga_plugins.egg-info/SOURCES.txt
swh.icinga_plugins.egg-info/dependency_links.txt
swh.icinga_plugins.egg-info/entry_points.txt
swh.icinga_plugins.egg-info/requires.txt
swh.icinga_plugins.egg-info/top_level.txt
swh/icinga_plugins/__init__.py
swh/icinga_plugins/base_check.py
swh/icinga_plugins/cli.py
swh/icinga_plugins/deposit.py
swh/icinga_plugins/py.typed
+swh/icinga_plugins/save_code_now.py
swh/icinga_plugins/vault.py
swh/icinga_plugins/tests/__init__.py
swh/icinga_plugins/tests/conftest.py
swh/icinga_plugins/tests/test_deposit.py
+swh/icinga_plugins/tests/test_save_code_now.py
swh/icinga_plugins/tests/test_vault.py
+swh/icinga_plugins/tests/utils.py
swh/icinga_plugins/tests/web_scenario.py
\ No newline at end of file
diff --git a/swh/icinga_plugins/cli.py b/swh/icinga_plugins/cli.py
index 46017c3..a98906f 100644
--- a/swh/icinga_plugins/cli.py
+++ b/swh/icinga_plugins/cli.py
@@ -1,104 +1,135 @@
# Copyright (C) 2019-2020 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
# WARNING: do not import unnecessary things here to keep cli startup time under
# control
import sys
import click
from swh.core.cli import CONTEXT_SETTINGS
from swh.core.cli import swh as swh_cli_group
@swh_cli_group.group(name="icinga_plugins", context_settings=CONTEXT_SETTINGS)
@click.option("-w", "--warning", type=int, help="Warning threshold.")
@click.option("-c", "--critical", type=int, help="Critical threshold.")
@click.pass_context
def icinga_cli_group(ctx, warning, critical):
"""Main command for Icinga plugins
"""
ctx.ensure_object(dict)
if warning:
ctx.obj["warning_threshold"] = int(warning)
if critical:
ctx.obj["critical_threshold"] = int(critical)
@icinga_cli_group.group(name="check-vault")
@click.option(
"--swh-storage-url", type=str, required=True, help="URL to an swh-storage HTTP API"
)
@click.option(
"--swh-web-url", type=str, required=True, help="URL to an swh-web instance"
)
@click.option(
"--poll-interval",
type=int,
default=10,
help="Interval (in seconds) between two polls to the API, "
"to check for cooking status.",
)
@click.pass_context
def check_vault(ctx, **kwargs):
ctx.obj.update(kwargs)
@check_vault.command(name="directory")
@click.pass_context
def check_vault_directory(ctx):
"""Picks a random directory, requests its cooking via swh-web,
and waits for completion."""
from .vault import VaultCheck
sys.exit(VaultCheck(ctx.obj).main())
+@icinga_cli_group.group(name="check-savecodenow")
+@click.option(
+ "--swh-web-url", type=str, required=True, help="URL to an swh-web instance"
+)
+@click.option(
+ "--poll-interval",
+ type=int,
+ default=10,
+ help="Interval (in seconds) between two polls to the API, "
+ "to check for save code now status.",
+)
+@click.pass_context
+def check_scn(ctx, **kwargs):
+ ctx.obj.update(kwargs)
+
+
+@check_scn.command(name="origin")
+@click.argument("origin", type=str)
+@click.option("--visit-type", type=str, required=True, help="Visit type for origin")
+@click.pass_context
+def check_scn_origin(ctx, origin, visit_type):
+ """Requests a save code now via the api for a given origin with type visit_type, waits
+ for its completion, report approximate time of completion (failed or succeeded) and
+ warn if threshold exceeded.
+
+ """
+ from .save_code_now import SaveCodeNowCheck
+
+ sys.exit(SaveCodeNowCheck(ctx.obj, origin, visit_type).main())
+
+
@icinga_cli_group.group(name="check-deposit")
@click.option(
"--server",
type=str,
default="https://deposit.softwareheritage.org/1",
help="URL to the SWORD server to test",
)
@click.option("--username", type=str, required=True, help="Login for the SWORD server")
@click.option(
"--password", type=str, required=True, help="Password for the SWORD server"
)
@click.option(
"--collection",
type=str,
required=True,
help="Software collection to use on the SWORD server",
)
@click.option(
"--poll-interval",
type=int,
default=10,
help="Interval (in seconds) between two polls to the API, "
"to check for ingestion status.",
)
@click.pass_context
def check_deposit(ctx, **kwargs):
ctx.obj.update(kwargs)
@check_deposit.command(name="single")
@click.option(
"--archive", type=click.Path(), required=True, help="Software artefact to upload"
)
@click.option(
"--metadata",
type=click.Path(),
required=True,
help="Metadata file for the software artefact.",
)
@click.pass_context
def check_deposit_single(ctx, **kwargs):
"""Checks the provided archive and metadata file and be deposited."""
from .deposit import DepositCheck
ctx.obj.update(kwargs)
sys.exit(DepositCheck(ctx.obj).main())
diff --git a/swh/icinga_plugins/save_code_now.py b/swh/icinga_plugins/save_code_now.py
new file mode 100644
index 0000000..02ed4fa
--- /dev/null
+++ b/swh/icinga_plugins/save_code_now.py
@@ -0,0 +1,113 @@
+# Copyright (C) 2021 The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+import time
+from typing import Dict, List
+
+import requests
+
+from .base_check import BaseCheck
+
+REPORT_MSG = "Save code now request for origin"
+
+WAITING_STATUSES = ("not yet scheduled", "running", "scheduled")
+
+
+class SaveCodeNowCheck(BaseCheck):
+ TYPE = "SAVECODENOW"
+ DEFAULT_WARNING_THRESHOLD = 60
+ DEFAULT_CRITICAL_THRESHOLD = 120
+
+ def __init__(self, obj: Dict, origin: str, visit_type: str) -> None:
+ super().__init__(obj)
+ self.api_url = obj["swh_web_url"].rstrip("/")
+ self.poll_interval = obj["poll_interval"]
+ self.origin = origin
+ self.visit_type = visit_type
+
+ @staticmethod
+ def api_url_scn(root_api_url: str, origin: str, visit_type: str) -> str:
+ """Compute the save code now api url for a given origin"""
+ return f"{root_api_url}/api/1/origin/save/{visit_type}/url/{origin}/"
+
+ def main(self) -> int:
+ """Scenario description:
+
+ 1. Requests a save code now request via the api for origin self.origin with type
+ self.visit_type.
+
+ 2. Polling regularly at self.poll_interval seconds the completion status.
+
+ 3. When either succeeded, failed or threshold exceeded, report approximate time
+ of completion. This will warn if thresholds are exceeded.
+
+ """
+ start_time: float = time.time()
+ total_time: float = 0.0
+ scn_url = self.api_url_scn(self.api_url, self.origin, self.visit_type)
+ response = requests.post(scn_url)
+ assert response.status_code == 200, (response, response.text)
+
+ result: Dict = response.json()
+
+ status_key = "save_task_status"
+ request_date = result["save_request_date"]
+ origin_info = (self.visit_type, self.origin)
+
+ while result[status_key] in WAITING_STATUSES:
+ time.sleep(self.poll_interval)
+ response = requests.get(scn_url)
+ assert (
+ response.status_code == 200
+ ), "Unexpected response: {response}, {response.text}"
+ raw_result: List[Dict] = response.json()
+ assert len(raw_result) > 0, f"Unexpected result: {raw_result}"
+
+ if len(raw_result) > 1:
+ # retrieve only the one status result we are interested in
+ result = next(
+ filter(lambda r: r["save_request_date"] == request_date, raw_result)
+ )
+ else:
+ result = raw_result[0]
+
+ # this because the api can return multiple entries for the same origin
+ assert result["save_request_date"] == request_date
+
+ total_time = time.time() - start_time
+
+ if total_time > self.critical_threshold:
+ self.print_result(
+ "CRITICAL",
+ f"{REPORT_MSG} {origin_info} took more than {total_time:.2f}s "
+ f'and has status: {result["save_task_status"]}.',
+ total_time=total_time,
+ )
+ return 2
+
+ if result[status_key] == "succeeded":
+ (status_code, status) = self.get_status(total_time)
+ self.print_result(
+ status,
+ f"{REPORT_MSG} {origin_info} took {total_time:.2f}s and succeeded.",
+ total_time=total_time,
+ )
+ return status_code
+ elif result[status_key] == "failed":
+ self.print_result(
+ "CRITICAL",
+ f"{REPORT_MSG} {origin_info} took {total_time:.2f}s and failed.",
+ total_time=total_time,
+ )
+ return 2
+ else:
+ self.print_result(
+ "CRITICAL",
+ f"{REPORT_MSG} {origin_info} took {total_time:.2f}s "
+ "and resulted in unsupported status: "
+ f"{result['save_request_status']} ; {result[status_key]}.",
+ total_time=total_time,
+ )
+ return 2
diff --git a/swh/icinga_plugins/tests/test_deposit.py b/swh/icinga_plugins/tests/test_deposit.py
index cc8e946..a3bed1a 100644
--- a/swh/icinga_plugins/tests/test_deposit.py
+++ b/swh/icinga_plugins/tests/test_deposit.py
@@ -1,628 +1,618 @@
-# Copyright (C) 2019-2020 The Software Heritage developers
+# Copyright (C) 2019-2021 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
import io
import os
import tarfile
import time
from typing import Optional
-from click.testing import CliRunner
import pytest
-from swh.icinga_plugins.cli import icinga_cli_group
+from swh.icinga_plugins.tests.utils import invoke
from .web_scenario import WebScenario
BASE_URL = "http://swh-deposit.example.org/1"
COMMON_OPTIONS = [
"--server",
BASE_URL,
"--username",
"test",
"--password",
"test",
"--collection",
"testcol",
]
SAMPLE_METADATA = """
Test Software
swh
test-software
No One
"""
ENTRY_TEMPLATE = """
42
2019-12-19 18:11:00
foo.tar.gz
{status}
http://purl.org/net/sword/package/SimpleZip
"""
STATUS_TEMPLATE = """
42
{status}
{status_detail}%s
"""
def status_template(
status: str, status_detail: str = "", swhid: Optional[str] = None
) -> str:
"""Generate a proper status template out of status, status_detail and optional swhid
"""
if swhid is not None:
template = (
STATUS_TEMPLATE % f"\n {swhid}"
)
return template.format(status=status, status_detail=status_detail, swhid=swhid)
template = STATUS_TEMPLATE % ""
return template.format(status=status, status_detail=status_detail)
def test_status_template():
actual_status = status_template(status="deposited")
assert (
actual_status
== """
42
deposited
"""
)
actual_status = status_template(status="verified", status_detail="detail")
assert (
actual_status
== """
42
verified
detail
"""
)
actual_status = status_template(status="done", swhid="10")
assert (
actual_status
== """
42
done
10
"""
)
@pytest.fixture(scope="session")
def tmp_path(tmp_path_factory):
return tmp_path_factory.mktemp(__name__)
@pytest.fixture(scope="session")
def sample_metadata(tmp_path):
"""Returns a sample metadata file's path
"""
path = os.path.join(tmp_path, "metadata.xml")
with open(path, "w") as fd:
fd.write(SAMPLE_METADATA)
return path
@pytest.fixture(scope="session")
def sample_archive(tmp_path):
"""Returns a sample archive's path
"""
path = os.path.join(tmp_path, "archive.tar.gz")
with tarfile.open(path, "w:gz") as tf:
tf.addfile(tarfile.TarInfo("hello.py"), io.BytesIO(b'print("Hello world")'))
return path
-def invoke(args, catch_exceptions=False):
- runner = CliRunner()
- result = runner.invoke(icinga_cli_group, args)
- if not catch_exceptions and result.exception:
- print(result.output)
- raise result.exception
- return result
-
-
def test_deposit_immediate_success(
requests_mock, mocker, sample_archive, sample_metadata, mocked_time
):
"""Both deposit creation and deposit metadata update passed without delays
"""
scenario = WebScenario()
status_xml = status_template(
status="done",
status_detail="",
swhid="swh:1:dir:02ed6084fb0e8384ac58980e07548a547431cf74",
)
# Initial deposit
scenario.add_step(
"post", f"{BASE_URL}/testcol/", ENTRY_TEMPLATE.format(status="done")
)
# Then metadata update
status_xml = status_template(
status="done",
status_detail="",
swhid="swh:1:dir:02ed6084fb0e8384ac58980e07548a547431cf74",
)
scenario.add_step("get", f"{BASE_URL}/testcol/42/status/", status_xml)
# internal deposit client does call status, then update metadata then status api
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_xml,
)
scenario.add_step(
"put", f"{BASE_URL}/testcol/42/atom/", status_xml,
)
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_xml,
)
scenario.install_mock(requests_mock)
result = invoke(
[
"check-deposit",
*COMMON_OPTIONS,
"single",
"--archive",
sample_archive,
"--metadata",
sample_metadata,
]
)
assert result.output == (
"DEPOSIT OK - Deposit took 0.00s and succeeded.\n"
"| 'load_time' = 0.00s\n"
"| 'total_time' = 0.00s\n"
"| 'upload_time' = 0.00s\n"
"| 'validation_time' = 0.00s\n"
"DEPOSIT OK - Deposit Metadata update took 0.00s and succeeded.\n"
"| 'total_time' = 0.00s\n"
"| 'update_time' = 0.00s\n"
)
assert result.exit_code == 0, f"Unexpected output: {result.output}"
def test_deposit_delays(
requests_mock, mocker, sample_archive, sample_metadata, mocked_time
):
"""Deposit creation passed with some delays, deposit metadata update passed without
delay
"""
scenario = WebScenario()
scenario.add_step(
"post", f"{BASE_URL}/testcol/", ENTRY_TEMPLATE.format(status="deposited")
)
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_template(status="verified"),
)
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_template(status="loading"),
)
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_template(status="done"),
)
# Then metadata update
status_xml = status_template(
status="done",
status_detail="",
swhid="swh:1:dir:02ed6084fb0e8384ac58980e07548a547431cf74",
)
scenario.add_step("get", f"{BASE_URL}/testcol/42/status/", status_xml)
# internal deposit client does call status, then update metadata then status api
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_xml,
)
scenario.add_step(
"put", f"{BASE_URL}/testcol/42/atom/", status_xml,
)
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_xml,
)
scenario.install_mock(requests_mock)
result = invoke(
[
"check-deposit",
*COMMON_OPTIONS,
"single",
"--archive",
sample_archive,
"--metadata",
sample_metadata,
]
)
assert result.output == (
"DEPOSIT OK - Deposit took 30.00s and succeeded.\n"
"| 'load_time' = 20.00s\n"
"| 'total_time' = 30.00s\n"
"| 'upload_time' = 0.00s\n"
"| 'validation_time' = 10.00s\n"
"DEPOSIT OK - Deposit Metadata update took 0.00s and succeeded.\n"
"| 'total_time' = 30.00s\n"
"| 'update_time' = 0.00s\n"
)
assert result.exit_code == 0, f"Unexpected output: {result.output}"
def test_deposit_then_metadata_update_failed(
requests_mock, mocker, sample_archive, sample_metadata, mocked_time
):
"""Deposit creation passed, deposit metadata update failed
"""
scenario = WebScenario()
scenario.add_step(
"post", f"{BASE_URL}/testcol/", ENTRY_TEMPLATE.format(status="deposited")
)
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_template(status="verified"),
)
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_template(status="loading"),
)
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_template(status="done"),
)
# Then metadata update calls
failed_status_xml = status_template(
status="failed", # lying here
status_detail="Failure to ingest",
swhid="swh:1:dir:02ed6084fb0e8384ac58980e07548a547431cf74",
)
scenario.add_step("get", f"{BASE_URL}/testcol/42/status/", failed_status_xml)
scenario.add_step("get", f"{BASE_URL}/testcol/42/status/", failed_status_xml)
scenario.install_mock(requests_mock)
result = invoke(
[
"check-deposit",
*COMMON_OPTIONS,
"single",
"--archive",
sample_archive,
"--metadata",
sample_metadata,
],
catch_exceptions=True,
)
assert result.output == (
"DEPOSIT OK - Deposit took 30.00s and succeeded.\n"
"| 'load_time' = 20.00s\n"
"| 'total_time' = 30.00s\n"
"| 'upload_time' = 0.00s\n"
"| 'validation_time' = 10.00s\n"
"DEPOSIT CRITICAL - Deposit Metadata update failed: You can only update "
"metadata on deposit with status 'done' \n"
"| 'total_time' = 30.00s\n"
"| 'update_time' = 0.00s\n"
)
assert result.exit_code == 2, f"Unexpected output: {result.output}"
def test_deposit_delay_warning(
requests_mock, mocker, sample_archive, sample_metadata, mocked_time
):
"""Deposit creation exceeded delays, no deposit update occurred.
"""
scenario = WebScenario()
scenario.add_step(
"post", f"{BASE_URL}/testcol/", ENTRY_TEMPLATE.format(status="deposited")
)
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_template(status="verified"),
)
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_template(status="done"),
)
scenario.install_mock(requests_mock)
result = invoke(
[
"--warning",
"15",
"check-deposit",
*COMMON_OPTIONS,
"single",
"--archive",
sample_archive,
"--metadata",
sample_metadata,
],
catch_exceptions=True,
)
assert result.output == (
"DEPOSIT WARNING - Deposit took 20.00s and succeeded.\n"
"| 'load_time' = 10.00s\n"
"| 'total_time' = 20.00s\n"
"| 'upload_time' = 0.00s\n"
"| 'validation_time' = 10.00s\n"
)
assert result.exit_code == 1, f"Unexpected output: {result.output}"
def test_deposit_delay_critical(
requests_mock, mocker, sample_archive, sample_metadata, mocked_time
):
scenario = WebScenario()
scenario.add_step(
"post", f"{BASE_URL}/testcol/", ENTRY_TEMPLATE.format(status="deposited")
)
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_template(status="verified"),
)
scenario.add_step(
"get",
f"{BASE_URL}/testcol/42/status/",
status_template(status="done"),
callback=lambda: time.sleep(60),
)
scenario.install_mock(requests_mock)
result = invoke(
[
"--critical",
"50",
"check-deposit",
*COMMON_OPTIONS,
"single",
"--archive",
sample_archive,
"--metadata",
sample_metadata,
],
catch_exceptions=True,
)
assert result.output == (
"DEPOSIT CRITICAL - Deposit took 80.00s and succeeded.\n"
"| 'load_time' = 70.00s\n"
"| 'total_time' = 80.00s\n"
"| 'upload_time' = 0.00s\n"
"| 'validation_time' = 10.00s\n"
)
assert result.exit_code == 2, f"Unexpected output: {result.output}"
def test_deposit_timeout(
requests_mock, mocker, sample_archive, sample_metadata, mocked_time
):
scenario = WebScenario()
scenario.add_step(
"post",
f"{BASE_URL}/testcol/",
ENTRY_TEMPLATE.format(status="deposited"),
callback=lambda: time.sleep(1500),
)
scenario.add_step(
"get",
f"{BASE_URL}/testcol/42/status/",
status_template(status="verified"),
callback=lambda: time.sleep(1500),
)
scenario.add_step(
"get",
f"{BASE_URL}/testcol/42/status/",
status_template(status="loading"),
callback=lambda: time.sleep(1500),
)
scenario.install_mock(requests_mock)
result = invoke(
[
"check-deposit",
*COMMON_OPTIONS,
"single",
"--archive",
sample_archive,
"--metadata",
sample_metadata,
],
catch_exceptions=True,
)
assert result.output == (
"DEPOSIT CRITICAL - Timed out while in status loading "
"(4520.0s seconds since deposit started)\n"
"| 'total_time' = 4520.00s\n"
"| 'upload_time' = 1500.00s\n"
"| 'validation_time' = 1510.00s\n"
)
assert result.exit_code == 2, f"Unexpected output: {result.output}"
def test_deposit_rejected(
requests_mock, mocker, sample_archive, sample_metadata, mocked_time
):
scenario = WebScenario()
scenario.add_step(
"post", f"{BASE_URL}/testcol/", ENTRY_TEMPLATE.format(status="deposited")
)
scenario.add_step(
"get",
f"{BASE_URL}/testcol/42/status/",
status_template(status="rejected", status_detail="booo"),
)
scenario.install_mock(requests_mock)
result = invoke(
[
"check-deposit",
*COMMON_OPTIONS,
"single",
"--archive",
sample_archive,
"--metadata",
sample_metadata,
],
catch_exceptions=True,
)
assert result.output == (
"DEPOSIT CRITICAL - Deposit was rejected: booo\n"
"| 'total_time' = 10.00s\n"
"| 'upload_time' = 0.00s\n"
"| 'validation_time' = 10.00s\n"
)
assert result.exit_code == 2, f"Unexpected output: {result.output}"
def test_deposit_failed(
requests_mock, mocker, sample_archive, sample_metadata, mocked_time
):
scenario = WebScenario()
scenario.add_step(
"post", f"{BASE_URL}/testcol/", ENTRY_TEMPLATE.format(status="deposited")
)
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_template(status="verified"),
)
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_template(status="loading"),
)
scenario.add_step(
"get",
f"{BASE_URL}/testcol/42/status/",
status_template(status="failed", status_detail="booo"),
)
scenario.install_mock(requests_mock)
result = invoke(
[
"check-deposit",
*COMMON_OPTIONS,
"single",
"--archive",
sample_archive,
"--metadata",
sample_metadata,
],
catch_exceptions=True,
)
assert result.output == (
"DEPOSIT CRITICAL - Deposit loading failed: booo\n"
"| 'load_time' = 20.00s\n"
"| 'total_time' = 30.00s\n"
"| 'upload_time' = 0.00s\n"
"| 'validation_time' = 10.00s\n"
)
assert result.exit_code == 2, f"Unexpected output: {result.output}"
def test_deposit_unexpected_status(
requests_mock, mocker, sample_archive, sample_metadata, mocked_time
):
scenario = WebScenario()
scenario.add_step(
"post", f"{BASE_URL}/testcol/", ENTRY_TEMPLATE.format(status="deposited")
)
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_template(status="verified"),
)
scenario.add_step(
"get", f"{BASE_URL}/testcol/42/status/", status_template(status="loading"),
)
scenario.add_step(
"get",
f"{BASE_URL}/testcol/42/status/",
status_template(status="what", status_detail="booo"),
)
scenario.install_mock(requests_mock)
result = invoke(
[
"check-deposit",
*COMMON_OPTIONS,
"single",
"--archive",
sample_archive,
"--metadata",
sample_metadata,
],
catch_exceptions=True,
)
assert result.output == (
"DEPOSIT CRITICAL - Deposit got unexpected status: what (booo)\n"
"| 'load_time' = 20.00s\n"
"| 'total_time' = 30.00s\n"
"| 'upload_time' = 0.00s\n"
"| 'validation_time' = 10.00s\n"
)
assert result.exit_code == 2, f"Unexpected output: {result.output}"
diff --git a/swh/icinga_plugins/tests/test_save_code_now.py b/swh/icinga_plugins/tests/test_save_code_now.py
new file mode 100644
index 0000000..bd5e95a
--- /dev/null
+++ b/swh/icinga_plugins/tests/test_save_code_now.py
@@ -0,0 +1,258 @@
+# Copyright (C) 2021 The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+from datetime import datetime, timezone
+import random
+from typing import Dict, Optional, Tuple
+
+import pytest
+
+from swh.icinga_plugins.save_code_now import (
+ REPORT_MSG,
+ WAITING_STATUSES,
+ SaveCodeNowCheck,
+)
+
+from .utils import invoke
+from .web_scenario import WebScenario
+
+
+def fake_response(
+ origin: str,
+ visit_type: str,
+ sor_status: str = "pending",
+ task_status: Optional[str] = None,
+) -> Dict:
+ """Fake a save code now request api response"""
+ visit_date = None
+ if task_status in ("failed", "succeeded"):
+ visit_date = str(datetime.now(tz=timezone.utc))
+
+ return {
+ "visit_type": visit_type,
+ "origin_url": origin,
+ "save_request_date": "to-replace",
+ "save_request_status": sor_status,
+ "save_task_status": task_status,
+ "visit_date": visit_date,
+ }
+
+
+@pytest.fixture
+def origin_info() -> Tuple[str, str]:
+ """Build an origin info to request save code now
+
+ """
+ origin_name = random.choice(range(10))
+ return random.choice(["git", "svn", "hg"]), f"mock://fake-origin-url/{origin_name}"
+
+
+def test_save_code_now_success(requests_mock, mocker, mocked_time, origin_info):
+ """Successful ingestion scenario below threshold"""
+ scenario = WebScenario()
+ visit_type, origin = origin_info
+
+ root_api_url = "mock://swh-web.example.org"
+ api_url = SaveCodeNowCheck.api_url_scn(root_api_url, origin, visit_type)
+
+ # creation request
+ scenario.add_step(
+ "post",
+ api_url,
+ fake_response(origin, visit_type, "accepted", "not yet scheduled"),
+ )
+ response_scheduled = fake_response(origin, visit_type, "accepted", "scheduled")
+ # status polling requests
+ scenario.add_step("get", api_url, [response_scheduled])
+ # sometimes we can have multiple response so we fake that here
+ scenario.add_step("get", api_url, [response_scheduled, response_scheduled])
+ scenario.add_step(
+ "get", api_url, [fake_response(origin, visit_type, "accepted", "succeeded")]
+ )
+ scenario.install_mock(requests_mock)
+
+ # fmt: off
+ result = invoke(
+ [
+ "check-savecodenow", "--swh-web-url", root_api_url,
+ "origin", origin,
+ "--visit-type", visit_type,
+ ]
+ )
+ # fmt: on
+
+ assert result.output == (
+ f"{SaveCodeNowCheck.TYPE} OK - {REPORT_MSG} {origin_info} took "
+ f"30.00s and succeeded.\n"
+ f"| 'total_time' = 30.00s\n"
+ )
+ assert result.exit_code == 0, f"Unexpected result: {result.output}"
+
+
+def test_save_code_now_failure(requests_mock, mocker, mocked_time, origin_info):
+ """Failed ingestion scenario should be reported"""
+ scenario = WebScenario()
+ visit_type, origin = origin_info
+
+ root_api_url = "mock://swh-web.example.org"
+ api_url = SaveCodeNowCheck.api_url_scn(root_api_url, origin, visit_type)
+
+ # creation request
+ scenario.add_step(
+ "post",
+ api_url,
+ fake_response(origin, visit_type, "accepted", "not yet scheduled"),
+ )
+ # status polling requests
+ scenario.add_step(
+ "get", api_url, [fake_response(origin, visit_type, "accepted", "scheduled")]
+ )
+ scenario.add_step(
+ "get", api_url, [fake_response(origin, visit_type, "accepted", "failed")]
+ )
+ scenario.install_mock(requests_mock)
+
+ # fmt: off
+ result = invoke(
+ [
+ "check-savecodenow", "--swh-web-url", root_api_url,
+ "origin", origin,
+ "--visit-type", visit_type,
+ ],
+ catch_exceptions=True,
+ )
+ # fmt: on
+
+ assert result.output == (
+ f"{SaveCodeNowCheck.TYPE} CRITICAL - {REPORT_MSG} {origin_info} took "
+ f"20.00s and failed.\n"
+ f"| 'total_time' = 20.00s\n"
+ )
+ assert result.exit_code == 2, f"Unexpected result: {result.output}"
+
+
+def test_save_code_now_pending_state_unsupported(
+ requests_mock, mocker, mocked_time, origin_info
+):
+ """Pending save requests are not supported in the test so they should fail early
+
+ Pending requests are requests that need a moderator to accept the repository into
+ the save code now flow.
+
+ Do not actually use such origin to trigger the checks.
+
+ """
+ scenario = WebScenario()
+ visit_type, origin = origin_info
+ root_api_url = "mock://swh-web2.example.org"
+ api_url = SaveCodeNowCheck.api_url_scn(root_api_url, origin, visit_type)
+
+ # creation request
+ scenario.add_step(
+ "post", api_url, fake_response(origin, visit_type, "pending", "not created"),
+ )
+ scenario.install_mock(requests_mock)
+
+ # fmt: off
+ result = invoke(
+ [
+ "check-savecodenow", "--swh-web-url", root_api_url,
+ "origin", origin,
+ "--visit-type", visit_type,
+ ],
+ catch_exceptions=True,
+ )
+ # fmt: on
+
+ assert result.output == (
+ f"{SaveCodeNowCheck.TYPE} CRITICAL - {REPORT_MSG} {origin_info} took "
+ f"0.00s and resulted in unsupported status: pending ; not created.\n"
+ f"| 'total_time' = 0.00s\n"
+ )
+ assert result.exit_code == 2, f"Unexpected output: {result.output}"
+
+
+def test_save_code_now_threshold_exceeded(
+ requests_mock, mocker, mocked_time, origin_info
+):
+ """Saving requests exceeding threshold should mention warning in output
+
+ """
+ scenario = WebScenario()
+ visit_type, origin = origin_info
+
+ root_api_url = "mock://swh-web2.example.org"
+ api_url = SaveCodeNowCheck.api_url_scn(root_api_url, origin, visit_type)
+
+ # creation request
+ scenario.add_step(
+ "post",
+ api_url,
+ fake_response(origin, visit_type, "accepted", "not yet scheduled"),
+ )
+
+ # we'll make the response being in the awaiting status
+ # beyond 13, this will exceed the threshold
+ for i in range(13):
+ waiting_status = random.choice(WAITING_STATUSES)
+ response_scheduled = fake_response(
+ origin, visit_type, "accepted", waiting_status
+ )
+ scenario.add_step("get", api_url, [response_scheduled])
+ scenario.install_mock(requests_mock)
+
+ # fmt: off
+ result = invoke(
+ [
+ "check-savecodenow", "--swh-web-url", root_api_url,
+ "origin", origin,
+ "--visit-type", visit_type,
+ ],
+ catch_exceptions=True,
+ )
+ # fmt: on
+
+ assert result.output == (
+ f"{SaveCodeNowCheck.TYPE} CRITICAL - {REPORT_MSG} {origin_info} took "
+ f"more than 130.00s and has status: {waiting_status}.\n"
+ f"| 'total_time' = 130.00s\n"
+ )
+ assert result.exit_code == 2, f"Unexpected output: {result.output}"
+
+
+def test_save_code_now_unexpected_failure(
+ requests_mock, mocker, mocked_time, origin_info
+):
+ """Unexpected failure if the webapi refuses to answer for example"""
+ scenario = WebScenario()
+ visit_type, origin = origin_info
+
+ root_api_url = "mock://swh-web.example.org"
+ api_url = SaveCodeNowCheck.api_url_scn(root_api_url, origin, visit_type)
+
+ # creation request
+ scenario.add_step(
+ "post",
+ api_url,
+ fake_response(origin, visit_type, "accepted", "not yet scheduled"),
+ )
+ # status polling requests
+ scenario.add_step(
+ "get", api_url, [fake_response(origin, visit_type, "accepted", "scheduled")]
+ )
+ # unexpected issue when communicating with the api
+ scenario.add_step("get", api_url, {}, status_code=500)
+ scenario.install_mock(requests_mock)
+
+ with pytest.raises(AssertionError):
+ # fmt: off
+ invoke(
+ [
+ "check-savecodenow", "--swh-web-url", root_api_url,
+ "origin", origin,
+ "--visit-type", visit_type,
+ ],
+ )
+ # fmt: on
diff --git a/swh/icinga_plugins/tests/test_vault.py b/swh/icinga_plugins/tests/test_vault.py
index 787612a..bf578b8 100644
--- a/swh/icinga_plugins/tests/test_vault.py
+++ b/swh/icinga_plugins/tests/test_vault.py
@@ -1,291 +1,280 @@
-# Copyright (C) 2019 The Software Heritage developers
+# Copyright (C) 2019-2021 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
import time
-from click.testing import CliRunner
-
-from swh.icinga_plugins.cli import icinga_cli_group
+from swh.icinga_plugins.tests.utils import invoke
from .web_scenario import WebScenario
dir_id = "ab" * 20
response_pending = {
"obj_id": dir_id,
"obj_type": "directory",
"progress_message": "foo",
"status": "pending",
}
response_done = {
"fetch_url": f"/api/1/vault/directory/{dir_id}/raw/",
"id": 9,
"obj_id": dir_id,
"obj_type": "directory",
"status": "done",
}
response_failed = {
"obj_id": dir_id,
"obj_type": "directory",
"progress_message": "foobar",
"status": "failed",
}
response_unknown_status = {
"obj_id": dir_id,
"obj_type": "directory",
"progress_message": "what",
"status": "boo",
}
class FakeStorage:
def __init__(self, foo, **kwargs):
pass
def directory_get_random(self):
return bytes.fromhex(dir_id)
-def invoke(args, catch_exceptions=False):
- runner = CliRunner()
- result = runner.invoke(icinga_cli_group, args)
- if not catch_exceptions and result.exception:
- print(result.output)
- raise result.exception
- return result
-
-
def test_vault_immediate_success(requests_mock, mocker, mocked_time):
scenario = WebScenario()
url = f"mock://swh-web.example.org/api/1/vault/directory/{dir_id}/"
scenario.add_step("get", url, {}, status_code=404)
scenario.add_step("post", url, response_pending)
scenario.add_step("get", url, response_done)
scenario.install_mock(requests_mock)
get_storage_mock = mocker.patch("swh.icinga_plugins.vault.get_storage")
get_storage_mock.side_effect = FakeStorage
result = invoke(
[
"check-vault",
"--swh-web-url",
"mock://swh-web.example.org",
"--swh-storage-url",
"foo://example.org",
"directory",
]
)
assert result.output == (
f"VAULT OK - cooking directory {dir_id} took "
f"10.00s and succeeded.\n"
f"| 'total_time' = 10.00s\n"
)
assert result.exit_code == 0, result.output
def test_vault_delayed_success(requests_mock, mocker, mocked_time):
scenario = WebScenario()
url = f"mock://swh-web.example.org/api/1/vault/directory/{dir_id}/"
scenario.add_step("get", url, {}, status_code=404)
scenario.add_step("post", url, response_pending)
scenario.add_step("get", url, response_pending)
scenario.add_step("get", url, response_done)
scenario.install_mock(requests_mock)
get_storage_mock = mocker.patch("swh.icinga_plugins.vault.get_storage")
get_storage_mock.side_effect = FakeStorage
result = invoke(
[
"check-vault",
"--swh-web-url",
"mock://swh-web.example.org",
"--swh-storage-url",
"foo://example.org",
"directory",
]
)
assert result.output == (
f"VAULT OK - cooking directory {dir_id} took "
f"20.00s and succeeded.\n"
f"| 'total_time' = 20.00s\n"
)
assert result.exit_code == 0, result.output
def test_vault_failure(requests_mock, mocker, mocked_time):
scenario = WebScenario()
url = f"mock://swh-web.example.org/api/1/vault/directory/{dir_id}/"
scenario.add_step("get", url, {}, status_code=404)
scenario.add_step("post", url, response_pending)
scenario.add_step("get", url, response_failed)
scenario.install_mock(requests_mock)
get_storage_mock = mocker.patch("swh.icinga_plugins.vault.get_storage")
get_storage_mock.side_effect = FakeStorage
result = invoke(
[
"check-vault",
"--swh-web-url",
"mock://swh-web.example.org",
"--swh-storage-url",
"foo://example.org",
"directory",
],
catch_exceptions=True,
)
assert result.output == (
f"VAULT CRITICAL - cooking directory {dir_id} took "
f"10.00s and failed with: foobar\n"
f"| 'total_time' = 10.00s\n"
)
assert result.exit_code == 2, result.output
def test_vault_unknown_status(requests_mock, mocker, mocked_time):
scenario = WebScenario()
url = f"mock://swh-web.example.org/api/1/vault/directory/{dir_id}/"
scenario.add_step("get", url, {}, status_code=404)
scenario.add_step("post", url, response_pending)
scenario.add_step("get", url, response_unknown_status)
scenario.install_mock(requests_mock)
get_storage_mock = mocker.patch("swh.icinga_plugins.vault.get_storage")
get_storage_mock.side_effect = FakeStorage
result = invoke(
[
"check-vault",
"--swh-web-url",
"mock://swh-web.example.org",
"--swh-storage-url",
"foo://example.org",
"directory",
],
catch_exceptions=True,
)
assert result.output == (
f"VAULT CRITICAL - cooking directory {dir_id} took "
f"10.00s and resulted in unknown status: boo\n"
f"| 'total_time' = 10.00s\n"
)
assert result.exit_code == 2, result.output
def test_vault_timeout(requests_mock, mocker, mocked_time):
scenario = WebScenario()
url = f"mock://swh-web.example.org/api/1/vault/directory/{dir_id}/"
scenario.add_step("get", url, {}, status_code=404)
scenario.add_step("post", url, response_pending)
scenario.add_step("get", url, response_pending)
scenario.add_step("get", url, response_pending, callback=lambda: time.sleep(4000))
scenario.install_mock(requests_mock)
get_storage_mock = mocker.patch("swh.icinga_plugins.vault.get_storage")
get_storage_mock.side_effect = FakeStorage
result = invoke(
[
"check-vault",
"--swh-web-url",
"mock://swh-web.example.org",
"--swh-storage-url",
"foo://example.org",
"directory",
],
catch_exceptions=True,
)
assert result.output == (
f"VAULT CRITICAL - cooking directory {dir_id} took more than "
f"4020.00s and has status: foo\n"
f"| 'total_time' = 4020.00s\n"
)
assert result.exit_code == 2, result.output
def test_vault_cached_directory(requests_mock, mocker, mocked_time):
"""First serves a directory that's already in the cache, to
test that vault_check requests another one."""
scenario = WebScenario()
url = f"mock://swh-web.example.org/api/1/vault/directory/{dir_id}/"
scenario.add_step("get", url, {}, status_code=200)
scenario.add_step("get", url, {}, status_code=404)
scenario.add_step("post", url, response_pending)
scenario.add_step("get", url, response_done)
scenario.install_mock(requests_mock)
get_storage_mock = mocker.patch("swh.icinga_plugins.vault.get_storage")
get_storage_mock.side_effect = FakeStorage
result = invoke(
[
"check-vault",
"--swh-web-url",
"mock://swh-web.example.org",
"--swh-storage-url",
"foo://example.org",
"directory",
]
)
assert result.output == (
f"VAULT OK - cooking directory {dir_id} took "
f"10.00s and succeeded.\n"
f"| 'total_time' = 10.00s\n"
)
assert result.exit_code == 0, result.output
def test_vault_no_directory(requests_mock, mocker, mocked_time):
"""Tests with an empty storage"""
scenario = WebScenario()
scenario.install_mock(requests_mock)
get_storage_mock = mocker.patch("swh.icinga_plugins.vault.get_storage")
get_storage_mock.side_effect = FakeStorage
mocker.patch(f"{__name__}.FakeStorage.directory_get_random", return_value=None)
result = invoke(
[
"check-vault",
"--swh-web-url",
"mock://swh-web.example.org",
"--swh-storage-url",
"foo://example.org",
"directory",
],
catch_exceptions=True,
)
assert result.output == ("VAULT CRITICAL - No directory exists in the archive.\n")
assert result.exit_code == 2, result.output
diff --git a/swh/icinga_plugins/tests/utils.py b/swh/icinga_plugins/tests/utils.py
new file mode 100644
index 0000000..cf6b9af
--- /dev/null
+++ b/swh/icinga_plugins/tests/utils.py
@@ -0,0 +1,20 @@
+# Copyright (C) 2019-2021 The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+from typing import List
+
+from click.testing import CliRunner, Result
+
+from swh.icinga_plugins.cli import icinga_cli_group
+
+
+def invoke(args: List[str], catch_exceptions: bool = False) -> Result:
+ """Invoke icinga plugin main cli command with args"""
+ runner = CliRunner()
+ result = runner.invoke(icinga_cli_group, args)
+ if not catch_exceptions and result.exception:
+ print(result.output)
+ raise result.exception
+ return result