Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(anta): Added merge_catalogs function #802

Merged
merged 5 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion anta/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import math
from collections import defaultdict
from inspect import isclass
from itertools import chain
from json import load as json_load
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
from warnings import warn

from pydantic import BaseModel, ConfigDict, RootModel, ValidationError, ValidationInfo, field_validator, model_serializer, model_validator
from pydantic.types import ImportString
Expand All @@ -34,6 +36,21 @@
ListAntaTestTuples = list[tuple[type[AntaTest], Optional[Union[AntaTest.Input, dict[str, Any]]]]]


def merge_catalogs(catalogs: list[AntaCatalog]) -> AntaCatalog:
gmuloc marked this conversation as resolved.
Show resolved Hide resolved
"""Merge multiple AntaCatalog instances.

Parameters
----------
catalogs: A list of AntaCatalog instances to merge.

Returns
-------
A new AntaCatalog instance containing the tests of all the input catalogs.
"""
combined_tests = list(chain(*(catalog.tests for catalog in catalogs)))
return AntaCatalog(tests=combined_tests)


class AntaTestDefinition(BaseModel):
"""Define a test with its associated inputs.

Expand Down Expand Up @@ -397,7 +414,13 @@ def merge(self, catalog: AntaCatalog) -> AntaCatalog:
-------
A new AntaCatalog instance containing the tests of the two instances.
"""
return AntaCatalog(tests=self.tests + catalog.tests)
# TODO: Use a decorator to deprecate this method instead. See https://github.com/aristanetworks/anta/issues/754
warn(
message="AntaCatalog.merge() is deprecated and will be removed in ANTA v2.0. Use merge_catalogs() from the anta.catalog module instead.",
category=DeprecationWarning,
stacklevel=2,
)
return merge_catalogs([self, catalog])

def dump(self) -> AntaCatalogFile:
"""Return an AntaCatalogFile instance from this AntaCatalog instance.
Expand Down
42 changes: 39 additions & 3 deletions docs/usage-inventory-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,45 @@ Once you run `anta nrfu table`, you will see following output:
└───────────┴────────────────────────────┴─────────────┴────────────┴───────────────────────────────────────────────┴───────────────┘
```

### Example script to merge catalogs
### Merging Catalogs

The following script reads all the files in `intended/test_catalogs/` with names `<device_name>-catalog.yml` and merge them together inside one big catalog `anta-catalog.yml`.
**Example script to merge catalogs using the new `merge_catalogs` function**

The following script reads all the files in `intended/test_catalogs/` with names `<device_name>-catalog.yml` and merge them together inside one big catalog `anta-catalog.yml` using the new `merge_catalogs` function.

```python
#!/usr/bin/env python
from anta.catalog import AntaCatalog, merge_catalogs

from pathlib import Path
from anta.models import AntaTest


CATALOG_SUFFIX = "-catalog.yml"
CATALOG_DIR = "intended/test_catalogs/"

if __name__ == "__main__":
catalogs = []
for file in Path(CATALOG_DIR).glob("*" + CATALOG_SUFFIX):
device = str(file).removesuffix(CATALOG_SUFFIX).removeprefix(CATALOG_DIR)
print(f"Loading test catalog for device {device}")
catalog = AntaCatalog.parse(file)
# Add the device name as a tag to all tests in the catalog
for test in catalog.tests:
test.inputs.filters = AntaTest.Input.Filters(tags={device})
catalogs.append(catalog)

# Merge all catalogs
merged_catalog = merge_catalogs(catalogs)

# Save the merged catalog to a file
with open(Path('anta-catalog.yml'), "w") as f:
f.write(catalog.dump().yaml())
```
!!! warning
The `AntaCatalog.merge()` method is deprecated and will be removed in ANTA v2.0. Please use the `merge_catalogs()` function from the `anta.catalog` module instead.

**For reference: Deprecated method (to be removed in ANTA v2.0)**
gmuloc marked this conversation as resolved.
Show resolved Hide resolved

```python
#!/usr/bin/env python
Expand All @@ -331,7 +367,7 @@ if __name__ == "__main__":
# Apply filters to all tests for this device
for test in c.tests:
test.inputs.filters = AntaTest.Input.Filters(tags=[device])
catalog = catalog.merge(c)
catalog = catalog.merge(c) # This line uses the deprecated method
with open(Path('anta-catalog.yml'), "w") as f:
f.write(catalog.dump().yaml())
```
22 changes: 19 additions & 3 deletions tests/units/test_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from pydantic import ValidationError
from yaml import safe_load

from anta.catalog import AntaCatalog, AntaCatalogFile, AntaTestDefinition
from anta.catalog import AntaCatalog, AntaCatalogFile, AntaTestDefinition, merge_catalogs
from anta.models import AntaTest
from anta.tests.interfaces import VerifyL3MTU
from anta.tests.mlag import VerifyMlagStatus
Expand Down Expand Up @@ -200,6 +200,18 @@
]


def test_merge_catalogs() -> None:
"""Test the merge_catalogs function."""
# Load catalogs of different sizes
small_catalog = AntaCatalog.parse(DATA_DIR / "test_catalog.yml")
medium_catalog = AntaCatalog.parse(DATA_DIR / "test_catalog_medium.yml")
tagged_catalog = AntaCatalog.parse(DATA_DIR / "test_catalog_with_tags.yml")

# Merge the catalogs and check the number of tests
final_catalog = merge_catalogs([small_catalog, medium_catalog, tagged_catalog])
assert len(final_catalog.tests) == 240
gmuloc marked this conversation as resolved.
Show resolved Hide resolved


class TestAntaCatalog:
"""Test for anta.catalog.AntaCatalog."""

Expand Down Expand Up @@ -354,11 +366,15 @@ def test_merge(self) -> None:
catalog3: AntaCatalog = AntaCatalog.parse(DATA_DIR / "test_catalog_medium.yml")
assert len(catalog3.tests) == 228

assert len(catalog1.merge(catalog2).tests) == 2
with pytest.deprecated_call():
merged_catalog = catalog1.merge(catalog2)
assert len(merged_catalog.tests) == 2
assert len(catalog1.tests) == 1
assert len(catalog2.tests) == 1

assert len(catalog2.merge(catalog3).tests) == 229
with pytest.deprecated_call():
merged_catalog = catalog2.merge(catalog3)
assert len(merged_catalog.tests) == 229
assert len(catalog2.tests) == 1
assert len(catalog3.tests) == 228

Expand Down
Loading