Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4b72b85
Initial Fabric Module
mikewiebe Jul 2, 2025
f3b192d
Updates to fabric module
mikewiebe Jul 15, 2025
7b03c49
Update docstrings for GetHave() object
mikewiebe Jul 15, 2025
e5601b1
Refactoring
mikewiebe Jul 15, 2025
1c6486c
Refactoring
mikewiebe Jul 16, 2025
97afe9c
Add docs
mikewiebe Jul 16, 2025
e0e4420
Remove docstrings for init methods
mikewiebe Jul 16, 2025
fd9dcb8
add docstrings
mikewiebe Jul 16, 2025
76bdd0d
More docstrings
mikewiebe Jul 16, 2025
2c38a6c
Add dependency injection
mikewiebe Jul 16, 2025
9a2dde9
Add unit tests
mikewiebe Jul 16, 2025
2415a5c
Update module docs and run black
mikewiebe Jul 18, 2025
ed609b3
Move fabric module under modules
mikewiebe Jul 18, 2025
88b3e65
Fix unit tests
mikewiebe Jul 18, 2025
6634d61
Refactored based on review
mikewiebe Jul 18, 2025
79b6fe5
Fix ansible sanity unused import
mikewiebe Jul 18, 2025
9c69687
Fix import errors from ansible sanity
mikewiebe Jul 19, 2025
805a86d
Workaround false positive unidiomatic-typecheck
mikewiebe Jul 19, 2025
91200ad
Module only supported on python version > 3.9
mikewiebe Jul 19, 2025
5cf7f77
Fix black formatting issues
mikewiebe Jul 19, 2025
157ac2b
Skip invalid sanity checks
mikewiebe Jul 21, 2025
bf19af4
Remove skip validate-module tests
mikewiebe Jul 21, 2025
1e28357
Fix unidiomatic-typecheck warning
mikewiebe Jul 21, 2025
0b39d08
Use task instead of playbook
mikewiebe Jul 21, 2025
06fe9f6
Add common merge and replace pydantic model functions
mikewiebe Jul 21, 2025
c95e4ac
Use snake_case and request instead of payload
mikewiebe Jul 21, 2025
29422b5
Add support for more management properties
mikewiebe Jul 23, 2025
3336f1d
Add enums based on openapi spec
mikewiebe Jul 23, 2025
b808729
Update docs and examples
mikewiebe Jul 23, 2025
7ec2cfa
Merge branch 'master' into fabric_module
mikewiebe Aug 6, 2025
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
369 changes: 369 additions & 0 deletions plugins/module_utils/common/log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,369 @@
from __future__ import absolute_import, division, print_function

__metaclass__ = type
__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates."
__author__ = "Allen Robel"

import inspect
import json
import logging
from logging.config import dictConfig
from os import environ


class Log:
"""
### Summary
Create the base dcnm logging object.

### Raises
- ``ValueError`` if:
- An error is encountered reading the logging config file.
- An error is encountered parsing the logging config file.
- An invalid handler is found in the logging config file.
- Valid handlers are listed in self.valid_handlers,
which currently contains: "file".
- No formatters are found in the logging config file that
are associated with the configured handlers.
- ``TypeError`` if:
- ``develop`` is not a boolean.

### Usage

By default, Log() does the following:

1. Reads the environment variable ``NDFC_LOGGING_CONFIG`` to determine
the path to the logging config file. If the environment variable is
not set, then logging is disabled.
2. Sets ``develop`` to False. This disables exceptions raised by the
logging module itself.

Hence, the simplest usage for Log() is:

- Set the environment variable ``NDFC_LOGGING_CONFIG`` to the
path of the logging config file. ``bash`` shell is used in the
example below.

```bash
export NDFC_LOGGING_CONFIG="/path/to/logging_config.json"
```

- Instantiate a Log() object instance and call ``commit()`` on the instance:

```python
from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log
try:
log = Log()
log.commit()
except ValueError as error:
# handle error
```

To later disable logging, unset the environment variable.
``bash`` shell is used in the example below.

```bash
unset NDFC_LOGGING_CONFIG
```

To enable exceptions from the logging module (not recommended, unless needed for
development), set ``develop`` to True:

```python
from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log
try:
log = Log()
log.develop = True
log.commit()
except ValueError as error:
# handle error
```

To directly set the path to the logging config file, overriding the
``NDFC_LOGGING_CONFIG`` environment variable, set the ``config``
property prior to calling ``commit()``:

```python
from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log
try:
log = Log()
log.config = "/path/to/logging_config.json"
log.commit()
except ValueError as error:
# handle error
```

At this point, a base/parent logger is created for which all other
loggers throughout the dcnm collection will be children.
This allows for a single logging config to be used for all modules in the
collection, and allows for the logging config to be specified in a
single place external to the code.

### Example module code using the Log() object

In the main() function of a module.
```python
from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log

def main():
try:
log = Log()
log.commit()
except ValueError as error:
ansible_module.fail_json(msg=str(error))

task = AnsibleTask()
```

In the AnsibleTask() class (or any other classes running in the
main() function's call stack i.e. classes instantiated in either
main() or in AnsibleTask()).

```python
class AnsibleTask:
def __init__(self):
self.class_name = self.__class__.__name__
self.log = logging.getLogger(f"dcnm.{self.class_name}")
def some_method(self):
self.log.debug("This is a debug message.")
```

### Logging Config File
The logging config file MUST conform to ``logging.config.dictConfig``
from Python's standard library and MUST NOT contain any handlers or
that log to stdout or stderr. The logging config file MUST only
contain handlers that log to files.

An example logging config file is shown below:

```json
{
"version": 1,
"formatters": {
"standard": {
"class": "logging.Formatter",
"format": "%(asctime)s - %(levelname)s - [%(name)s.%(funcName)s.%(lineno)d] %(message)s"
}
},
"handlers": {
"file": {
"class": "logging.handlers.RotatingFileHandler",
"formatter": "standard",
"level": "DEBUG",
"filename": "/tmp/dcnm.log",
"mode": "a",
"encoding": "utf-8",
"maxBytes": 50000000,
"backupCount": 4
}
},
"loggers": {
"dcnm": {
"handlers": [
"file"
],
"level": "DEBUG",
"propagate": false
}
},
"root": {
"level": "INFO",
"handlers": [
"file"
]
}
}
```
"""

