runspec-linux 0.4.2__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of runspec-linux might be problematic. Click here for more details.
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/CHANGELOG.md +43 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/PKG-INFO +2 -1
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/pyproject.toml +11 -1
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/runspec_linux/runspec.toml +241 -0
- runspec_linux-0.5.0/runspec_linux/webops.py +148 -0
- runspec_linux-0.5.0/tests/test_webops.py +107 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/.gitignore +0 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/runspec_linux/__init__.py +0 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/runspec_linux/containers.py +0 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/runspec_linux/files.py +0 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/runspec_linux/logs.py +0 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/runspec_linux/nc_command.py +0 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/runspec_linux/network.py +0 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/runspec_linux/packages.py +0 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/runspec_linux/power.py +0 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/runspec_linux/security.py +0 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/runspec_linux/services.py +0 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/runspec_linux/sudoers.py +0 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/runspec_linux/system_info.py +0 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/runspec_linux/venvs.py +0 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/tests/__init__.py +0 -0
- {runspec_linux-0.4.2 → runspec_linux-0.5.0}/tests/test_nc_send.py +0 -0
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# runspec-linux Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0] — 2026-06-22
|
|
4
|
+
|
|
5
|
+
Add 8 cross-platform **web / TLS / API** runnables — thin wrappers over the new
|
|
6
|
+
`runspec-webops-core` (a new dependency). All are read-only network probes
|
|
7
|
+
(`autonomy = "autonomous"`, `output = "json"`) and use only the Python stdlib,
|
|
8
|
+
so the same runnables ship in `runspec-windows` too. They are a Python port of
|
|
9
|
+
the `runspec-webops` Node package (plus four extras), so a fleet that can't run
|
|
10
|
+
Node still gets them.
|
|
11
|
+
|
|
12
|
+
- **`cert-check`** — inspect a host's TLS certificate (days-to-expiry, issuer,
|
|
13
|
+
SANs, fingerprint, trust). Exits `1` when expiring/expired, `2` when
|
|
14
|
+
unreachable. Args: `--port`/`-p` (443), `--warn-days`/`-w` (30),
|
|
15
|
+
`--timeout`/`-t` (10).
|
|
16
|
+
- **`cert-chain`** — print the full certificate chain (leaf → intermediates →
|
|
17
|
+
root), flagging the self-signed root.
|
|
18
|
+
- **`tls-info`** — report the negotiated TLS protocol, cipher, and trust result.
|
|
19
|
+
- **`http-check`** — probe an HTTP(S) URL (status, response time, redirect
|
|
20
|
+
target); `--expect-status`/`-e` exits non-zero on a mismatch; `--insecure`/`-k`
|
|
21
|
+
skips TLS verification.
|
|
22
|
+
- **`http-headers`** — fetch response headers and flag missing security headers
|
|
23
|
+
(HSTS, CSP, X-Frame-Options, …); `--method`/`-m` (HEAD/GET).
|
|
24
|
+
- **`redirect-trace`** — follow the redirect chain hop by hop with loop
|
|
25
|
+
detection; `--max-hops`/`-n` (10).
|
|
26
|
+
- **`dns-lookup`** — resolve a hostname (A/AAAA + canonical name) or
|
|
27
|
+
reverse-resolve an IP; `--reverse`/`-r` also PTRs each resolved address.
|
|
28
|
+
- **`api-spec`** — read an OpenAPI/Swagger spec (`source` = an http(s) URL or a
|
|
29
|
+
local file) and condense its shape to bounded JSON, so an agent can write or
|
|
30
|
+
update the runnables that call an internal API (and re-check when it drifts).
|
|
31
|
+
Default is an overview (info, servers, every operation), narrowable with
|
|
32
|
+
`--tag` / `--match`; `--operation`/`-o` (or `--path` + `--method`/`-X`) drills
|
|
33
|
+
into one operation's params, request body and responses with `$ref`s resolved.
|
|
34
|
+
OpenAPI 3.x and Swagger 2.0; JSON needs no extra, YAML needs
|
|
35
|
+
`runspec-webops-core[yaml]`.
|
|
36
|
+
|
|
37
|
+
## [0.4.3] — 2026-06-21
|
|
38
|
+
|
|
39
|
+
- **`install-into-venv --venv` and `configure-pip --venv` are now marked
|
|
40
|
+
`ui = "directory"`.** Both point at an existing venv *folder*, so the console's
|
|
41
|
+
path picker opens a folder picker ("Use this folder") instead of a file picker
|
|
42
|
+
— it was previously unclear the arg wanted the venv directory, not the
|
|
43
|
+
`bin/python` inside it. (`install-into-venv --project` stays a generic `path`
|
|
44
|
+
since it accepts either a project directory or a wheel/sdist file.)
|
|
45
|
+
|
|
3
46
|
## [0.4.2] — 2026-06-21
|
|
4
47
|
|
|
5
48
|
Add 3 runnables for managing Python virtualenvs on a fleet host — thin wrappers
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runspec-linux
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Linux system admin runnables for runspec
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: runspec-linux-core>=0.3.2
|
|
7
|
+
Requires-Dist: runspec-webops-core>=0.1.0
|
|
7
8
|
Requires-Dist: runspec>=0.27.0
|
|
8
9
|
Provides-Extra: dev
|
|
9
10
|
Requires-Dist: mypy; extra == 'dev'
|
|
@@ -4,12 +4,13 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "runspec-linux"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
9
|
description = "Linux system admin runnables for runspec"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"runspec>=0.27.0",
|
|
12
12
|
"runspec-linux-core>=0.3.2",
|
|
13
|
+
"runspec-webops-core>=0.1.0",
|
|
13
14
|
]
|
|
14
15
|
|
|
15
16
|
[project.scripts]
|
|
@@ -44,6 +45,15 @@ create-venv = "runspec_linux.venvs:main_create_venv"
|
|
|
44
45
|
install-into-venv = "runspec_linux.venvs:main_install_into_venv"
|
|
45
46
|
configure-pip = "runspec_linux.venvs:main_configure_pip"
|
|
46
47
|
nc-command = "runspec_linux.nc_command:main"
|
|
48
|
+
# Web / TLS probes (runspec-webops-core; cross-platform)
|
|
49
|
+
cert-check = "runspec_linux.webops:main_cert_check"
|
|
50
|
+
cert-chain = "runspec_linux.webops:main_cert_chain"
|
|
51
|
+
tls-info = "runspec_linux.webops:main_tls_info"
|
|
52
|
+
http-check = "runspec_linux.webops:main_http_check"
|
|
53
|
+
http-headers = "runspec_linux.webops:main_http_headers"
|
|
54
|
+
redirect-trace = "runspec_linux.webops:main_redirect_trace"
|
|
55
|
+
dns-lookup = "runspec_linux.webops:main_dns_lookup"
|
|
56
|
+
api-spec = "runspec_linux.webops:main_api_spec"
|
|
47
57
|
|
|
48
58
|
[project.optional-dependencies]
|
|
49
59
|
dev = [
|
|
@@ -508,6 +508,7 @@ output = "json"
|
|
|
508
508
|
|
|
509
509
|
[install-into-venv.args.venv]
|
|
510
510
|
type = "path"
|
|
511
|
+
ui = "directory"
|
|
511
512
|
short = "-v"
|
|
512
513
|
description = "Path to the venv to install into (its bundled python runs pip)"
|
|
513
514
|
|
|
@@ -611,6 +612,7 @@ description = "Where to write pip.conf: user (~/.config/pip/pip.conf) or venv (<
|
|
|
611
612
|
|
|
612
613
|
[configure-pip.args.venv]
|
|
613
614
|
type = "path"
|
|
615
|
+
ui = "directory"
|
|
614
616
|
description = "Venv path for --scope venv (the file is written to <venv>/pip.conf)"
|
|
615
617
|
required = false
|
|
616
618
|
|
|
@@ -625,3 +627,242 @@ type = "choice"
|
|
|
625
627
|
options = ["sudo", "su"]
|
|
626
628
|
default = "sudo"
|
|
627
629
|
description = "How to escalate to run-as: sudo (passwordless, -n) or su"
|
|
630
|
+
|
|
631
|
+
# ── Web / TLS probes (runspec-webops-core; cross-platform, read-only) ──────────
|
|
632
|
+
|
|
633
|
+
[cert-check]
|
|
634
|
+
serve = ["local"]
|
|
635
|
+
description = "Inspect a host's TLS certificate — days-to-expiry, issuer, SANs, fingerprint, trust"
|
|
636
|
+
autonomy = "autonomous"
|
|
637
|
+
output = "json"
|
|
638
|
+
|
|
639
|
+
[cert-check.args.host]
|
|
640
|
+
type = "str"
|
|
641
|
+
position = 1
|
|
642
|
+
description = "Hostname to connect to"
|
|
643
|
+
|
|
644
|
+
[cert-check.args.port]
|
|
645
|
+
type = "int"
|
|
646
|
+
short = "-p"
|
|
647
|
+
description = "TLS port"
|
|
648
|
+
default = 443
|
|
649
|
+
|
|
650
|
+
[cert-check.args.warn-days]
|
|
651
|
+
type = "int"
|
|
652
|
+
short = "-w"
|
|
653
|
+
description = "Report 'expiring' when fewer than N days remain"
|
|
654
|
+
default = 30
|
|
655
|
+
|
|
656
|
+
[cert-check.args.timeout]
|
|
657
|
+
type = "int"
|
|
658
|
+
short = "-t"
|
|
659
|
+
description = "Connection timeout in seconds"
|
|
660
|
+
default = 10
|
|
661
|
+
|
|
662
|
+
[cert-chain]
|
|
663
|
+
serve = ["local"]
|
|
664
|
+
description = "Show the full TLS certificate chain (leaf -> intermediates -> root)"
|
|
665
|
+
autonomy = "autonomous"
|
|
666
|
+
output = "json"
|
|
667
|
+
|
|
668
|
+
[cert-chain.args.host]
|
|
669
|
+
type = "str"
|
|
670
|
+
position = 1
|
|
671
|
+
description = "Hostname to connect to"
|
|
672
|
+
|
|
673
|
+
[cert-chain.args.port]
|
|
674
|
+
type = "int"
|
|
675
|
+
short = "-p"
|
|
676
|
+
description = "TLS port"
|
|
677
|
+
default = 443
|
|
678
|
+
|
|
679
|
+
[cert-chain.args.timeout]
|
|
680
|
+
type = "int"
|
|
681
|
+
short = "-t"
|
|
682
|
+
description = "Connection timeout in seconds"
|
|
683
|
+
default = 10
|
|
684
|
+
|
|
685
|
+
[tls-info]
|
|
686
|
+
serve = ["local"]
|
|
687
|
+
description = "Report the negotiated TLS protocol, cipher, and trust result for a host"
|
|
688
|
+
autonomy = "autonomous"
|
|
689
|
+
output = "json"
|
|
690
|
+
|
|
691
|
+
[tls-info.args.host]
|
|
692
|
+
type = "str"
|
|
693
|
+
position = 1
|
|
694
|
+
description = "Hostname to connect to"
|
|
695
|
+
|
|
696
|
+
[tls-info.args.port]
|
|
697
|
+
type = "int"
|
|
698
|
+
short = "-p"
|
|
699
|
+
description = "TLS port"
|
|
700
|
+
default = 443
|
|
701
|
+
|
|
702
|
+
[tls-info.args.timeout]
|
|
703
|
+
type = "int"
|
|
704
|
+
short = "-t"
|
|
705
|
+
description = "Connection timeout in seconds"
|
|
706
|
+
default = 10
|
|
707
|
+
|
|
708
|
+
[http-check]
|
|
709
|
+
serve = ["local"]
|
|
710
|
+
description = "Probe an HTTP(S) URL — status code, response time, redirect target"
|
|
711
|
+
autonomy = "autonomous"
|
|
712
|
+
output = "json"
|
|
713
|
+
|
|
714
|
+
[http-check.args.url]
|
|
715
|
+
type = "str"
|
|
716
|
+
position = 1
|
|
717
|
+
description = "URL to request"
|
|
718
|
+
|
|
719
|
+
[http-check.args.timeout]
|
|
720
|
+
type = "int"
|
|
721
|
+
short = "-t"
|
|
722
|
+
description = "Request timeout in seconds"
|
|
723
|
+
default = 10
|
|
724
|
+
|
|
725
|
+
[http-check.args.expect-status]
|
|
726
|
+
type = "int"
|
|
727
|
+
short = "-e"
|
|
728
|
+
description = "Exit non-zero unless the response status matches this code"
|
|
729
|
+
required = false
|
|
730
|
+
|
|
731
|
+
[http-check.args.insecure]
|
|
732
|
+
type = "flag"
|
|
733
|
+
short = "-k"
|
|
734
|
+
description = "Skip TLS certificate verification"
|
|
735
|
+
default = false
|
|
736
|
+
|
|
737
|
+
[http-headers]
|
|
738
|
+
serve = ["local"]
|
|
739
|
+
description = "Fetch an endpoint's response headers and flag missing security headers"
|
|
740
|
+
autonomy = "autonomous"
|
|
741
|
+
output = "json"
|
|
742
|
+
|
|
743
|
+
[http-headers.args.url]
|
|
744
|
+
type = "str"
|
|
745
|
+
position = 1
|
|
746
|
+
description = "URL to request"
|
|
747
|
+
|
|
748
|
+
[http-headers.args.method]
|
|
749
|
+
type = "choice"
|
|
750
|
+
options = ["HEAD", "GET"]
|
|
751
|
+
short = "-m"
|
|
752
|
+
description = "HTTP method to use for the header probe"
|
|
753
|
+
default = "HEAD"
|
|
754
|
+
|
|
755
|
+
[http-headers.args.timeout]
|
|
756
|
+
type = "int"
|
|
757
|
+
short = "-t"
|
|
758
|
+
description = "Request timeout in seconds"
|
|
759
|
+
default = 10
|
|
760
|
+
|
|
761
|
+
[http-headers.args.insecure]
|
|
762
|
+
type = "flag"
|
|
763
|
+
short = "-k"
|
|
764
|
+
description = "Skip TLS certificate verification"
|
|
765
|
+
default = false
|
|
766
|
+
|
|
767
|
+
[redirect-trace]
|
|
768
|
+
serve = ["local"]
|
|
769
|
+
description = "Follow an HTTP redirect chain and list each hop with status and Location"
|
|
770
|
+
autonomy = "autonomous"
|
|
771
|
+
output = "json"
|
|
772
|
+
|
|
773
|
+
[redirect-trace.args.url]
|
|
774
|
+
type = "str"
|
|
775
|
+
position = 1
|
|
776
|
+
description = "URL to start from"
|
|
777
|
+
|
|
778
|
+
[redirect-trace.args.timeout]
|
|
779
|
+
type = "int"
|
|
780
|
+
short = "-t"
|
|
781
|
+
description = "Per-request timeout in seconds"
|
|
782
|
+
default = 10
|
|
783
|
+
|
|
784
|
+
[redirect-trace.args.max-hops]
|
|
785
|
+
type = "int"
|
|
786
|
+
short = "-n"
|
|
787
|
+
description = "Maximum number of redirects to follow"
|
|
788
|
+
default = 10
|
|
789
|
+
|
|
790
|
+
[redirect-trace.args.insecure]
|
|
791
|
+
type = "flag"
|
|
792
|
+
short = "-k"
|
|
793
|
+
description = "Skip TLS certificate verification"
|
|
794
|
+
default = false
|
|
795
|
+
|
|
796
|
+
[dns-lookup]
|
|
797
|
+
serve = ["local"]
|
|
798
|
+
description = "Resolve a hostname (A/AAAA + canonical name), or reverse-resolve an IP"
|
|
799
|
+
autonomy = "autonomous"
|
|
800
|
+
output = "json"
|
|
801
|
+
|
|
802
|
+
[dns-lookup.args.host]
|
|
803
|
+
type = "str"
|
|
804
|
+
position = 1
|
|
805
|
+
description = "Hostname to resolve, or an IP address to reverse-resolve"
|
|
806
|
+
|
|
807
|
+
[dns-lookup.args.reverse]
|
|
808
|
+
type = "flag"
|
|
809
|
+
short = "-r"
|
|
810
|
+
description = "Also reverse-resolve (PTR) each resolved address"
|
|
811
|
+
default = false
|
|
812
|
+
|
|
813
|
+
[api-spec]
|
|
814
|
+
serve = ["local"]
|
|
815
|
+
description = "Summarise an OpenAPI/Swagger spec (URL or file) — operations, params, schemas — to help write/update runnables for an API"
|
|
816
|
+
autonomy = "autonomous"
|
|
817
|
+
output = "json"
|
|
818
|
+
|
|
819
|
+
examples = [
|
|
820
|
+
{cmd = "api-spec https://api.internal/openapi.json", description = "Overview: info, servers, and every operation"},
|
|
821
|
+
{cmd = "api-spec https://api.internal/openapi.json --tag orders", description = "Only operations tagged 'orders'"},
|
|
822
|
+
{cmd = "api-spec ./openapi.json --operation createOrder", description = "Full detail for one operation (params, body, responses)"},
|
|
823
|
+
{cmd = "api-spec ./openapi.json --path /orders --method post", description = "Drill into an operation by path + method"},
|
|
824
|
+
]
|
|
825
|
+
|
|
826
|
+
[api-spec.args.source]
|
|
827
|
+
type = "str"
|
|
828
|
+
position = 1
|
|
829
|
+
description = "OpenAPI/Swagger spec — an http(s) URL or a local file path (JSON always; YAML needs the pyyaml extra)"
|
|
830
|
+
|
|
831
|
+
[api-spec.args.operation]
|
|
832
|
+
type = "str"
|
|
833
|
+
short = "-o"
|
|
834
|
+
description = "operationId to describe in full (params, request body, responses)"
|
|
835
|
+
required = false
|
|
836
|
+
|
|
837
|
+
[api-spec.args.path]
|
|
838
|
+
type = "str"
|
|
839
|
+
description = "API path to describe in full (pair with --method); alternative to --operation"
|
|
840
|
+
required = false
|
|
841
|
+
|
|
842
|
+
[api-spec.args.method]
|
|
843
|
+
type = "str"
|
|
844
|
+
short = "-X"
|
|
845
|
+
description = "HTTP method for --path (e.g. get, post)"
|
|
846
|
+
required = false
|
|
847
|
+
|
|
848
|
+
[api-spec.args.tag]
|
|
849
|
+
type = "str"
|
|
850
|
+
description = "Overview only: limit to operations carrying this tag"
|
|
851
|
+
required = false
|
|
852
|
+
|
|
853
|
+
[api-spec.args.match]
|
|
854
|
+
type = "str"
|
|
855
|
+
description = "Overview only: limit to operations whose path contains this substring"
|
|
856
|
+
required = false
|
|
857
|
+
|
|
858
|
+
[api-spec.args.timeout]
|
|
859
|
+
type = "int"
|
|
860
|
+
short = "-t"
|
|
861
|
+
description = "Fetch timeout in seconds (URL sources)"
|
|
862
|
+
default = 15
|
|
863
|
+
|
|
864
|
+
[api-spec.args.insecure]
|
|
865
|
+
type = "flag"
|
|
866
|
+
short = "-k"
|
|
867
|
+
description = "Skip TLS certificate verification when fetching the spec"
|
|
868
|
+
default = false
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Web/TLS runnables — thin cross-platform wrappers over runspec-webops-core.
|
|
2
|
+
|
|
3
|
+
These probe remote HTTP(S)/TLS endpoints and resolve DNS using only the Python
|
|
4
|
+
stdlib (via ``runspec-webops-core``), so they behave identically on Linux and
|
|
5
|
+
Windows. All are read-only network probes (``autonomy = "autonomous"``).
|
|
6
|
+
|
|
7
|
+
Exit codes follow the web-monitor convention so a caller can branch on them:
|
|
8
|
+
``0`` success, ``1`` a reportable problem (expiring/expired cert, unexpected
|
|
9
|
+
status), ``2`` the endpoint was unreachable.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
import runspec as rs
|
|
18
|
+
from runspec_webops_core import (
|
|
19
|
+
ConnectError,
|
|
20
|
+
ResolveError,
|
|
21
|
+
SpecError,
|
|
22
|
+
chain_report,
|
|
23
|
+
connect_peer,
|
|
24
|
+
describe_operation,
|
|
25
|
+
fetch_headers,
|
|
26
|
+
leaf_report,
|
|
27
|
+
load_document,
|
|
28
|
+
probe,
|
|
29
|
+
resolve,
|
|
30
|
+
summarize_spec,
|
|
31
|
+
trace_redirects,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _emit(obj: object) -> None:
|
|
36
|
+
print(json.dumps(obj))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main_cert_check() -> None:
|
|
40
|
+
spec = rs.parse("cert-check")
|
|
41
|
+
host, port = spec.host.value, spec.port.value
|
|
42
|
+
try:
|
|
43
|
+
peer = connect_peer(host, port, spec.timeout.value)
|
|
44
|
+
report = leaf_report(peer, spec.warn_days.value)
|
|
45
|
+
except ConnectError as e:
|
|
46
|
+
_emit({"error": str(e), "host": host, "port": port})
|
|
47
|
+
sys.exit(2)
|
|
48
|
+
_emit({"host": host, "port": port, **report})
|
|
49
|
+
if report["status"] != "ok":
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main_cert_chain() -> None:
|
|
54
|
+
spec = rs.parse("cert-chain")
|
|
55
|
+
host, port = spec.host.value, spec.port.value
|
|
56
|
+
try:
|
|
57
|
+
peer = connect_peer(host, port, spec.timeout.value)
|
|
58
|
+
except ConnectError as e:
|
|
59
|
+
_emit({"error": str(e), "host": host, "port": port})
|
|
60
|
+
sys.exit(2)
|
|
61
|
+
_emit({"host": host, "port": port, "chain": chain_report(peer)})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main_tls_info() -> None:
|
|
65
|
+
spec = rs.parse("tls-info")
|
|
66
|
+
host, port = spec.host.value, spec.port.value
|
|
67
|
+
try:
|
|
68
|
+
peer = connect_peer(host, port, spec.timeout.value)
|
|
69
|
+
except ConnectError as e:
|
|
70
|
+
_emit({"error": str(e), "host": host, "port": port})
|
|
71
|
+
sys.exit(2)
|
|
72
|
+
_emit(
|
|
73
|
+
{
|
|
74
|
+
"host": host,
|
|
75
|
+
"port": port,
|
|
76
|
+
"protocol": peer.protocol,
|
|
77
|
+
"cipher": peer.cipher_name,
|
|
78
|
+
"cipher_version": peer.cipher_version,
|
|
79
|
+
"authorized": peer.authorized,
|
|
80
|
+
"authorization_error": peer.authorization_error,
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main_http_check() -> None:
|
|
86
|
+
spec = rs.parse("http-check")
|
|
87
|
+
url = spec.url.value
|
|
88
|
+
expect = spec.expect_status.value
|
|
89
|
+
try:
|
|
90
|
+
result = probe(url, spec.timeout.value, insecure=spec.insecure.value)
|
|
91
|
+
except ConnectError as e:
|
|
92
|
+
_emit({"error": str(e), "url": url})
|
|
93
|
+
sys.exit(2)
|
|
94
|
+
_emit(result)
|
|
95
|
+
if expect is not None and result["status"] != expect:
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def main_http_headers() -> None:
|
|
100
|
+
spec = rs.parse("http-headers")
|
|
101
|
+
url = spec.url.value
|
|
102
|
+
try:
|
|
103
|
+
result = fetch_headers(url, spec.timeout.value, method=spec.method.value, insecure=spec.insecure.value)
|
|
104
|
+
except ConnectError as e:
|
|
105
|
+
_emit({"error": str(e), "url": url})
|
|
106
|
+
sys.exit(2)
|
|
107
|
+
_emit(result)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main_redirect_trace() -> None:
|
|
111
|
+
spec = rs.parse("redirect-trace")
|
|
112
|
+
url = spec.url.value
|
|
113
|
+
try:
|
|
114
|
+
result = trace_redirects(url, spec.timeout.value, max_hops=spec.max_hops.value, insecure=spec.insecure.value)
|
|
115
|
+
except ConnectError as e:
|
|
116
|
+
_emit({"error": str(e), "url": url})
|
|
117
|
+
sys.exit(2)
|
|
118
|
+
_emit(result)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def main_dns_lookup() -> None:
|
|
122
|
+
spec = rs.parse("dns-lookup")
|
|
123
|
+
host = spec.host.value
|
|
124
|
+
try:
|
|
125
|
+
_emit(resolve(host, want_reverse=spec.reverse.value))
|
|
126
|
+
except ResolveError as e:
|
|
127
|
+
_emit({"error": str(e), "host": host})
|
|
128
|
+
sys.exit(1)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main_api_spec() -> None:
|
|
132
|
+
spec = rs.parse("api-spec")
|
|
133
|
+
source = spec.source.value
|
|
134
|
+
try:
|
|
135
|
+
doc = load_document(source, spec.timeout.value, insecure=spec.insecure.value)
|
|
136
|
+
if spec.operation.value or spec.path.value:
|
|
137
|
+
result: object = describe_operation(
|
|
138
|
+
doc,
|
|
139
|
+
operation_id=spec.operation.value or None,
|
|
140
|
+
path=spec.path.value or None,
|
|
141
|
+
method=spec.method.value or None,
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
result = summarize_spec(doc, tag=spec.tag.value or None, path_filter=spec.match.value or None)
|
|
145
|
+
except SpecError as e:
|
|
146
|
+
_emit({"error": str(e), "source": source})
|
|
147
|
+
sys.exit(2)
|
|
148
|
+
_emit(result)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""The webops runnables parse from runspec.toml and the wrappers emit JSON + exit codes."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import runspec as rs
|
|
8
|
+
|
|
9
|
+
from runspec_linux import webops
|
|
10
|
+
|
|
11
|
+
TOML = Path(__file__).resolve().parent.parent / "runspec_linux" / "runspec.toml"
|
|
12
|
+
|
|
13
|
+
# Each new runnable plus a minimal valid argv (its required positional).
|
|
14
|
+
WEBOPS_RUNNABLES = {
|
|
15
|
+
"cert-check": ["example.com"],
|
|
16
|
+
"cert-chain": ["example.com"],
|
|
17
|
+
"tls-info": ["example.com"],
|
|
18
|
+
"http-check": ["https://example.com"],
|
|
19
|
+
"http-headers": ["https://example.com"],
|
|
20
|
+
"redirect-trace": ["https://example.com"],
|
|
21
|
+
"dns-lookup": ["example.com"],
|
|
22
|
+
"api-spec": ["/tmp/spec.json"],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.parametrize("name", list(WEBOPS_RUNNABLES))
|
|
27
|
+
def test_runnable_is_defined(name: str) -> None:
|
|
28
|
+
spec = rs.parse(name, WEBOPS_RUNNABLES[name], config_path=TOML)
|
|
29
|
+
assert spec.runspec_runnable == name
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_cert_check_defaults_and_positional() -> None:
|
|
33
|
+
spec = rs.parse("cert-check", ["example.com"], config_path=TOML)
|
|
34
|
+
assert spec.host.value == "example.com"
|
|
35
|
+
assert spec.port.value == 443
|
|
36
|
+
assert spec.warn_days.value == 30
|
|
37
|
+
assert spec.timeout.value == 10
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_http_check_flags() -> None:
|
|
41
|
+
spec = rs.parse("http-check", ["https://x.test", "--expect-status", "200", "--insecure"], config_path=TOML)
|
|
42
|
+
assert spec.url.value == "https://x.test"
|
|
43
|
+
assert spec.expect_status.value == 200
|
|
44
|
+
assert spec.insecure.value is True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _fixed_parse(name: str, argv: list[str]):
|
|
48
|
+
"""A real parse bound to a runnable + argv, for patching ``webops.rs.parse``."""
|
|
49
|
+
real = rs.parse
|
|
50
|
+
return lambda *_a, **_k: real(name, argv, config_path=TOML)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_main_cert_check_problem_exits_1(monkeypatch, capsys) -> None:
|
|
54
|
+
monkeypatch.setattr(webops.rs, "parse", _fixed_parse("cert-check", ["expired.test"]))
|
|
55
|
+
monkeypatch.setattr(webops, "connect_peer", lambda *a, **k: object())
|
|
56
|
+
monkeypatch.setattr(webops, "leaf_report", lambda *a, **k: {"status": "expired", "days_remaining": -3})
|
|
57
|
+
with pytest.raises(SystemExit) as exc:
|
|
58
|
+
webops.main_cert_check()
|
|
59
|
+
assert exc.value.code == 1
|
|
60
|
+
out = json.loads(capsys.readouterr().out)
|
|
61
|
+
assert out["host"] == "expired.test"
|
|
62
|
+
assert out["status"] == "expired"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_main_cert_check_unreachable_exits_2(monkeypatch, capsys) -> None:
|
|
66
|
+
monkeypatch.setattr(webops.rs, "parse", _fixed_parse("cert-check", ["down.test"]))
|
|
67
|
+
|
|
68
|
+
def boom(*_a, **_k):
|
|
69
|
+
raise webops.ConnectError("refused")
|
|
70
|
+
|
|
71
|
+
monkeypatch.setattr(webops, "connect_peer", boom)
|
|
72
|
+
with pytest.raises(SystemExit) as exc:
|
|
73
|
+
webops.main_cert_check()
|
|
74
|
+
assert exc.value.code == 2
|
|
75
|
+
out = json.loads(capsys.readouterr().out)
|
|
76
|
+
assert "error" in out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_main_api_spec_overview(monkeypatch, capsys, tmp_path) -> None:
|
|
80
|
+
spec_file = tmp_path / "openapi.json"
|
|
81
|
+
spec_file.write_text(json.dumps({"openapi": "3.0.0", "info": {"title": "T", "version": "1"}, "paths": {"/x": {"get": {"operationId": "getX", "responses": {"200": {"description": "ok"}}}}}}))
|
|
82
|
+
monkeypatch.setattr(webops.rs, "parse", _fixed_parse("api-spec", [str(spec_file)]))
|
|
83
|
+
webops.main_api_spec()
|
|
84
|
+
out = json.loads(capsys.readouterr().out)
|
|
85
|
+
assert out["title"] == "T"
|
|
86
|
+
assert out["operations"][0]["operation_id"] == "getX"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_main_api_spec_bad_source_exits_2(monkeypatch, capsys) -> None:
|
|
90
|
+
monkeypatch.setattr(webops.rs, "parse", _fixed_parse("api-spec", ["/no/such/spec.json"]))
|
|
91
|
+
with pytest.raises(SystemExit) as exc:
|
|
92
|
+
webops.main_api_spec()
|
|
93
|
+
assert exc.value.code == 2
|
|
94
|
+
assert "error" in json.loads(capsys.readouterr().out)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_main_dns_lookup_resolve_error_exits_1(monkeypatch, capsys) -> None:
|
|
98
|
+
monkeypatch.setattr(webops.rs, "parse", _fixed_parse("dns-lookup", ["bad.invalid"]))
|
|
99
|
+
|
|
100
|
+
def boom(*_a, **_k):
|
|
101
|
+
raise webops.ResolveError("nxdomain")
|
|
102
|
+
|
|
103
|
+
monkeypatch.setattr(webops, "resolve", boom)
|
|
104
|
+
with pytest.raises(SystemExit) as exc:
|
|
105
|
+
webops.main_dns_lookup()
|
|
106
|
+
assert exc.value.code == 1
|
|
107
|
+
assert "error" in json.loads(capsys.readouterr().out)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|