nectar-conformance 0.1.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.
- nectar_conformance-0.1.0/PKG-INFO +117 -0
- nectar_conformance-0.1.0/README.md +92 -0
- nectar_conformance-0.1.0/nectar_conformance/__init__.py +3 -0
- nectar_conformance-0.1.0/nectar_conformance/cli/__init__.py +1 -0
- nectar_conformance-0.1.0/nectar_conformance/cli/commands/__init__.py +1 -0
- nectar_conformance-0.1.0/nectar_conformance/cli/commands/changelog_cmd.py +46 -0
- nectar_conformance-0.1.0/nectar_conformance/cli/commands/check_meta.py +112 -0
- nectar_conformance-0.1.0/nectar_conformance/cli/commands/check_run.py +136 -0
- nectar_conformance-0.1.0/nectar_conformance/cli/commands/diff.py +50 -0
- nectar_conformance-0.1.0/nectar_conformance/cli/commands/report_diff.py +52 -0
- nectar_conformance-0.1.0/nectar_conformance/cli/commands/version_cmd.py +110 -0
- nectar_conformance-0.1.0/nectar_conformance/cli/main.py +29 -0
- nectar_conformance-0.1.0/nectar_conformance/config.py +235 -0
- nectar_conformance-0.1.0/nectar_conformance/datasources/__init__.py +1 -0
- nectar_conformance-0.1.0/nectar_conformance/datasources/base.py +48 -0
- nectar_conformance-0.1.0/nectar_conformance/datasources/compile.py +110 -0
- nectar_conformance-0.1.0/nectar_conformance/datasources/puppetdb.py +178 -0
- nectar_conformance-0.1.0/nectar_conformance/datasources/static_repo.py +192 -0
- nectar_conformance-0.1.0/nectar_conformance/engine/__init__.py +1 -0
- nectar_conformance-0.1.0/nectar_conformance/engine/operators.py +103 -0
- nectar_conformance-0.1.0/nectar_conformance/engine/queries.py +72 -0
- nectar_conformance-0.1.0/nectar_conformance/engine/runner.py +331 -0
- nectar_conformance-0.1.0/nectar_conformance/engine/selectors.py +32 -0
- nectar_conformance-0.1.0/nectar_conformance/errors.py +38 -0
- nectar_conformance-0.1.0/nectar_conformance/model.py +113 -0
- nectar_conformance-0.1.0/nectar_conformance/plugins/__init__.py +1 -0
- nectar_conformance-0.1.0/nectar_conformance/plugins/base.py +31 -0
- nectar_conformance-0.1.0/nectar_conformance/plugins/registry.py +24 -0
- nectar_conformance-0.1.0/nectar_conformance/report/__init__.py +1 -0
- nectar_conformance-0.1.0/nectar_conformance/report/human.py +101 -0
- nectar_conformance-0.1.0/nectar_conformance/report/json_report.py +13 -0
- nectar_conformance-0.1.0/nectar_conformance/results/__init__.py +1 -0
- nectar_conformance-0.1.0/nectar_conformance/results/compare.py +66 -0
- nectar_conformance-0.1.0/nectar_conformance/results/model.py +196 -0
- nectar_conformance-0.1.0/nectar_conformance/results/serialise.py +72 -0
- nectar_conformance-0.1.0/nectar_conformance/rollout.py +109 -0
- nectar_conformance-0.1.0/nectar_conformance/rules/__init__.py +1 -0
- nectar_conformance-0.1.0/nectar_conformance/rules/changelog.py +311 -0
- nectar_conformance-0.1.0/nectar_conformance/rules/loader.py +84 -0
- nectar_conformance-0.1.0/nectar_conformance/rules/model.py +224 -0
- nectar_conformance-0.1.0/nectar_conformance/rules/schema.json +113 -0
- nectar_conformance-0.1.0/nectar_conformance/rules/schema.py +44 -0
- nectar_conformance-0.1.0/nectar_conformance/service.py +434 -0
- nectar_conformance-0.1.0/nectar_conformance/web/__init__.py +8 -0
- nectar_conformance-0.1.0/nectar_conformance/web/api.py +211 -0
- nectar_conformance-0.1.0/nectar_conformance/web/app.py +111 -0
- nectar_conformance-0.1.0/nectar_conformance/web/refresh.py +231 -0
- nectar_conformance-0.1.0/nectar_conformance/web/serialise.py +63 -0
- nectar_conformance-0.1.0/nectar_conformance/web/settings.py +63 -0
- nectar_conformance-0.1.0/nectar_conformance/web/store.py +71 -0
- nectar_conformance-0.1.0/nectar_conformance.egg-info/PKG-INFO +117 -0
- nectar_conformance-0.1.0/nectar_conformance.egg-info/SOURCES.txt +73 -0
- nectar_conformance-0.1.0/nectar_conformance.egg-info/dependency_links.txt +1 -0
- nectar_conformance-0.1.0/nectar_conformance.egg-info/entry_points.txt +14 -0
- nectar_conformance-0.1.0/nectar_conformance.egg-info/requires.txt +19 -0
- nectar_conformance-0.1.0/nectar_conformance.egg-info/top_level.txt +1 -0
- nectar_conformance-0.1.0/pyproject.toml +78 -0
- nectar_conformance-0.1.0/setup.cfg +4 -0
- nectar_conformance-0.1.0/tests/test_changelog.py +373 -0
- nectar_conformance-0.1.0/tests/test_cli.py +355 -0
- nectar_conformance-0.1.0/tests/test_compile.py +103 -0
- nectar_conformance-0.1.0/tests/test_config.py +75 -0
- nectar_conformance-0.1.0/tests/test_engine.py +236 -0
- nectar_conformance-0.1.0/tests/test_loader_schema.py +71 -0
- nectar_conformance-0.1.0/tests/test_operators.py +61 -0
- nectar_conformance-0.1.0/tests/test_puppetdb_source.py +111 -0
- nectar_conformance-0.1.0/tests/test_refresh.py +136 -0
- nectar_conformance-0.1.0/tests/test_report.py +38 -0
- nectar_conformance-0.1.0/tests/test_report_diff.py +45 -0
- nectar_conformance-0.1.0/tests/test_rollout.py +134 -0
- nectar_conformance-0.1.0/tests/test_service_web.py +149 -0
- nectar_conformance-0.1.0/tests/test_static_source.py +68 -0
- nectar_conformance-0.1.0/tests/test_web_api.py +127 -0
- nectar_conformance-0.1.0/tests/test_web_app.py +79 -0
- nectar_conformance-0.1.0/tests/test_web_internals.py +99 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nectar-conformance
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Conformance checker for Nectar puppet-managed OpenStack cloud sites
|
|
5
|
+
Author: ARDC Nectar Core Services
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: httpx>=0.24
|
|
10
|
+
Requires-Dist: PyYAML>=6
|
|
11
|
+
Requires-Dist: jsonschema>=4
|
|
12
|
+
Requires-Dist: packaging>=21
|
|
13
|
+
Requires-Dist: cliff>=4
|
|
14
|
+
Requires-Dist: rich>=13
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
17
|
+
Requires-Dist: pytest-cov>=4; extra == "test"
|
|
18
|
+
Requires-Dist: respx>=0.20; extra == "test"
|
|
19
|
+
Requires-Dist: fastapi>=0.110; extra == "test"
|
|
20
|
+
Requires-Dist: uvicorn>=0.27; extra == "test"
|
|
21
|
+
Provides-Extra: static
|
|
22
|
+
Provides-Extra: web
|
|
23
|
+
Requires-Dist: fastapi>=0.110; extra == "web"
|
|
24
|
+
Requires-Dist: uvicorn[standard]>=0.27; extra == "web"
|
|
25
|
+
|
|
26
|
+
# nectar-conformance
|
|
27
|
+
|
|
28
|
+
A conformance checker for Nectar puppet-managed OpenStack cloud sites.
|
|
29
|
+
|
|
30
|
+
Nectar Core Services manage every site through a central puppet server. Sites drift
|
|
31
|
+
from the published specification over time. This tool tells a site operator how their
|
|
32
|
+
site conforms to a versioned Nectar conformance specification, what is wrong or
|
|
33
|
+
missing, and how to fix it.
|
|
34
|
+
|
|
35
|
+
## How it works
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
DataSource -> normalised Model -> Engine (versioned rules) -> Report -> human / JSON
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- A **site is a puppet environment**. The tool identifies a site by the node's
|
|
42
|
+
`environment` in PuppetDB.
|
|
43
|
+
- The **PuppetDB** data source (primary) reads the compiled catalog (resources and
|
|
44
|
+
parameters, with hiera fully resolved) and facts for every node in the site's
|
|
45
|
+
environment. The **static repo** data source reads pre-compiled catalog JSON, or
|
|
46
|
+
compiles catalogs from a site repo, for pre-deployment and commissioning checks.
|
|
47
|
+
- **Checks** are curated by Core Services as value-free *definitions* (the logic) plus
|
|
48
|
+
per-version *manifests* (which checks apply and their expected values). Conformance
|
|
49
|
+
is versioned; several conformance versions are active at once.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
The check definitions and conformance changelog live in the separate
|
|
54
|
+
[`nectar-conformance-checks`](https://review.rc.nectar.org.au) repository, not in this
|
|
55
|
+
tool. Every command that reads them needs a checks directory: pass `--checks-dir <path>`,
|
|
56
|
+
set `NECTAR_CONFORMANCE_CHECKS_DIR`, or set `checks_dir` in the config file. The examples
|
|
57
|
+
below omit it for brevity.
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
nectar-conformance check run --site ardctest --conformance-version 2025.1
|
|
61
|
+
nectar-conformance check run --site ardctest --conformance-version 2025.1 --format json
|
|
62
|
+
nectar-conformance check list --conformance-version 2025.1
|
|
63
|
+
nectar-conformance check show glance.api.image_tag
|
|
64
|
+
nectar-conformance version list
|
|
65
|
+
nectar-conformance version diff 2024.1 2025.1
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Static source (no live PuppetDB), either pre-compiled catalogs or compile-from-repo:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
# pre-compiled catalog JSON, one file per node
|
|
72
|
+
nectar-conformance check run --site ardctest --source static \
|
|
73
|
+
--catalog-dir ./catalogs --facts-dir ./facts
|
|
74
|
+
|
|
75
|
+
# compile from the site repo (one node per facts file; compiler set via
|
|
76
|
+
# static.compile_command in config, defaults to octocatalog-diff)
|
|
77
|
+
nectar-conformance check run --site ardctest --source static \
|
|
78
|
+
--site-repo /path/to/site-repo --facts-dir ./facts
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Will a change fix conformance before it goes live?
|
|
82
|
+
|
|
83
|
+
A site is a puppet environment, and r10k deploys each branch of the control repo as its
|
|
84
|
+
own environment, so you can check a *proposed* change pre-merge and compare it to the
|
|
85
|
+
live site. Capture the live baseline, capture the proposed environment, and diff them:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
# baseline: the live site
|
|
89
|
+
nectar-conformance check run --site ardctest --conformance-version 2025.1 \
|
|
90
|
+
--format json > before.json
|
|
91
|
+
|
|
92
|
+
# the proposed change, deployed as a branch environment (--environment keeps the
|
|
93
|
+
# 'ardctest' label but queries that environment)
|
|
94
|
+
nectar-conformance check run --site ardctest --environment ardctest_fix_glance \
|
|
95
|
+
--conformance-version 2025.1 --format json > after.json
|
|
96
|
+
|
|
97
|
+
# what does the change fix, and does it break anything? (exit 1 if it regresses)
|
|
98
|
+
nectar-conformance report diff before.json after.json
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
For a truly offline check (no deploy, no node runs), produce `after.json` with the
|
|
102
|
+
static source by compiling the proposed branch (`--source static --site-repo ...`); that
|
|
103
|
+
needs the environment's modules assembled, which is what octocatalog-diff / r10k do.
|
|
104
|
+
|
|
105
|
+
Exit codes: `0` conformant, `1` conformance failure at or above the severity
|
|
106
|
+
threshold, `2` usage error, `3` operational error (PuppetDB unreachable, etc.).
|
|
107
|
+
`report diff` exits `1` if the change introduces any new failure.
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
tox # run unit tests and lint
|
|
113
|
+
tox -e pep8 # lint only
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Releases use [reno](https://docs.openstack.org/reno/) for release notes. Contributions
|
|
117
|
+
go through gerrit; use conventional commits and `git commit -s`.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# nectar-conformance
|
|
2
|
+
|
|
3
|
+
A conformance checker for Nectar puppet-managed OpenStack cloud sites.
|
|
4
|
+
|
|
5
|
+
Nectar Core Services manage every site through a central puppet server. Sites drift
|
|
6
|
+
from the published specification over time. This tool tells a site operator how their
|
|
7
|
+
site conforms to a versioned Nectar conformance specification, what is wrong or
|
|
8
|
+
missing, and how to fix it.
|
|
9
|
+
|
|
10
|
+
## How it works
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
DataSource -> normalised Model -> Engine (versioned rules) -> Report -> human / JSON
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- A **site is a puppet environment**. The tool identifies a site by the node's
|
|
17
|
+
`environment` in PuppetDB.
|
|
18
|
+
- The **PuppetDB** data source (primary) reads the compiled catalog (resources and
|
|
19
|
+
parameters, with hiera fully resolved) and facts for every node in the site's
|
|
20
|
+
environment. The **static repo** data source reads pre-compiled catalog JSON, or
|
|
21
|
+
compiles catalogs from a site repo, for pre-deployment and commissioning checks.
|
|
22
|
+
- **Checks** are curated by Core Services as value-free *definitions* (the logic) plus
|
|
23
|
+
per-version *manifests* (which checks apply and their expected values). Conformance
|
|
24
|
+
is versioned; several conformance versions are active at once.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
The check definitions and conformance changelog live in the separate
|
|
29
|
+
[`nectar-conformance-checks`](https://review.rc.nectar.org.au) repository, not in this
|
|
30
|
+
tool. Every command that reads them needs a checks directory: pass `--checks-dir <path>`,
|
|
31
|
+
set `NECTAR_CONFORMANCE_CHECKS_DIR`, or set `checks_dir` in the config file. The examples
|
|
32
|
+
below omit it for brevity.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
nectar-conformance check run --site ardctest --conformance-version 2025.1
|
|
36
|
+
nectar-conformance check run --site ardctest --conformance-version 2025.1 --format json
|
|
37
|
+
nectar-conformance check list --conformance-version 2025.1
|
|
38
|
+
nectar-conformance check show glance.api.image_tag
|
|
39
|
+
nectar-conformance version list
|
|
40
|
+
nectar-conformance version diff 2024.1 2025.1
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Static source (no live PuppetDB), either pre-compiled catalogs or compile-from-repo:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
# pre-compiled catalog JSON, one file per node
|
|
47
|
+
nectar-conformance check run --site ardctest --source static \
|
|
48
|
+
--catalog-dir ./catalogs --facts-dir ./facts
|
|
49
|
+
|
|
50
|
+
# compile from the site repo (one node per facts file; compiler set via
|
|
51
|
+
# static.compile_command in config, defaults to octocatalog-diff)
|
|
52
|
+
nectar-conformance check run --site ardctest --source static \
|
|
53
|
+
--site-repo /path/to/site-repo --facts-dir ./facts
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Will a change fix conformance before it goes live?
|
|
57
|
+
|
|
58
|
+
A site is a puppet environment, and r10k deploys each branch of the control repo as its
|
|
59
|
+
own environment, so you can check a *proposed* change pre-merge and compare it to the
|
|
60
|
+
live site. Capture the live baseline, capture the proposed environment, and diff them:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
# baseline: the live site
|
|
64
|
+
nectar-conformance check run --site ardctest --conformance-version 2025.1 \
|
|
65
|
+
--format json > before.json
|
|
66
|
+
|
|
67
|
+
# the proposed change, deployed as a branch environment (--environment keeps the
|
|
68
|
+
# 'ardctest' label but queries that environment)
|
|
69
|
+
nectar-conformance check run --site ardctest --environment ardctest_fix_glance \
|
|
70
|
+
--conformance-version 2025.1 --format json > after.json
|
|
71
|
+
|
|
72
|
+
# what does the change fix, and does it break anything? (exit 1 if it regresses)
|
|
73
|
+
nectar-conformance report diff before.json after.json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
For a truly offline check (no deploy, no node runs), produce `after.json` with the
|
|
77
|
+
static source by compiling the proposed branch (`--source static --site-repo ...`); that
|
|
78
|
+
needs the environment's modules assembled, which is what octocatalog-diff / r10k do.
|
|
79
|
+
|
|
80
|
+
Exit codes: `0` conformant, `1` conformance failure at or above the severity
|
|
81
|
+
threshold, `2` usage error, `3` operational error (PuppetDB unreachable, etc.).
|
|
82
|
+
`report diff` exits `1` if the change introduces any new failure.
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
tox # run unit tests and lint
|
|
88
|
+
tox -e pep8 # lint only
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Releases use [reno](https://docs.openstack.org/reno/) for release notes. Contributions
|
|
92
|
+
go through gerrit; use conventional commits and `git commit -s`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Operator-facing command-line interface (cliff)."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""cliff command implementations."""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""``changelog lint`` - structurally validate a checks directory.
|
|
2
|
+
|
|
3
|
+
The checks data lives in its own repository (``nectar-conformance-checks``); this is the
|
|
4
|
+
CI gate that repository runs to catch a malformed changelog or definition before merge.
|
|
5
|
+
It loads the definitions and changelog, runs :func:`changelog_lint`, and reports any
|
|
6
|
+
structural violations (unknown check ids, ``effective`` after ``due``, test due later
|
|
7
|
+
than prod, colliding entries).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from cliff.command import Command
|
|
13
|
+
|
|
14
|
+
from nectar_conformance import config as config_mod
|
|
15
|
+
from nectar_conformance.errors import ConformanceError
|
|
16
|
+
from nectar_conformance.service import lint_versions
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ChangelogLint(Command):
|
|
20
|
+
"""Validate the conformance changelog and definitions are structurally sound."""
|
|
21
|
+
|
|
22
|
+
def get_parser(self, prog_name):
|
|
23
|
+
parser = super().get_parser(prog_name)
|
|
24
|
+
parser.add_argument("--config", help="path to a config file")
|
|
25
|
+
parser.add_argument("--checks-dir", help="checks dir to lint")
|
|
26
|
+
return parser
|
|
27
|
+
|
|
28
|
+
def take_action(self, parsed_args):
|
|
29
|
+
overrides = (
|
|
30
|
+
{"checks_dir": parsed_args.checks_dir}
|
|
31
|
+
if parsed_args.checks_dir
|
|
32
|
+
else None
|
|
33
|
+
)
|
|
34
|
+
cfg = config_mod.load(parsed_args.config, overrides)
|
|
35
|
+
try:
|
|
36
|
+
violations = lint_versions(cfg)
|
|
37
|
+
except ConformanceError as exc:
|
|
38
|
+
self.app.stderr.write(f"error: {exc}\n")
|
|
39
|
+
return 3
|
|
40
|
+
if violations:
|
|
41
|
+
self.app.stderr.write("changelog lint found problems:\n")
|
|
42
|
+
for v in violations:
|
|
43
|
+
self.app.stderr.write(f" - {v}\n")
|
|
44
|
+
return 1
|
|
45
|
+
self.app.stdout.write("changelog lint: ok\n")
|
|
46
|
+
return 0
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""``check list`` and ``check show`` - inspect checks without touching PuppetDB."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from cliff.command import Command
|
|
6
|
+
|
|
7
|
+
from nectar_conformance import config as config_mod
|
|
8
|
+
from nectar_conformance.errors import ConformanceError
|
|
9
|
+
from nectar_conformance.service import get_check, list_checks
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _resolve_version(parsed_args, cfg):
|
|
13
|
+
return parsed_args.conformance_version or cfg.default_conformance_version
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CheckList(Command):
|
|
17
|
+
"""List the checks that apply at a conformance version."""
|
|
18
|
+
|
|
19
|
+
def get_parser(self, prog_name):
|
|
20
|
+
parser = super().get_parser(prog_name)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--conformance-version", help="conformance version, e.g. 2026.1"
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument("--config", help="path to a config file")
|
|
25
|
+
parser.add_argument("--checks-dir", help="load checks from this dir")
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--severity", choices=["info", "warning", "error"], help="filter"
|
|
28
|
+
)
|
|
29
|
+
return parser
|
|
30
|
+
|
|
31
|
+
def take_action(self, parsed_args):
|
|
32
|
+
overrides = (
|
|
33
|
+
{"checks_dir": parsed_args.checks_dir}
|
|
34
|
+
if parsed_args.checks_dir
|
|
35
|
+
else None
|
|
36
|
+
)
|
|
37
|
+
cfg = config_mod.load(parsed_args.config, overrides)
|
|
38
|
+
version = _resolve_version(parsed_args, cfg)
|
|
39
|
+
if not version:
|
|
40
|
+
self.app.stderr.write(
|
|
41
|
+
"error: no conformance version (use --conformance-version)\n"
|
|
42
|
+
)
|
|
43
|
+
return 2
|
|
44
|
+
try:
|
|
45
|
+
rules = list_checks(cfg, version)
|
|
46
|
+
except ConformanceError as exc:
|
|
47
|
+
self.app.stderr.write(f"error: {exc}\n")
|
|
48
|
+
return 3
|
|
49
|
+
out = self.app.stdout
|
|
50
|
+
out.write(f"Checks for conformance {version}:\n")
|
|
51
|
+
for rule in rules:
|
|
52
|
+
if parsed_args.severity and rule.severity != parsed_args.severity:
|
|
53
|
+
continue
|
|
54
|
+
expected = (
|
|
55
|
+
""
|
|
56
|
+
if rule.expected is None
|
|
57
|
+
else f" expected={rule.expected!r}"
|
|
58
|
+
)
|
|
59
|
+
section = rule.spec_section or "-"
|
|
60
|
+
out.write(
|
|
61
|
+
f" [{rule.severity:7}] {rule.id:30} ({section}){expected}\n"
|
|
62
|
+
)
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CheckShow(Command):
|
|
67
|
+
"""Show the full definition of one check."""
|
|
68
|
+
|
|
69
|
+
def get_parser(self, prog_name):
|
|
70
|
+
parser = super().get_parser(prog_name)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"check_id", help="check id, e.g. glance.api.image_tag"
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument("--config", help="path to a config file")
|
|
75
|
+
parser.add_argument("--checks-dir", help="load checks from this dir")
|
|
76
|
+
return parser
|
|
77
|
+
|
|
78
|
+
def take_action(self, parsed_args):
|
|
79
|
+
overrides = (
|
|
80
|
+
{"checks_dir": parsed_args.checks_dir}
|
|
81
|
+
if parsed_args.checks_dir
|
|
82
|
+
else None
|
|
83
|
+
)
|
|
84
|
+
cfg = config_mod.load(parsed_args.config, overrides)
|
|
85
|
+
check = get_check(cfg, parsed_args.check_id)
|
|
86
|
+
if check is None:
|
|
87
|
+
self.app.stderr.write(
|
|
88
|
+
f"error: no such check '{parsed_args.check_id}'\n"
|
|
89
|
+
)
|
|
90
|
+
return 3
|
|
91
|
+
out = self.app.stdout
|
|
92
|
+
out.write(f"{check.id}\n")
|
|
93
|
+
out.write(f" title: {check.title}\n")
|
|
94
|
+
out.write(f" severity: {check.severity}\n")
|
|
95
|
+
out.write(f" spec_section: {check.spec_section}\n")
|
|
96
|
+
out.write(f" kind: {check.kind}\n")
|
|
97
|
+
out.write(
|
|
98
|
+
f" selector: {check.selector.type} {check.selector.params}\n"
|
|
99
|
+
)
|
|
100
|
+
if check.query is not None:
|
|
101
|
+
out.write(
|
|
102
|
+
f" query: {check.query.type} {check.query.params}\n"
|
|
103
|
+
)
|
|
104
|
+
if check.assertion_op is not None:
|
|
105
|
+
out.write(f" assertion: {check.assertion_op}\n")
|
|
106
|
+
if check.remediation is not None:
|
|
107
|
+
out.write(f" remediation: {check.remediation.guidance}\n")
|
|
108
|
+
if check.remediation.hiera_key:
|
|
109
|
+
out.write(
|
|
110
|
+
f" hiera_key={check.remediation.hiera_key}\n"
|
|
111
|
+
)
|
|
112
|
+
return 0
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""``check run`` - evaluate a site against a conformance version."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from cliff.command import Command
|
|
6
|
+
|
|
7
|
+
from nectar_conformance import config as config_mod
|
|
8
|
+
from nectar_conformance.errors import ConformanceError
|
|
9
|
+
from nectar_conformance.report import human, json_report
|
|
10
|
+
from nectar_conformance.results.model import Report, Severity
|
|
11
|
+
from nectar_conformance.service import run_check
|
|
12
|
+
|
|
13
|
+
_RANK = {Severity.INFO: 1, Severity.WARNING: 2, Severity.ERROR: 3}
|
|
14
|
+
_THRESHOLD = {"info": 1, "warning": 2, "error": 3}
|
|
15
|
+
|
|
16
|
+
# Process exit codes.
|
|
17
|
+
EXIT_OK = 0
|
|
18
|
+
EXIT_NONCONFORMANT = 1
|
|
19
|
+
EXIT_OPERATIONAL = 3
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def exit_code(report: Report, threshold: str) -> int:
|
|
23
|
+
worst = report.worst_failing_severity()
|
|
24
|
+
if worst is None:
|
|
25
|
+
return EXIT_OK
|
|
26
|
+
return (
|
|
27
|
+
EXIT_NONCONFORMANT
|
|
28
|
+
if _RANK[worst] >= _THRESHOLD[threshold]
|
|
29
|
+
else EXIT_OK
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CheckRun(Command):
|
|
34
|
+
"""Run conformance checks for a site and report the results."""
|
|
35
|
+
|
|
36
|
+
def get_parser(self, prog_name):
|
|
37
|
+
parser = super().get_parser(prog_name)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--site", required=True, help="site (puppet environment) to check"
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--environment",
|
|
43
|
+
help="puppet environment to query instead of the site name; use to check a "
|
|
44
|
+
"proposed branch environment before it goes live (report is still labelled --site)",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--conformance-version",
|
|
48
|
+
help="conformance version tag to pin the evaluation date to (e.g. 2026.1); "
|
|
49
|
+
"omit for a live run against today",
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--as-of",
|
|
53
|
+
help="evaluate as if today were this date (YYYY-MM-DD), for scheduled and "
|
|
54
|
+
"what-if runs; overrides the version tag's pinned date",
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--site-tier",
|
|
58
|
+
choices=["test", "prod"],
|
|
59
|
+
help="treat the site as this tier for dated changes (overrides config; "
|
|
60
|
+
"default prod)",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--source", choices=["puppetdb", "static"], help="data source"
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--format", choices=["human", "json"], default="human"
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--severity-threshold",
|
|
70
|
+
choices=["info", "warning", "error"],
|
|
71
|
+
default="error",
|
|
72
|
+
help="lowest severity that makes the run exit non-zero (default: error)",
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument("--config", help="path to a config file")
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--puppetdb-url", help="override the PuppetDB base URL"
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--checks-dir",
|
|
80
|
+
help="load check definitions/manifests from this dir",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--site-repo", help="path to the site puppet repo (static source)"
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--catalog-dir",
|
|
87
|
+
help="dir of compiled catalog JSON (static source)",
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--facts-dir", help="dir of per-node facts JSON (static source)"
|
|
91
|
+
)
|
|
92
|
+
return parser
|
|
93
|
+
|
|
94
|
+
def take_action(self, parsed_args):
|
|
95
|
+
overrides: dict = {}
|
|
96
|
+
if parsed_args.puppetdb_url:
|
|
97
|
+
overrides.setdefault("puppetdb", {})["base_url"] = (
|
|
98
|
+
parsed_args.puppetdb_url
|
|
99
|
+
)
|
|
100
|
+
if parsed_args.source:
|
|
101
|
+
overrides["source"] = parsed_args.source
|
|
102
|
+
if parsed_args.checks_dir:
|
|
103
|
+
overrides["checks_dir"] = parsed_args.checks_dir
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
cfg = config_mod.load(parsed_args.config, overrides)
|
|
107
|
+
if parsed_args.environment:
|
|
108
|
+
# Query this environment but keep the site label/identity.
|
|
109
|
+
cfg.site_environment[parsed_args.site] = (
|
|
110
|
+
parsed_args.environment
|
|
111
|
+
)
|
|
112
|
+
if parsed_args.site_tier:
|
|
113
|
+
# Override the site's tier for this run (mirrors --environment).
|
|
114
|
+
cfg.site_tier[parsed_args.site] = parsed_args.site_tier
|
|
115
|
+
source_kwargs = {
|
|
116
|
+
"site_repo": parsed_args.site_repo,
|
|
117
|
+
"catalog_dir": parsed_args.catalog_dir,
|
|
118
|
+
"facts_dir": parsed_args.facts_dir,
|
|
119
|
+
}
|
|
120
|
+
report = run_check(
|
|
121
|
+
cfg,
|
|
122
|
+
site=parsed_args.site,
|
|
123
|
+
version=parsed_args.conformance_version,
|
|
124
|
+
source=parsed_args.source,
|
|
125
|
+
source_kwargs=source_kwargs,
|
|
126
|
+
as_of=parsed_args.as_of,
|
|
127
|
+
)
|
|
128
|
+
except ConformanceError as exc:
|
|
129
|
+
self.app.stderr.write(f"error: {exc}\n")
|
|
130
|
+
return EXIT_OPERATIONAL
|
|
131
|
+
|
|
132
|
+
if parsed_args.format == "json":
|
|
133
|
+
json_report.render(report, self.app.stdout)
|
|
134
|
+
else:
|
|
135
|
+
human.render(report, self.app.stdout)
|
|
136
|
+
return exit_code(report, parsed_args.severity_threshold)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""``version diff`` - show how the check set differs between two versions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from cliff.command import Command
|
|
6
|
+
|
|
7
|
+
from nectar_conformance import config as config_mod
|
|
8
|
+
from nectar_conformance.errors import ConformanceError
|
|
9
|
+
from nectar_conformance.service import diff_versions
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DiffVersions(Command):
|
|
13
|
+
"""Show checks added and removed between two conformance versions."""
|
|
14
|
+
|
|
15
|
+
def get_parser(self, prog_name):
|
|
16
|
+
parser = super().get_parser(prog_name)
|
|
17
|
+
parser.add_argument("version_a", help="the 'from' conformance version")
|
|
18
|
+
parser.add_argument("version_b", help="the 'to' conformance version")
|
|
19
|
+
parser.add_argument("--config", help="path to a config file")
|
|
20
|
+
parser.add_argument("--checks-dir", help="load checks from this dir")
|
|
21
|
+
return parser
|
|
22
|
+
|
|
23
|
+
def take_action(self, parsed_args):
|
|
24
|
+
overrides = (
|
|
25
|
+
{"checks_dir": parsed_args.checks_dir}
|
|
26
|
+
if parsed_args.checks_dir
|
|
27
|
+
else None
|
|
28
|
+
)
|
|
29
|
+
cfg = config_mod.load(parsed_args.config, overrides)
|
|
30
|
+
try:
|
|
31
|
+
diff = diff_versions(
|
|
32
|
+
cfg, parsed_args.version_a, parsed_args.version_b
|
|
33
|
+
)
|
|
34
|
+
except ConformanceError as exc:
|
|
35
|
+
self.app.stderr.write(f"error: {exc}\n")
|
|
36
|
+
return 3
|
|
37
|
+
out = self.app.stdout
|
|
38
|
+
out.write(f"{diff['from']} -> {diff['to']}\n")
|
|
39
|
+
out.write(f" changed ({len(diff['changed'])}):\n")
|
|
40
|
+
for change in diff["changed"]:
|
|
41
|
+
out.write(
|
|
42
|
+
f" ~ {change['check_id']}: {change['from']} -> {change['to']}\n"
|
|
43
|
+
)
|
|
44
|
+
out.write(f" added ({len(diff['added'])}):\n")
|
|
45
|
+
for cid in diff["added"]:
|
|
46
|
+
out.write(f" + {cid}\n")
|
|
47
|
+
out.write(f" removed ({len(diff['removed'])}):\n")
|
|
48
|
+
for cid in diff["removed"]:
|
|
49
|
+
out.write(f" - {cid}\n")
|
|
50
|
+
return 0
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""``report diff`` - compare two conformance report JSON files (before vs after)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from cliff.command import Command
|
|
8
|
+
|
|
9
|
+
from nectar_conformance.results.compare import compare_reports
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ReportDiff(Command):
|
|
13
|
+
"""Diff two 'check run --format json' reports to see what a change fixes or breaks."""
|
|
14
|
+
|
|
15
|
+
def get_parser(self, prog_name):
|
|
16
|
+
parser = super().get_parser(prog_name)
|
|
17
|
+
parser.add_argument("old", help="baseline report JSON (current/live)")
|
|
18
|
+
parser.add_argument("new", help="proposed report JSON (the change)")
|
|
19
|
+
return parser
|
|
20
|
+
|
|
21
|
+
def take_action(self, parsed_args):
|
|
22
|
+
try:
|
|
23
|
+
with open(parsed_args.old) as fh:
|
|
24
|
+
old = json.load(fh)
|
|
25
|
+
with open(parsed_args.new) as fh:
|
|
26
|
+
new = json.load(fh)
|
|
27
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
28
|
+
self.app.stderr.write(f"error: could not read report: {exc}\n")
|
|
29
|
+
return 3
|
|
30
|
+
|
|
31
|
+
diff = compare_reports(old, new)
|
|
32
|
+
out = self.app.stdout
|
|
33
|
+
|
|
34
|
+
def section(label, rows):
|
|
35
|
+
out.write(f"{label} ({len(rows)}):\n")
|
|
36
|
+
for r in rows:
|
|
37
|
+
arrow = f"{r['old']} -> {r['new']}"
|
|
38
|
+
out.write(f" {r['rule_id']} [{r['severity']}] {arrow}\n")
|
|
39
|
+
|
|
40
|
+
section("Fixed", diff["fixed"])
|
|
41
|
+
section("Regressed", diff["regressed"])
|
|
42
|
+
section("Still failing", diff["still_failing"])
|
|
43
|
+
if diff["added"] or diff["removed"]:
|
|
44
|
+
section("Added checks", diff["added"])
|
|
45
|
+
section("Removed checks", diff["removed"])
|
|
46
|
+
|
|
47
|
+
out.write(
|
|
48
|
+
f"\nSummary: {len(diff['fixed'])} fixed, {len(diff['regressed'])} regressed, "
|
|
49
|
+
f"{len(diff['still_failing'])} still failing\n"
|
|
50
|
+
)
|
|
51
|
+
# Non-zero exit if the change introduces any new failure, so CI can gate on it.
|
|
52
|
+
return 1 if diff["regressed"] else 0
|