def __init__(self):
self.class_name = self.__class__.__name__
# Disable exceptions raised by the logging module.
# Set this to True during development to catch logging errors.
logging.raiseExceptions = False

self.valid_handlers = set()
self.valid_handlers.add("file")

self._build_properties()

def _build_properties(self) -> None:
self.properties = {}
self.properties["config"] = environ.get("NDFC_LOGGING_CONFIG", None)
self.properties["develop"] = False

def disable_logging(self):
"""
### Summary
- Disable logging by removing all handlers from the base logger.

### Raises
None
"""
logger = logging.getLogger()
for handler in logger.handlers.copy():
try:
logger.removeHandler(handler)
except ValueError: # if handler already removed
pass
logger.addHandler(logging.NullHandler())
logger.propagate = False

def enable_logging(self):
"""
### Summary
- Enable logging by reading the logging config file and configuring
the base logger instance.
### Raises
- ``ValueError`` if:
- An error is encountered reading the logging config file.
"""
if str(self.config).strip() == "":
return

try:
with open(self.config, "r", encoding="utf-8") as file:
try:
logging_config = json.load(file)
except json.JSONDecodeError as error:
msg = f"error parsing logging config from {self.config}. "
msg += f"Error detail: {error}"
raise ValueError(msg) from error
except IOError as error:
msg = f"error reading logging config from {self.config}. "
msg += f"Error detail: {error}"
raise ValueError(msg) from error

try:
self.validate_logging_config(logging_config)
except ValueError as error:
raise ValueError(str(error)) from error

try:
dictConfig(logging_config)
except (RuntimeError, TypeError, ValueError) as error:
msg = "logging.config.dictConfig: "
msg += f"Unable to configure logging from {self.config}. "
msg += f"Error detail: {error}"
raise ValueError(msg) from error

def validate_logging_config(self, logging_config: dict) -> None:
"""
### Summary
- Validate the logging config file.
- Ensure that the logging config file does not contain any handlers
that log to console, stdout, or stderr.

### Raises
- ``ValueError`` if:
- The logging config file contains no handlers.
- The logging config file contains a handler other than
the handlers listed in self.valid_handlers (see class
docstring).

### Usage
```python
log = Log()
log.config = "/path/to/logging_config.json"
log.commit()
```
"""
if len(logging_config.get("handlers", {})) == 0:
msg = "logging.config.dictConfig: "
msg += "No file handlers found. "
msg += "Add a file handler to the logging config file "
msg += f"and try again: {self.config}"
raise ValueError(msg)
bad_handlers = []
for handler in logging_config.get("handlers", {}):
if handler not in self.valid_handlers:
msg = "logging.config.dictConfig: "
msg += "handlers found that may interrupt Ansible module "
msg += "execution. "
msg += "Remove these handlers from the logging config file "
msg += "and try again. "
bad_handlers.append(handler)
if len(bad_handlers) > 0:
msg += f"Handlers: {','.join(bad_handlers)}. "
msg += f"Logging config file: {self.config}."
raise ValueError(msg)

def commit(self):
"""
### Summary
- If ``config`` is None, disable logging.
- If ``config`` is a JSON file conformant with
``logging.config.dictConfig``, read the file and configure the
base logger instance from the file's contents.

### Raises
- ``ValueError`` if:
- An error is encountered reading the logging config file.

### Notes
1. If self.config is None, then logging is disabled.
2. If self.config is a path to a JSON file, then the file is read
and logging is configured from the file.

### Usage
```python
log = Log()
log.config = "/path/to/logging_config.json"
log.commit()
```
"""
if self.config is None:
self.disable_logging()
else:
self.enable_logging()

@property
def config(self):
"""
### Summary
Path to a JSON file from which logging config is read.
JSON file must conform to ``logging.config.dictConfig`` from Python's
standard library.

### Default
If the environment variable ``NDFC_LOGGING_CONFIG`` is set, then
the value of that variable is used. Otherwise, None.

The environment variable can be overridden by directly setting
``config`` to one of the following prior to calling ``commit()``:

1. None. Logging will be disabled.
2. Path to a JSON file from which logging config is read.
Must conform to ``logging.config.dictConfig`` from Python's
standard library.
"""
return self.properties["config"]

@config.setter
def config(self, value):
self.properties["config"] = value

@property
def develop(self):
"""
### Summary
Disable or enable exceptions raised by the logging module.

### Default
False

### Valid Values
- ``True``: Exceptions will be raised by the logging module.
- ``False``: Exceptions will not be raised by the logging module.
"""
return self.properties["develop"]

@develop.setter
def develop(self, value):
method_name = inspect.stack()[0][3]
if not isinstance(value, bool):
msg = f"{self.class_name}.{method_name}: Expected boolean for develop. "
msg += f"Got: type {type(value).__name__} for value {value}."
raise TypeError(msg)
self.properties["develop"] = value
logging.raiseExceptions = value
Loading
Loading