mongo-x-ray-gmd 2.0.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.
- mongo_x_ray_gmd-2.0.0/PKG-INFO +79 -0
- mongo_x_ray_gmd-2.0.0/README.md +68 -0
- mongo_x_ray_gmd-2.0.0/pyproject.toml +63 -0
- mongo_x_ray_gmd-2.0.0/setup.cfg +4 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/__init__.py +11 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/framework.py +137 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/__init__.py +11 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/base_item.py +218 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/build_info_item.py +43 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/coll_info_item.py +107 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/db_item.py +78 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/host_info_item.py +64 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/index_info_item.py +97 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/rs_info_item.py +131 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/security_item.py +64 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/server_status_item.py +129 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/sh_info_item.py +88 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/gmd_items/summary_item.py +93 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/parsers/__init__.py +9 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/parsers/base_parser.py +22 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/parsers/coll_stats_parser.py +139 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/parsers/db_parser.py +138 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/parsers/sh_details_parser.py +42 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/plugin.py +79 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/shared.py +91 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/full.html +1 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/full.raw.html +30 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/script.js +1 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/script.raw.js +108 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/snippets/CollStatsParser_1.js +1 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/snippets/CollStatsParser_1.raw.js +89 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/snippets/DBParser_1.js +1 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/gmd/snippets/DBParser_1.raw.js +47 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd/templates/minify.sh +51 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd.egg-info/PKG-INFO +79 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd.egg-info/SOURCES.txt +41 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd.egg-info/dependency_links.txt +1 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd.egg-info/entry_points.txt +2 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd.egg-info/requires.txt +4 -0
- mongo_x_ray_gmd-2.0.0/src/mongo_x_ray_gmd.egg-info/top_level.txt +1 -0
- mongo_x_ray_gmd-2.0.0/tests/test_pdf_output.py +44 -0
- mongo_x_ray_gmd-2.0.0/tests/test_report_ui.py +214 -0
- mongo_x_ray_gmd-2.0.0/tests/test_shared.py +66 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mongo-x-ray-gmd
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: getMongoData analysis plugin for x-ray
|
|
5
|
+
Requires-Python: <4,>=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: mongo-x-ray>=2.0.0
|
|
8
|
+
Requires-Dist: mongo-x-ray-hc>=2.0.0
|
|
9
|
+
Requires-Dist: pymongo>=4.0
|
|
10
|
+
Requires-Dist: python-dateutil>=2.9.0
|
|
11
|
+
|
|
12
|
+
# mongo-x-ray-gmd
|
|
13
|
+
|
|
14
|
+
[](https://github.com/zhangyaoxing/mongo-x-ray-gmd/actions/workflows/ci.yml)
|
|
15
|
+
[](https://pypi.org/project/mongo-x-ray-gmd/)
|
|
16
|
+
|
|
17
|
+
getMongoData analysis plugin for [x-ray](https://github.com/mongodb-ps/ce-mongo-x-ray).
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install mongo-x-ray mongo-x-ray-hc mongo-x-ray-gmd
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
x-ray gmd /path/to/getMongoData-output.json
|
|
29
|
+
x-ray gmd /path/to/getMongoData-output.json -f html -o /path/to/output/
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Compatibility
|
|
33
|
+
|
|
34
|
+
Supports MongoDB 5.0 and above on all topologies:
|
|
35
|
+
|
|
36
|
+
| Replica Set | Sharded Cluster | Standalone |
|
|
37
|
+
| :---------: | :-------------: | :--------: |
|
|
38
|
+
| ✅ | ✅ | ✅ |
|
|
39
|
+
|
|
40
|
+
## Parameters
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
x-ray gmd [-h] [-s CHECKSET] [-o OUTPUT] [-f {markdown,html,pdf}] [--no-browser]
|
|
44
|
+
gmd_file
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Argument | Description | Default |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `gmd_file` | Path to the getMongoData output JSON file. | required |
|
|
50
|
+
| `-s, --checkset` | Checkset to run. | `default` |
|
|
51
|
+
| `-o, --output` | Output folder path. | `output/` |
|
|
52
|
+
| `-f, --format` | Output format: `markdown`, `html` or `pdf` (PDF also keeps Markdown and HTML). | `html` |
|
|
53
|
+
| `--no-browser` | Do not open the generated report in the browser. | `false` |
|
|
54
|
+
|
|
55
|
+
## Analysis Items
|
|
56
|
+
|
|
57
|
+
| Item | Purpose |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `SummaryItem` | Overall summary of the getMongoData output. |
|
|
60
|
+
| `BuildInfoItem` | Build information (reuses the healthcheck build info parser and version EOL rule). |
|
|
61
|
+
| `CollInfoItem` | Collection statistics: sizes and fragmentation. |
|
|
62
|
+
| `DBItem` | Database-level information. |
|
|
63
|
+
| `HostInfoItem` | Host filesystem type, NUMA settings and host properties. |
|
|
64
|
+
| `IndexInfoItem` | Index information. |
|
|
65
|
+
| `RSInfoItem` | Replica set topology, oplog window and status. |
|
|
66
|
+
| `SecurityItem` | Security posture. |
|
|
67
|
+
| `ServerStatusItem` | Cache, connections and query targeting status. |
|
|
68
|
+
| `SHInfoItem` | Sharding architecture and shard details. |
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
Requires Python 3.10+, MongoDB 5.0 or later, the [mongo-x-ray](https://github.com/mongodb-ps/ce-mongo-x-ray) core and the
|
|
73
|
+
[mongo-x-ray-hc](https://github.com/zhangyaoxing/mongo-x-ray-hc) plugin (gmd reuses its parsers and rules).
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
make unit-test # run the unit tests
|
|
77
|
+
make lint # ruff check + ruff format --check
|
|
78
|
+
make minify # minify templates
|
|
79
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# mongo-x-ray-gmd
|
|
2
|
+
|
|
3
|
+
[](https://github.com/zhangyaoxing/mongo-x-ray-gmd/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/mongo-x-ray-gmd/)
|
|
5
|
+
|
|
6
|
+
getMongoData analysis plugin for [x-ray](https://github.com/mongodb-ps/ce-mongo-x-ray).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install mongo-x-ray mongo-x-ray-hc mongo-x-ray-gmd
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
x-ray gmd /path/to/getMongoData-output.json
|
|
18
|
+
x-ray gmd /path/to/getMongoData-output.json -f html -o /path/to/output/
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Compatibility
|
|
22
|
+
|
|
23
|
+
Supports MongoDB 5.0 and above on all topologies:
|
|
24
|
+
|
|
25
|
+
| Replica Set | Sharded Cluster | Standalone |
|
|
26
|
+
| :---------: | :-------------: | :--------: |
|
|
27
|
+
| ✅ | ✅ | ✅ |
|
|
28
|
+
|
|
29
|
+
## Parameters
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
x-ray gmd [-h] [-s CHECKSET] [-o OUTPUT] [-f {markdown,html,pdf}] [--no-browser]
|
|
33
|
+
gmd_file
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Argument | Description | Default |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| `gmd_file` | Path to the getMongoData output JSON file. | required |
|
|
39
|
+
| `-s, --checkset` | Checkset to run. | `default` |
|
|
40
|
+
| `-o, --output` | Output folder path. | `output/` |
|
|
41
|
+
| `-f, --format` | Output format: `markdown`, `html` or `pdf` (PDF also keeps Markdown and HTML). | `html` |
|
|
42
|
+
| `--no-browser` | Do not open the generated report in the browser. | `false` |
|
|
43
|
+
|
|
44
|
+
## Analysis Items
|
|
45
|
+
|
|
46
|
+
| Item | Purpose |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `SummaryItem` | Overall summary of the getMongoData output. |
|
|
49
|
+
| `BuildInfoItem` | Build information (reuses the healthcheck build info parser and version EOL rule). |
|
|
50
|
+
| `CollInfoItem` | Collection statistics: sizes and fragmentation. |
|
|
51
|
+
| `DBItem` | Database-level information. |
|
|
52
|
+
| `HostInfoItem` | Host filesystem type, NUMA settings and host properties. |
|
|
53
|
+
| `IndexInfoItem` | Index information. |
|
|
54
|
+
| `RSInfoItem` | Replica set topology, oplog window and status. |
|
|
55
|
+
| `SecurityItem` | Security posture. |
|
|
56
|
+
| `ServerStatusItem` | Cache, connections and query targeting status. |
|
|
57
|
+
| `SHInfoItem` | Sharding architecture and shard details. |
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
Requires Python 3.10+, MongoDB 5.0 or later, the [mongo-x-ray](https://github.com/mongodb-ps/ce-mongo-x-ray) core and the
|
|
62
|
+
[mongo-x-ray-hc](https://github.com/zhangyaoxing/mongo-x-ray-hc) plugin (gmd reuses its parsers and rules).
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
make unit-test # run the unit tests
|
|
66
|
+
make lint # ruff check + ruff format --check
|
|
67
|
+
make minify # minify templates
|
|
68
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools==83.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mongo-x-ray-gmd"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "getMongoData analysis plugin for x-ray"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10,<4"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"mongo-x-ray>=2.0.0",
|
|
13
|
+
"mongo-x-ray-hc>=2.0.0",
|
|
14
|
+
"pymongo>=4.0",
|
|
15
|
+
"python-dateutil>=2.9.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.entry-points."mongo_x_ray.plugins"]
|
|
19
|
+
gmd = "mongo_x_ray_gmd.plugin:GmdPlugin"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
23
|
+
include = ["mongo_x_ray_gmd*"]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.package-data]
|
|
26
|
+
"mongo_x_ray_gmd" = ["templates/**/*"]
|
|
27
|
+
|
|
28
|
+
[tool.pytest.ini_options]
|
|
29
|
+
testpaths = ["tests"]
|
|
30
|
+
pythonpath = ["src"]
|
|
31
|
+
python_files = ["test_*.py"]
|
|
32
|
+
python_classes = ["Test*"]
|
|
33
|
+
python_functions = ["test_*"]
|
|
34
|
+
markers = [
|
|
35
|
+
"slow: marks tests as slow",
|
|
36
|
+
"integration: marks tests as integration tests",
|
|
37
|
+
"unit: marks tests as unit tests",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
line-length = 120
|
|
43
|
+
target-version = "py310"
|
|
44
|
+
exclude = [".venv", "build", "dist"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint]
|
|
47
|
+
select = ["E", "F", "I"]
|
|
48
|
+
ignore = ["E501"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint.isort]
|
|
51
|
+
known-first-party = ["mongo_x_ray", "mongo_x_ray_ftdc", "mongo_x_ray_gmd", "mongo_x_ray_hc", "mongo_x_ray_log", "mongo_x_ray_risk"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
[tool.pyright]
|
|
55
|
+
pythonVersion = "3.10"
|
|
56
|
+
venvPath = "../ce-mongo-x-ray"
|
|
57
|
+
venv = ".venv"
|
|
58
|
+
typeCheckingMode = "basic"
|
|
59
|
+
extraPaths = [
|
|
60
|
+
"src",
|
|
61
|
+
"../ce-mongo-x-ray/src",
|
|
62
|
+
"../mongo-x-ray-hc/src",
|
|
63
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026 MongoDB Inc.
|
|
3
|
+
|
|
4
|
+
DISCLAIMER: THESE CODE SAMPLES ARE PROVIDED FOR EDUCATIONAL AND ILLUSTRATIVE PURPOSES ONLY,
|
|
5
|
+
TO DEMONSTRATE THE FUNCTIONALITY OF SPECIFIC MONGODB FEATURES.
|
|
6
|
+
THEY ARE NOT PRODUCTION-READY AND MAY LACK THE SECURITY HARDENING, ERROR HANDLING, AND TESTING REQUIRED FOR A LIVE ENVIRONMENT.
|
|
7
|
+
YOU ARE RESPONSIBLE FOR TESTING, VALIDATING, AND SECURING THIS CODE WITHIN YOUR OWN ENVIRONMENT BEFORE IMPLEMENTATION.
|
|
8
|
+
THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OR LIABILITY.
|
|
9
|
+
|
|
10
|
+
MongoDB getMongoData analysis package.
|
|
11
|
+
"""
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026 MongoDB Inc.
|
|
3
|
+
|
|
4
|
+
DISCLAIMER: THESE CODE SAMPLES ARE PROVIDED FOR EDUCATIONAL AND ILLUSTRATIVE PURPOSES ONLY,
|
|
5
|
+
TO DEMONSTRATE THE FUNCTIONALITY OF SPECIFIC MONGODB FEATURES.
|
|
6
|
+
THEY ARE NOT PRODUCTION-READY AND MAY LACK THE SECURITY HARDENING, ERROR HANDLING, AND TESTING REQUIRED FOR A LIVE ENVIRONMENT.
|
|
7
|
+
YOU ARE RESPONSIBLE FOR TESTING, VALIDATING, AND SECURING THIS CODE WITHIN YOUR OWN ENVIRONMENT BEFORE IMPLEMENTATION.
|
|
8
|
+
THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OR LIABILITY.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import TextIO
|
|
13
|
+
|
|
14
|
+
from mongo_x_ray.framework import BaseFramework
|
|
15
|
+
from mongo_x_ray.shared import str_to_md_id, to_json
|
|
16
|
+
from mongo_x_ray.utils import bold, cyan, env, green, load_classes, red, yellow
|
|
17
|
+
from mongo_x_ray_gmd.gmd_items.summary_item import SummaryItem
|
|
18
|
+
from mongo_x_ray_gmd.shared import load_json
|
|
19
|
+
|
|
20
|
+
GMD_CLASSES = load_classes("mongo_x_ray_gmd.gmd_items")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Framework(BaseFramework):
|
|
24
|
+
template_module = "gmd"
|
|
25
|
+
template_package = "mongo_x_ray_gmd"
|
|
26
|
+
|
|
27
|
+
def __init__(self, file_path: str, config: dict):
|
|
28
|
+
super().__init__(config)
|
|
29
|
+
self._file_path = file_path
|
|
30
|
+
self._logger.debug(to_json(self._config))
|
|
31
|
+
self._log_start = None
|
|
32
|
+
self._log_end = None
|
|
33
|
+
if env == "development":
|
|
34
|
+
self._logger.info(yellow("Running in development mode."))
|
|
35
|
+
|
|
36
|
+
def run_gmd_analysis(self, gmd_set_name: str, *_args, **kwargs):
|
|
37
|
+
self._set_name = gmd_set_name
|
|
38
|
+
# Create output folder if it doesn't exist
|
|
39
|
+
output_folder = kwargs.get("output_folder", "output/")
|
|
40
|
+
batch_folder = self._get_output_folder(output_folder)
|
|
41
|
+
# Dynamically load the gmd checkset based on the name
|
|
42
|
+
gmdsets = self._config.get("gmdsets", {})
|
|
43
|
+
if gmd_set_name not in gmdsets:
|
|
44
|
+
self._logger.warning(yellow(f"GMD checkset '{gmd_set_name}' not found in configuration. Using default."))
|
|
45
|
+
gmd_set_name = "default"
|
|
46
|
+
gmdset = gmdsets[gmd_set_name]
|
|
47
|
+
self._logger.info("Running GMD checkset: %s", bold(cyan(gmd_set_name)))
|
|
48
|
+
|
|
49
|
+
self._items = []
|
|
50
|
+
for item_name in gmdset.get("items", []):
|
|
51
|
+
item_cls = GMD_CLASSES.get(item_name)
|
|
52
|
+
if not item_cls:
|
|
53
|
+
self._logger.warning(yellow(f"GMD item '{item_name}' not found. Skipping."))
|
|
54
|
+
continue
|
|
55
|
+
# The config for the item can be specified in the `item_config` section, under the item class name.
|
|
56
|
+
item_config = self._config.get("item_config", {}).get(item_name, {})
|
|
57
|
+
item = item_cls(str(batch_folder), item_config)
|
|
58
|
+
self._items.append(item)
|
|
59
|
+
self._logger.info("GMD analyze item loaded: %s", bold(cyan(item_name)))
|
|
60
|
+
gmd_output = self._file_path
|
|
61
|
+
|
|
62
|
+
# Read the getMongoData output and parse the whole content.
|
|
63
|
+
with open(gmd_output, "r", encoding="utf-8", errors="ignore") as f:
|
|
64
|
+
content = f.read()
|
|
65
|
+
try:
|
|
66
|
+
objects = load_json(content)
|
|
67
|
+
except Exception as ex:
|
|
68
|
+
self._logger.error(red(f"Failed to parse the getMongoData output as JSON: {ex}"))
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
self._logger.info("Ingesting %s objects from getMongoData output...", green(str(len(objects))))
|
|
72
|
+
|
|
73
|
+
for i, obj in enumerate(objects):
|
|
74
|
+
if (i + 1) % 10000 == 0:
|
|
75
|
+
self._logger.info("%s objects ingested...", green(str(i + 1)))
|
|
76
|
+
for item in self._items:
|
|
77
|
+
try:
|
|
78
|
+
item.test(obj)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
self._logger.warning(yellow(f"GMD analysis item '{item.name}' failed: {e}"))
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
for item in self._items:
|
|
84
|
+
try:
|
|
85
|
+
item.finalize_analysis()
|
|
86
|
+
except Exception as e:
|
|
87
|
+
self._logger.warning(yellow(f"GMD analysis item '{item.name}' finalization failed: {e}"))
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
def _render_markdown(self, output: TextIO) -> None:
|
|
91
|
+
output.write("# getMongoData Analysis Report\n")
|
|
92
|
+
output.write(f"Generated at: `{str(datetime.now(tz=timezone.utc))} UTC`\n\n")
|
|
93
|
+
output.write(f"File path: `{self._file_path}`\n\n")
|
|
94
|
+
output.write("## 1 Review Test Results\n\n")
|
|
95
|
+
output.write("### Overview\n\n")
|
|
96
|
+
# Enrich all test results with matched risks before building summary
|
|
97
|
+
risk_available = False
|
|
98
|
+
try:
|
|
99
|
+
from mongo_x_ray_risk import enrich_test_results, has_risks
|
|
100
|
+
|
|
101
|
+
risk_available = has_risks()
|
|
102
|
+
for item in self._items:
|
|
103
|
+
enrich_test_results(item._test_result)
|
|
104
|
+
except Exception:
|
|
105
|
+
self._logger.debug("Risk register matching not available", exc_info=True)
|
|
106
|
+
summary_item = SummaryItem(risk_available=risk_available)
|
|
107
|
+
summary_item.summarize(self._items)
|
|
108
|
+
summary_item.overview(output)
|
|
109
|
+
for i, item in enumerate(self._items):
|
|
110
|
+
if item._in_complete_flag:
|
|
111
|
+
self._logger.warning(
|
|
112
|
+
yellow(f"GMD item '{item.name}' is incomplete because of too many databases/collections.")
|
|
113
|
+
)
|
|
114
|
+
try:
|
|
115
|
+
title = f"1.{i + 1} {item.name}"
|
|
116
|
+
review_title = f"2.{i + 1} Review {item.name}"
|
|
117
|
+
review_title_id = str_to_md_id(review_title)
|
|
118
|
+
output.write(f"### {title}\n\n")
|
|
119
|
+
output.write(f"{item.description}\n\n")
|
|
120
|
+
output.write(f"[Review Raw Results →](#{review_title_id})\n\n")
|
|
121
|
+
item.test_result_markdown(output)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
self._logger.warning(yellow(f"Failed to generate markdown for GMD item '{item.name}': {e}"))
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
output.write("## 2 Review Raw Results\n\n")
|
|
127
|
+
for i, item in enumerate(self._items):
|
|
128
|
+
try:
|
|
129
|
+
title = f"1.{i + 1} {item.name}"
|
|
130
|
+
title_id = str_to_md_id(title)
|
|
131
|
+
review_title = f"2.{i + 1} Review {item.name}"
|
|
132
|
+
output.write(f"### {review_title}\n\n")
|
|
133
|
+
output.write(f"[← Review Test Results](#{title_id})\n\n")
|
|
134
|
+
item.review_results_markdown(output)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
self._logger.warning(yellow(f"Failed to generate review markdown for GMD item '{item.name}': {e}"))
|
|
137
|
+
continue
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026 MongoDB Inc.
|
|
3
|
+
|
|
4
|
+
DISCLAIMER: THESE CODE SAMPLES ARE PROVIDED FOR EDUCATIONAL AND ILLUSTRATIVE PURPOSES ONLY,
|
|
5
|
+
TO DEMONSTRATE THE FUNCTIONALITY OF SPECIFIC MONGODB FEATURES.
|
|
6
|
+
THEY ARE NOT PRODUCTION-READY AND MAY LACK THE SECURITY HARDENING, ERROR HANDLING, AND TESTING REQUIRED FOR A LIVE ENVIRONMENT.
|
|
7
|
+
YOU ARE RESPONSIBLE FOR TESTING, VALIDATING, AND SECURING THIS CODE WITHIN YOUR OWN ENVIRONMENT BEFORE IMPLEMENTATION.
|
|
8
|
+
THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OR LIABILITY.
|
|
9
|
+
|
|
10
|
+
Package for GMD analysis items.
|
|
11
|
+
"""
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026 MongoDB Inc.
|
|
3
|
+
|
|
4
|
+
DISCLAIMER: THESE CODE SAMPLES ARE PROVIDED FOR EDUCATIONAL AND ILLUSTRATIVE PURPOSES ONLY,
|
|
5
|
+
TO DEMONSTRATE THE FUNCTIONALITY OF SPECIFIC MONGODB FEATURES.
|
|
6
|
+
THEY ARE NOT PRODUCTION-READY AND MAY LACK THE SECURITY HARDENING, ERROR HANDLING, AND TESTING REQUIRED FOR A LIVE ENVIRONMENT.
|
|
7
|
+
YOU ARE RESPONSIBLE FOR TESTING, VALIDATING, AND SECURING THIS CODE WITHIN YOUR OWN ENVIRONMENT BEFORE IMPLEMENTATION.
|
|
8
|
+
THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OR LIABILITY.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import html as html_mod
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
from abc import abstractmethod
|
|
15
|
+
from typing import Callable, Optional
|
|
16
|
+
|
|
17
|
+
from bson import json_util
|
|
18
|
+
|
|
19
|
+
from mongo_x_ray.shared import SEVERITY
|
|
20
|
+
from mongo_x_ray.utils import bold, to_ejson, yellow
|
|
21
|
+
from mongo_x_ray.version import Version
|
|
22
|
+
from mongo_x_ray_gmd.shared import GmdEvents
|
|
23
|
+
from mongo_x_ray_hc.check_items.base_item import colorize_severity
|
|
24
|
+
from mongo_x_ray_hc.rules.base_rule import BaseRule
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BaseItem:
|
|
28
|
+
def __init__(self, output_folder: str, config, **_kwargs) -> None:
|
|
29
|
+
self.config: dict = config
|
|
30
|
+
self._output_file = os.path.join(output_folder, f"{self.__class__.__name__}.json")
|
|
31
|
+
self._logger = logging.getLogger(__name__)
|
|
32
|
+
self._server_version: Optional[Version] = None
|
|
33
|
+
self._hostname: Optional[str] = None
|
|
34
|
+
self._set_name: Optional[str] = None
|
|
35
|
+
self._cluster_type: Optional[str] = None
|
|
36
|
+
self._test_result: list = []
|
|
37
|
+
self._rules: dict[str, BaseRule] = {}
|
|
38
|
+
if os.path.isfile(self._output_file):
|
|
39
|
+
os.remove(self._output_file)
|
|
40
|
+
|
|
41
|
+
self._in_complete_flag = False
|
|
42
|
+
# Subscribe some common events that most items care about
|
|
43
|
+
self._cache = None
|
|
44
|
+
self._watched_events: dict[GmdEvents, list[Callable]] = {}
|
|
45
|
+
self._watched_all_events: list[tuple[set[GmdEvents], Callable]] = []
|
|
46
|
+
self._fired_events: set[GmdEvents] = set()
|
|
47
|
+
|
|
48
|
+
def get_version(block):
|
|
49
|
+
self._server_version = Version.parse(block.get("output", {}).get("version", ""))
|
|
50
|
+
|
|
51
|
+
def get_host(block):
|
|
52
|
+
if self._hostname is None:
|
|
53
|
+
self._hostname = block.get("output", {}).get("system", {}).get("hostname", "unknown")
|
|
54
|
+
|
|
55
|
+
def get_cluster_type(block):
|
|
56
|
+
output = block.get("output", {})
|
|
57
|
+
set_name = output.get("setName", None)
|
|
58
|
+
msg = output.get("msg", "")
|
|
59
|
+
if set_name is not None:
|
|
60
|
+
self._cluster_type = "RS"
|
|
61
|
+
self._set_name = set_name
|
|
62
|
+
elif msg == "isdbgrid":
|
|
63
|
+
self._cluster_type = "SH"
|
|
64
|
+
self._set_name = "mongos"
|
|
65
|
+
else:
|
|
66
|
+
self._cluster_type = "STANDALONE"
|
|
67
|
+
|
|
68
|
+
# Use hostname in ismaster whenever possible
|
|
69
|
+
if "me" in output:
|
|
70
|
+
self._hostname = output["me"]
|
|
71
|
+
|
|
72
|
+
self.watch_one(GmdEvents.SERVER_BUILD_INFO, get_version)
|
|
73
|
+
self.watch_one(GmdEvents.HOST_INFO, get_host)
|
|
74
|
+
self.watch_one(GmdEvents.ISMASTER, get_cluster_type)
|
|
75
|
+
|
|
76
|
+
def test(self, block) -> None:
|
|
77
|
+
sub_sec = block.get("subsection", "")
|
|
78
|
+
sub_sec = sub_sec.replace("INCOMPLETE_", "")
|
|
79
|
+
try:
|
|
80
|
+
current_event = GmdEvents(sub_sec)
|
|
81
|
+
except ValueError:
|
|
82
|
+
current_event = GmdEvents.UNKNOWN
|
|
83
|
+
# Fire subscribed single events
|
|
84
|
+
for event, funcs in self._watched_events.items():
|
|
85
|
+
if current_event == event:
|
|
86
|
+
if block.get("subsection", "").startswith("INCOMPLETE_"):
|
|
87
|
+
self._in_complete_flag = True
|
|
88
|
+
for func in funcs:
|
|
89
|
+
try:
|
|
90
|
+
func(block)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
self._logger.warning(yellow("Error in subscribed function for event %s: %s"), event.value, e)
|
|
93
|
+
self._fired_events.add(event)
|
|
94
|
+
|
|
95
|
+
# Fire subscribed all events
|
|
96
|
+
for events, func in self._watched_all_events:
|
|
97
|
+
if events.issubset(self._fired_events) and current_event in events:
|
|
98
|
+
try:
|
|
99
|
+
func()
|
|
100
|
+
except Exception as e:
|
|
101
|
+
self._logger.warning("Error in subscribed all-events function for events %s: %s", events, e)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def name(self) -> str:
|
|
105
|
+
return self._name
|
|
106
|
+
|
|
107
|
+
@name.setter
|
|
108
|
+
def name(self, value: str) -> None:
|
|
109
|
+
self._name = value
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def description(self) -> str:
|
|
113
|
+
desc: str = ""
|
|
114
|
+
for rule in self._rules.values():
|
|
115
|
+
desc += rule.description_md + "\n"
|
|
116
|
+
return desc
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def test_result(self) -> list:
|
|
120
|
+
return self._test_result
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def captured_sample(self) -> Optional[dict]:
|
|
124
|
+
try:
|
|
125
|
+
with open(self._output_file, "r", encoding="utf-8") as f:
|
|
126
|
+
return json_util.loads(f.read())
|
|
127
|
+
except FileNotFoundError:
|
|
128
|
+
self._logger.warning(
|
|
129
|
+
"Captured sample file not found: %s. This is probably because the getMongoData output is incomplete.",
|
|
130
|
+
bold(self._output_file),
|
|
131
|
+
)
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
@captured_sample.setter
|
|
135
|
+
def captured_sample(self, data: Optional[dict]) -> None:
|
|
136
|
+
with open(self._output_file, "w", encoding="utf-8") as f:
|
|
137
|
+
f.write(to_ejson(data, indent=None))
|
|
138
|
+
|
|
139
|
+
def test_result_markdown(self, output) -> None:
|
|
140
|
+
if len(self._test_result) == 0:
|
|
141
|
+
output.write("<b style='color: green;'>Pass.</b>\n\n")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
output.write(
|
|
145
|
+
'| <span data-sortable="false">\\#</span>{60px}'
|
|
146
|
+
' | <span data-sortable="true">Host</span>{180px}'
|
|
147
|
+
' | <span data-sortable="true">Severity</span>{120px}'
|
|
148
|
+
' | <span data-sortable="true">Category</span>{200px}'
|
|
149
|
+
' | <span data-sortable="false">Message</span>{*} |\n'
|
|
150
|
+
)
|
|
151
|
+
output.write("|:----------:|:----------:|:----------:|---------|---------|\n")
|
|
152
|
+
for idx, item in enumerate(self._test_result):
|
|
153
|
+
severity = item["severity"]
|
|
154
|
+
severity_cell = (
|
|
155
|
+
f'<span data-sort-value="{severity.value}">'
|
|
156
|
+
f"<b style='color: {colorize_severity(severity)}'>"
|
|
157
|
+
f" {severity.name} </b></span>"
|
|
158
|
+
)
|
|
159
|
+
category_cell = item["title"]
|
|
160
|
+
risk = item.get("matched_risk")
|
|
161
|
+
if risk:
|
|
162
|
+
risk_id = html_mod.escape(str(risk.get("id", "")))
|
|
163
|
+
risk_name = html_mod.escape(str(risk.get("name", ""))).replace("\r\n", "<br>").replace("\n", "<br>")
|
|
164
|
+
risk_desc = (
|
|
165
|
+
html_mod.escape(str(risk.get("description", ""))).replace("\r\n", "<br>").replace("\n", "<br>")
|
|
166
|
+
)
|
|
167
|
+
category_cell += (
|
|
168
|
+
f' <span class="risk-badge">RISK-{risk_id}'
|
|
169
|
+
f'<span class="risk-tooltip">'
|
|
170
|
+
f'<span class="risk-name">{risk_name}</span>'
|
|
171
|
+
f"{risk_desc}"
|
|
172
|
+
f"</span></span>"
|
|
173
|
+
)
|
|
174
|
+
output.write(
|
|
175
|
+
f"| **{idx + 1}** | `{item['host']}` | {severity_cell} | {category_cell} | {item['message']} |\n"
|
|
176
|
+
)
|
|
177
|
+
output.write("\n")
|
|
178
|
+
|
|
179
|
+
def finalize_analysis(self) -> None:
|
|
180
|
+
"""
|
|
181
|
+
This function will be called after all GMD data is ingested and before generating the report.
|
|
182
|
+
You can do some final analysis here that requires all data to be available.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
@abstractmethod
|
|
186
|
+
def review_results_markdown(self, output) -> None:
|
|
187
|
+
raise NotImplementedError("Subclasses should implement this method.")
|
|
188
|
+
|
|
189
|
+
def append_test_result(self, host: str, severity: SEVERITY, title: str, message: str) -> None:
|
|
190
|
+
self._test_result.append({"host": host, "severity": severity, "title": title, "message": message})
|
|
191
|
+
|
|
192
|
+
def append_test_results(self, items: list) -> None:
|
|
193
|
+
for item in items:
|
|
194
|
+
self.append_test_result(item["host"], item["severity"], item["title"], item["description"])
|
|
195
|
+
|
|
196
|
+
def watch_one(self, event: GmdEvents, func) -> None:
|
|
197
|
+
"""
|
|
198
|
+
Fires when the specified event occurs.
|
|
199
|
+
The order of `watch_one` depends on the order of events in the GMD log, not the order of `watch_one` calls.
|
|
200
|
+
"""
|
|
201
|
+
if event not in self._watched_events:
|
|
202
|
+
self._watched_events[event] = []
|
|
203
|
+
self._watched_events[event].append(func)
|
|
204
|
+
|
|
205
|
+
def watch_all(self, events: set[GmdEvents], func) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Fires when all the specified events occured.
|
|
208
|
+
`watch_all` fires after the last `watch_one` fires.
|
|
209
|
+
"""
|
|
210
|
+
self._watched_all_events.append((events, func))
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def all_events_fired(self) -> bool:
|
|
214
|
+
"""
|
|
215
|
+
Check if all watched events have been fired.
|
|
216
|
+
If not all events are fired, some data may be missing in the GMD.
|
|
217
|
+
"""
|
|
218
|
+
return all(event in self._fired_events for event in self._watched_events)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2026 MongoDB Inc.
|
|
3
|
+
|
|
4
|
+
DISCLAIMER: THESE CODE SAMPLES ARE PROVIDED FOR EDUCATIONAL AND ILLUSTRATIVE PURPOSES ONLY,
|
|
5
|
+
TO DEMONSTRATE THE FUNCTIONALITY OF SPECIFIC MONGODB FEATURES.
|
|
6
|
+
THEY ARE NOT PRODUCTION-READY AND MAY LACK THE SECURITY HARDENING, ERROR HANDLING, AND TESTING REQUIRED FOR A LIVE ENVIRONMENT.
|
|
7
|
+
YOU ARE RESPONSIBLE FOR TESTING, VALIDATING, AND SECURING THIS CODE WITHIN YOUR OWN ENVIRONMENT BEFORE IMPLEMENTATION.
|
|
8
|
+
THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OR LIABILITY.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from mongo_x_ray.parsers.base_parser import BaseParser
|
|
14
|
+
from mongo_x_ray_gmd.gmd_items.base_item import BaseItem
|
|
15
|
+
from mongo_x_ray_gmd.shared import GmdEvents
|
|
16
|
+
from mongo_x_ray_hc.parsers.build_info_parser import BuildInfoParser
|
|
17
|
+
from mongo_x_ray_hc.rules.version_eol_rule import VersionEOLRule
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BuildInfoItem(BaseItem):
|
|
21
|
+
def __init__(self, output_folder: str, config, **kwargs):
|
|
22
|
+
super().__init__(output_folder, config, **kwargs)
|
|
23
|
+
self.name: str = "Build Information"
|
|
24
|
+
self._build_info: dict[str, Any] = {}
|
|
25
|
+
self._rules["version_eol"] = VersionEOLRule(config)
|
|
26
|
+
|
|
27
|
+
def get_build_info(block):
|
|
28
|
+
self._build_info = block.get("output", {})
|
|
29
|
+
|
|
30
|
+
def process_build_info():
|
|
31
|
+
test_result, _ = self._rules["version_eol"].apply(self._build_info, extra_info={"host": self._hostname})
|
|
32
|
+
self.append_test_results(test_result)
|
|
33
|
+
# self.captured_sample = self._build_info
|
|
34
|
+
|
|
35
|
+
self.watch_one(GmdEvents.SERVER_BUILD_INFO, get_build_info)
|
|
36
|
+
self.watch_all({GmdEvents.SERVER_BUILD_INFO, GmdEvents.HOST_INFO}, process_build_info)
|
|
37
|
+
|
|
38
|
+
def review_results_markdown(self, output) -> None:
|
|
39
|
+
data = self._build_info
|
|
40
|
+
assert data is not None, f"GMD subsection {GmdEvents.SERVER_BUILD_INFO.value} should be available for review."
|
|
41
|
+
parser: BaseParser = BuildInfoParser()
|
|
42
|
+
parsed_output = parser.markdown([(self._set_name, self._hostname, data)], caller=self.__class__.__name__)
|
|
43
|
+
output.write(parsed_output)
|