diff --git a/plugins/module_utils/bootflash/bootflash_files.py b/plugins/module_utils/bootflash/bootflash_files.py index 31c279866..6143a48e4 100644 --- a/plugins/module_utils/bootflash/bootflash_files.py +++ b/plugins/module_utils/bootflash/bootflash_files.py @@ -11,6 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +""" +Delete files from bootflash devices. +""" from __future__ import absolute_import, division, print_function __metaclass__ = type # pylint: disable=invalid-name @@ -30,25 +33,30 @@ class BootflashFiles: """ - ### Summary + # Summary + Delete files from bootflash devices. - ### Raises - - ``ValueError`` if: - - ``rest_send`` is not set before calling commit() - - ``results`` is not set before calling commit() - - ``switch_details`` is not set before calling commit() - - payload.deleteFiles is empty when calling commit() - - ``filename`` is not set before calling add_file() - - ``filepath`` is not set before calling add_file() - - ``ip_address`` is not set before calling add_file() - - ``supervisor`` is not set before calling add_file() - - ``switch_details`` is not set before calling add_file() - - ``TypeError`` if: - - ``switch_details`` is not an instance of ``SwitchDetails``. - - ip_address to serial_number conversion fails. - - ### Usage + ## Raises + + ### ValueError + + - `rest_send` is not set before calling commit() + - `results` is not set before calling commit() + - `switch_details` is not set before calling commit() + - payload.deleteFiles is empty when calling commit() + - `filename` is not set before calling add_file() + - `filepath` is not set before calling add_file() + - `ip_address` is not set before calling add_file() + - `supervisor` is not set before calling add_file() + - `switch_details` is not set before calling add_file() + + ### TypeError + + - `switch_details` is not an instance of `SwitchDetails`. + - ip_address to serial_number conversion fails. + + ## Usage ```python sender = Sender() @@ -84,7 +92,7 @@ class BootflashFiles: instance.commit() ``` - ### Payload Structure + ## Payload Structure The structure of the request body to delete bootflash files. @@ -146,13 +154,16 @@ def __init__(self) -> None: def refresh_switch_details(self) -> None: """ - ### Summary + # Summary + If switch details are not already refreshed, refresh them. - ### Raises - - ``ValueError`` if: - - ``switch_details`` is not set. - - ``rest_send`` is not set. + ## Raises + + ### ValueError + + - `switch_details` is not set. + - `rest_send` is not set. """ method_name: str = inspect.stack()[0][3] @@ -172,12 +183,15 @@ def refresh_switch_details(self) -> None: def ip_address_to_serial_number(self, ip_address: str) -> str: """ - ### Summary + # Summary + Convert ip_address to serial_number. - ### Raises - - ``ValueError`` if: - - switch_details is not set. + ## Raises + + ### ValueError + + - switch_details is not set. """ method_name: str = inspect.stack()[0][3] @@ -194,11 +208,13 @@ def ip_address_to_serial_number(self, ip_address: str) -> str: def ok_to_delete_files(self, ip_address: str) -> bool: """ - ### Summary - - Return True if files can be deleted on the switch with ip_address. - - Return False otherwise. + # Summary + + - Return True if files can be deleted on the switch with ip_address. + - Return False otherwise. + + ## Raises - ### Raises None """ self.refresh_switch_details() @@ -274,14 +290,17 @@ def raise_exception(property_name: str) -> None: def commit(self) -> None: """ - ### Summary + # Summary + Send the payload to delete files. - ### Raises - - ``ValueError`` if: - - Mandatory parameters are not set. + ## Raises + + ### ValueError + + - Mandatory parameters are not set. - ### Notes + ## Notes - pylint: disable=no-member is needed due to the results property being dynamically created by the @Properties.add_results decorator. """ @@ -296,10 +315,12 @@ def commit(self) -> None: def delete_files(self) -> None: """ - ### Summary + # Summary + Delete files that have been added with add_files(). - ### Raises + ## Raises + None """ # pylint: disable=no-member @@ -322,17 +343,20 @@ def delete_files(self) -> None: def validate_prerequisites_for_add_file(self) -> None: """ - ### Summary + # Summary + Verify that mandatory prerequisites are met before calling add_file() - ### Raises - - ``ValueError`` if: - - ``filename`` is not set. - - ``filepath`` is not set. - - ``ip_address`` is not set. - - ``supervisor`` is not set. - - ``switch_details`` is not set. - - ``target`` is not set. + ## Raises + + ### ValueError + + - `filename` is not set. + - `filepath` is not set. + - `ip_address` is not set. + - `supervisor` is not set. + - `switch_details` is not set. + - `target` is not set. """ method_name: str = inspect.stack()[0][3] @@ -356,40 +380,45 @@ def raise_exception(property_name: str) -> None: def partition_and_serial_number_exist_in_payload(self) -> bool: """ - ### Summary - - Return True if the partition and serialNumber associated with the - file exist in the payload. - - Return False otherwise. + # Summary + + - Return True if the partition and serialNumber associated with the file exist in the payload. + - Return False otherwise. + + ## Raises - ### Raises None - ### payload Structure + ## payload Structure - "deleteFiles": [ - { - "files": [ - { - "bootflashType": "active", - "fileName": "bar.txt", - "filePath": "bootflash:" - } - ], - "partition": "bootflash:", - "serialNumber": "FOX2109PGCS" - }, - { - "files": [ - { - "bootflashType": "active", - "fileName": "black.txt", - "filePath": "bootflash:" - } - ], - "partition": "bootflash:", - "serialNumber": "FOX2109PGD0" - } - ] + ```json + { + "deleteFiles": [ + { + "files": [ + { + "bootflashType": "active", + "fileName": "bar.txt", + "filePath": "bootflash:" + } + ], + "partition": "bootflash:", + "serialNumber": "FOX2109PGCS" + }, + { + "files": [ + { + "bootflashType": "active", + "fileName": "black.txt", + "filePath": "bootflash:" + } + ], + "partition": "bootflash:", + "serialNumber": "FOX2109PGD0" + } + ] + } + ``` """ found: bool = False for item in self.payload["deleteFiles"]: @@ -409,10 +438,8 @@ def add_file_to_existing_payload(self) -> None: Add a file to the payload if the following are true: - - The serialNumber and partition associated with the file exist in - the payload. - - The file does not already exist in the files list for that - serialNumber and partition. + - The serialNumber and partition associated with the file exist in the payload. + - The file does not already exist in the files list for that serialNumber and partition. ## Raises @@ -420,7 +447,7 @@ def add_file_to_existing_payload(self) -> None: ## Details - We are looking at the following structure. + self.payload consists of the following structure. ```json { @@ -475,11 +502,13 @@ def add_file_to_existing_payload(self) -> None: def add_file_to_payload(self) -> None: """ - ### Summary + # Summary + Add a file to the payload if the serialNumber and partition do not yet exist in the payload. - ### Raises + ## Raises + None """ if not self.partition_and_serial_number_exist_in_payload(): @@ -500,12 +529,16 @@ def add_file_to_payload(self) -> None: def add_file(self) -> None: """ - ### Summary + # Summary + Add a file to the payload. - ### Raises - - ``ValueError`` if: - - The switch does not allow file deletion. + ## Raises + + ### ValueError + + - The switch does not allow file deletion. + - Mandatory parameters are not set (see `validate_prerequisites_for_add_file()`). """ method_name: str = inspect.stack()[0][3] self.validate_prerequisites_for_add_file() @@ -521,17 +554,18 @@ def add_file(self) -> None: def update_diff(self) -> None: """ - ### Summary - Update ``diff`` with ``target``. + # Summary + + Update `diff` with `target`. + + ## Raises - ### Raises None - ### Notes - - ``target`` has already been validated to be set (not None) in - ``validate_prerequisites_for_add_file()``. - - ``target`` has already been validated to be a dictionary and to - contain ``ip_address`` in ``target.setter``. + ## Notes + + - `target` has already been validated to be set (not None) in `validate_prerequisites_for_add_file()`. + - `target` has already been validated to be a dictionary and to contain `ip_address` in `target.setter`. """ ip_address: str = self.target.get("ip_address", "") if ip_address not in self.diff: @@ -541,20 +575,24 @@ def update_diff(self) -> None: @property def filepath(self) -> str: """ - ### Summary - Return the current ``filepath``. + # Summary + + Return the current `filepath`. - ``filepath`` is the path to the file to be deleted. + `filepath` is the path to the file to be deleted. + + ## Raises - ### Raises None - ### Associated key - ``filePath`` + ## Associated key - ### Example values - - ``bootflash:`` - - ``bootflash:/mydir/mysubdir/`` + `filePath` + + ## Example values + + - `bootflash:` + - `bootflash:/mydir/mysubdir/` """ return self._filepath @@ -565,19 +603,22 @@ def filepath(self, value: str) -> None: @property def filename(self) -> str: """ - ### Summary - Return the current ``filename``. + # Summary - ``filename`` is the name of the file to be deleted. + Return the current `filename`. + + `filename` is the name of the file to be deleted. + + ## Raises - ### Raises None - ### Associated key - ``fileName`` + ## Associated key + `fileName` + + ## Example value - ### Example value - ``n9000-epld.10.2.5.M.img`` + `n9000-epld.10.2.5.M.img` """ return self._filename @@ -588,17 +629,21 @@ def filename(self, value: str) -> None: @property def ip_address(self) -> str: """ - ### Summary - The ip address of the switch on which ``filename`` resides. + # Summary + + The ip address of the switch on which `filename` resides. + + ## Raises - ### Raises None - ### Associated key - ``serialNumber`` (ip_address is converted to serialNumber) + ## Associated key + + `serialNumber` (ip_address is converted to serialNumber) - ### Example value - ``192.168.1.2`` + ## Example value + + `192.168.1.2` """ return self._ip_address @@ -609,17 +654,21 @@ def ip_address(self, value: str) -> None: @property def partition(self) -> str: """ - ### Summary - The partition on which ``filename`` resides. + # Summary + + The partition on which `filename` resides. + + ## Raises - ### Raises None - ### Associated key - ``partition`` + ## Associated key + + `partition` - ### Example value - ``bootflash:`` + ## Example value + + `bootflash:` """ return self._partition @@ -632,20 +681,18 @@ def rest_send(self) -> RestSend: """ # Summary - An instance of the RestSend class. + Set/get an instance of the RestSend class. ## Raises - - setter: `TypeError` if the value is not an instance of RestSend. - - setter: `ValueError` if RestSend.params is not set. + ### TypeError - ## getter + - setter: if the value is not an instance of RestSend. - Return an instance of the RestSend class. + ### ValueError - ## setter + - setter: if RestSend.params is not set. - Set an instance of the RestSend class. """ method_name: str = inspect.stack()[0][3] if not self._rest_send.params: @@ -676,19 +723,13 @@ def results(self) -> Results: """ # Summary - An instance of the Results class. + Set/get an instance of the Results class. ## Raises - - setter: `TypeError` if the value is not an instance of Results. - - ## getter - - Return an instance of the Results class. - - ## setter + ### TypeError - Set an instance of the Results class. + - setter: if the value is not an instance of Results. """ return self._results @@ -712,21 +753,24 @@ def results(self, value: Results) -> None: @property def supervisor(self) -> str: """ - ### Summary - Return the current ``supervisor``. + # Summary + + Return the current `supervisor`. - ``supervisor`` is the switch supervisor card (active or standby) - on which ``filename`` resides. + `supervisor` is the switch supervisor card (active or standby) on which `filename` resides. + + ## Raises - ### Raises None - ### Associated key - ``bootflashType`` + ## Associated key - ### Example values - - ``active`` - - ``standby`` + `bootflashType` + + ## Example values + + - `active` + - `standby` """ return self._supervisor @@ -737,12 +781,15 @@ def supervisor(self, value: str) -> None: @property def switch_details(self) -> SwitchDetails: """ - ### Summary + # Summary + An instance of the ``SwitchDetails()`` class. - ### Raises - - ``TypeError`` if ``switch_details`` is not an instance of - ``SwitchDetails``. + ## Raises + + ### TypeError + + - `switch_details` is not an instance of `SwitchDetails`. """ return self._switch_details @@ -766,15 +813,25 @@ def switch_details(self, value): @property def target(self) -> dict[str, str]: """ - ### Summary - ``target`` is a dictionary that is used to set the diff passed to - Results. + # Summary + + `target` is a dictionary that is used to set the diff passed to Results. - ``target`` is appended to a list of targets in - ``BootflashFiles().add_file()``, so must be passed for each file - to be deleted. See Usage example in the class docstring. + `target` is appended to a list of targets in `BootflashFiles().add_file()`, so must be + passed for each file to be deleted. See Usage example in the class docstring. + + ## Raises + + ### TypeError + + - `target` is not a dictionary. + + ### ValueError + + - `target` is missing a mandatory key. + + ## `target` Structure - ### ``target`` Structure ```json { "date": "2023-09-19 22:20:07", @@ -787,25 +844,21 @@ def target(self) -> dict[str, str]: } ``` - ### Raises - - ``TypeError`` if: - - ``target`` is not a dictionary. - - ``ValueError`` if: - - ``target`` is missing a mandatory key. + ## Associated key - ### Associated key None - ### Notes + ## Notes + 1. Since (at least with the dcnm_bootflash module) the user references switches using ip_address, and the NDFC bootflash-files payload includes only serialNumber, we - decided to use ``target`` as the diff since it contains the + decided to use `target` as the diff since it contains the ip_address and serial_number (as well as the size, date etc, which are potentially more useful than the info in the payload. - 2. ``BootflashFiles()`` requires that the ``ip_address`` key - be present in target, since it uses ``ip_address`` as the key + 2. `BootflashFiles()` requires that the `ip_address` key + be present in `target`, since it uses `ip_address` as the key for the diff. Of the other fields, we also require that filepath, serial_number and supervisor are present since they add value to the diff. The other fields shown above SHOULD be included diff --git a/plugins/module_utils/bootflash/bootflash_info.py b/plugins/module_utils/bootflash/bootflash_info.py index 035c65397..56cced696 100644 --- a/plugins/module_utils/bootflash/bootflash_info.py +++ b/plugins/module_utils/bootflash/bootflash_info.py @@ -11,6 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +""" +Retrieve and filter bootflash contents. +""" from __future__ import absolute_import, division, print_function __metaclass__ = type # pylint: disable=invalid-name @@ -40,12 +43,15 @@ class BootflashInfo: ## Raises - - ``ValueError`` if: - - params is not set. - - switches is not set. - - ``TypeError`` if: - - switches is not a list. - - switches contains anything other than strings. + ### ValueError + + - params is not set. + - switches is not set. + + ### TypeError + + - switches is not a list. + - switches contains anything other than strings. ## Usage @@ -265,10 +271,11 @@ def validate_refresh_parameters(self) -> None: ## Raises - - `ValueError` if: - - self.rest_send.params is not set. - - self.switches is not set. - - switches is not set. + ### ValueError + + - self.rest_send.params is not set. + - self.switches is not set. + - switches is not set. """ method_name: str = inspect.stack()[0][3] @@ -293,8 +300,9 @@ def refresh(self) -> None: ## Raises - - `ValueError` if: - - switches is not set. + ### ValueError + + - switches is not set. """ self.validate_refresh_parameters() @@ -318,8 +326,9 @@ def refresh_bootflash_info(self) -> None: ## Raises - - `ValueError` if: - - serial_number cannot be found for a switch. + ### ValueError + + - serial_number cannot be found for a switch. """ method_name: str = inspect.stack()[0][3] self.info_dict = {} @@ -355,13 +364,13 @@ def validate_prerequisites_for_build_matches(self) -> None: """ # Summary - Verify that mandatory prerequisites are met before calling - `build_matches()`. + Verify that mandatory prerequisites are met before calling `build_matches()`. ## Raises - - `ValueError` if: - - info_dict is empty i.e. `refresh` has not been called. + ### ValueError + + - info_dict is empty i.e. `refresh` has not been called. """ method_name: str = inspect.stack()[0][3] @@ -373,10 +382,10 @@ def validate_prerequisites_for_build_matches(self) -> None: def match_filter_filepath(self, target: dict[str, str]) -> bool: """ - ## Summary + # Summary - - Return True if the target's `filepath` matches `filter_filepath`. - - Return False otherwise. + - Return True if the target's `filepath` matches `filter_filepath`. + - Return False otherwise. ## Raises @@ -384,7 +393,7 @@ def match_filter_filepath(self, target: dict[str, str]) -> bool: """ if not self.filter_filepath: return False - filepath: str = target.get("filepath") or "" + filepath: str = target.get("filepath", "") posix: PurePosixPath = PurePosixPath(filepath) if not posix.match(self.filter_filepath): return False @@ -394,8 +403,8 @@ def match_filter_supervisor(self, target: dict[str, str]) -> bool: """ # Summary - - Return True if the target's `bootflash_type` matches `filter_supervisor`. - - Return False otherwise. + - Return True if the target's `bootflash_type` matches `filter_supervisor`. + - Return False otherwise. ## Raises @@ -403,7 +412,7 @@ def match_filter_supervisor(self, target: dict[str, str]) -> bool: """ if not self.filter_supervisor: return False - if target.get("supervisor", None) != self.filter_supervisor: + if target.get("supervisor", "") != self.filter_supervisor: return False return True @@ -411,8 +420,8 @@ def match_filter_switch(self, target: dict[str, str]) -> bool: """ # Summary - - Return True if the target's `ip_address` matches `filter_switch`. - - Return False otherwise. + - Return True if the target's `ip_address` matches `filter_switch`. + - Return False otherwise. ## Raises @@ -420,7 +429,7 @@ def match_filter_switch(self, target: dict[str, str]) -> bool: """ if not self.filter_switch: return False - if target.get("ip_address", None) != self.filter_switch: + if target.get("ip_address", "") != self.filter_switch: return False return True @@ -463,8 +472,8 @@ def build_matches(self) -> None: diff: dict[str, list[dict[str, str]]] = {} for match in self._matches: - ip_address = match.get("ip_address", None) - if ip_address is None: + ip_address = match.get("ip_address", "") + if not ip_address: continue if ip_address not in diff: diff[ip_address] = [] @@ -478,8 +487,7 @@ def filter_filepath(self) -> str: Return the current `filter_filepath`. - `filter_filepath` is a file path used to filter the results - of the query. It can include file globbing. + `filter_filepath` is a file path used to filter the results of the query. It can include file globbing. ## Raises @@ -487,10 +495,10 @@ def filter_filepath(self) -> str: ## Examples - - All txt files in the bootflash directory - - instance.filter_filepath = "bootflash:/*.txt" - - All txt files on all flash devices - - instance.filter_filepath = "*:/*.txt" + - All txt files in the bootflash directory + - instance.filter_filepath = "bootflash:/*.txt" + - All txt files on all flash devices + - instance.filter_filepath = "*:/*.txt" """ return self._filter_filepath @@ -513,9 +521,9 @@ def filter_supervisor(self) -> str: ## Raises - - `ValueError` if: - - value is not one of the valid_supervisor values - "active" or "standby". + ### ValueError + + - `value` is not one of the valid_supervisor values "active" or "standby". ## Example @@ -604,26 +612,18 @@ def rest_send(self) -> RestSend: """ # Summary - An instance of the RestSend class. + Get/set an instance of the RestSend class. ## Raises - - setter: `TypeError` if the value is not an instance of RestSend. - - getter: `ValueError` if RestSend.params is not set. + ### TypeError - ## getter + - setter: if the value is not an instance of RestSend. - Return an instance of the RestSend class. + ### ValueError - ## setter - - Set an instance of the RestSend class. + - getter: if RestSend.params is not set. """ - method_name: str = inspect.stack()[0][3] - if not self._rest_send.params: - msg = f"{self.class_name}.{method_name}: " - msg += "RestSend.params must be set before accessing." - raise ValueError(msg) return self._rest_send @rest_send.setter @@ -641,6 +641,10 @@ def rest_send(self, value: RestSend) -> None: raise TypeError(msg) from error if _class_have != _class_need: raise TypeError(msg) + if not value.params: + msg = f"{self.class_name}.{method_name}: " + msg += "RestSend.params must be set." + raise ValueError(msg) self._rest_send = value @property @@ -648,19 +652,14 @@ def results(self) -> Results: """ # Summary - An instance of the Results class. + Get/set an instance of the Results class. ## Raises - - setter: `TypeError` if the value is not an instance of Results. - - ## getter + ### TypeError - Return an instance of the Results class. + - setter: if the value is not an instance of Results. - ## setter - - Set an instance of the Results class. """ return self._results @@ -690,7 +689,9 @@ def switch_details(self) -> SwitchDetails: ## Raises - - `TypeError` if `switch_details` is not an instance of `SwitchDetails()`. + ### TypeError + + - `switch_details` is not an instance of `SwitchDetails()`. """ return self._switch_details @@ -720,13 +721,14 @@ def switches(self) -> list[str]: ## Raises - - getter: None - - setter: - - `TypeError` if: - - switches is not a list. - - switches contains anything other than strings. - - `ValueError` if: - - switches list is empty. + ### TypeError + + - setter: switches is not a list. + - setter: switches contains anything other than strings. + + ### ValueError + + - setter: switches list is empty. ## Example diff --git a/plugins/module_utils/bootflash/convert_file_info_to_target.py b/plugins/module_utils/bootflash/convert_file_info_to_target.py index 0eb9d7208..e09cf4728 100644 --- a/plugins/module_utils/bootflash/convert_file_info_to_target.py +++ b/plugins/module_utils/bootflash/convert_file_info_to_target.py @@ -11,9 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +""" +Build a `target` dictionary from a `file_info` dictionary. +""" from __future__ import absolute_import, division, print_function -__metaclass__ = type +__metaclass__ = type # pylint: disable=invalid-name __author__ = "Allen Robel" import inspect @@ -24,12 +27,16 @@ class ConvertFileInfoToTarget: """ - ### Summary - Build a ``target`` dictionary from a ``file_info`` dictionary. + # Summary - ### Raises + Build a `target` dictionary from a `file_info` dictionary. + + ## Raises + + ## `file_info` structure + + Returned by the bootflash-info endpoint. - ### ``file_info`` Dictionary (from bootflash-info endpoint response) ```json { "bootflash_type": "active", @@ -44,7 +51,8 @@ class ConvertFileInfoToTarget: } ``` - ### ``target`` Dictionary + ## `target` structure + ```json { "date": "2023-09-19 22:20:07", @@ -57,7 +65,8 @@ class ConvertFileInfoToTarget: } ``` - ### Usage + ## Usage + ```python instance = ConvertFileInfoToTarget() instance.file_info = { @@ -75,7 +84,8 @@ class ConvertFileInfoToTarget: print(instance.target) ``` - ### Output + ## Output + ```json { "date": "2023-09-19 22:20:07", @@ -94,29 +104,32 @@ def __init__(self) -> None: self.action = "convert_file_info_to_target" self.timestamp_format = "%b %d %H:%M:%S %Y" - self._file_info = None - self._filename = None - self._filepath = None - self._ip_address = None - self._serial_number = None - self._supervisor = None - self._target = None + self.log: logging.Logger = logging.getLogger(f"dcnm.{self.class_name}") + + self._file_info: dict[str, str] = {} + self._filename: str = "" + self._filepath: str = "" + self._ip_address: str = "" + self._serial_number: str = "" + self._supervisor: str = "" + self._target: dict[str, str] = {} - self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED ConvertFileInfoToTarget(): " + msg = f"{self.class_name}.__init__(): ENTERED" self.log.debug(msg) def validate_commit_parameters(self) -> None: """ - ### Summary - Validate that the parameters required to build the target dictionary - are present. + # Summary + + Validate that the parameters required to build the target dictionary are present. + + ## Raises - ### Raises - - ``ValueError`` if: - - ``file_info`` is not set. + ### ValueError + + - `file_info` is not set. """ - method_name = inspect.stack()[0][3] + method_name: str = inspect.stack()[0][3] def raise_error(msg): raise ValueError(f"{self.class_name}.{method_name}: {msg}") @@ -127,28 +140,30 @@ def raise_error(msg): def commit(self) -> None: """ - ### Summary - Given ``file_info``, which is the information for a single file from - the bootflash-info endpoint response, build a ``target`` dictionary + # Summary + + Given `file_info`, which is the information for a single file from + the bootflash-info endpoint response, build a `target` dictionary containing: - 1. A Posix path ``filepath`` from the ``file_info`` dictionary. - 2. Rename ``bootflash_type`` to ``supervisor`` in the target - dictionary. - 3. Convert the ``date`` value to a more easily digestable format - (YYYY-MM-DD HH:MM:SS). - 4. Rename ipAddr to ip_address and strip the leading space that - NDFC adds. - 5. Rename serialNumber to serial_number and add to the target - dictionary. - 6. Add size to the target dictionary. - - ### Raises - - ``ValueError`` if: - - ``file_info`` is not set. - - ``target`` cannot be built from ``file_info``. - - ### ``file_info`` (from bootflash-info endpoint response) + 1. A Posix path `filepath` from the `file_info` dictionary. + 2. Rename `bootflash_type` to `supervisor` in the `target` dictionary. + 3. Convert the `date` value to a more easily digestible format (YYYY-MM-DD HH:MM:SS). + 4. Rename `ipAddr` to `ip_address` and strip the leading space that NDFC adds. + 5. Rename `serialNumber` to `serial_number` and add to the `target` dictionary. + 6. Add `size` to the `target` dictionary. + + ## Raises + + ### ValueError + + - `file_info` is not set. + - `target` cannot be built from `file_info`. + + ## `file_info` structure + + From bootflash-info endpoint response. + ```json { "bootflash_type": "active", @@ -163,7 +178,8 @@ def commit(self) -> None: } ``` - ### ``target`` Structure + ## `target` structure + ```json { "date": "2023-09-19 22:20:07", @@ -177,7 +193,7 @@ def commit(self) -> None: ``` """ - method_name = inspect.stack()[0][3] + method_name: str = inspect.stack()[0][3] self.validate_commit_parameters() def raise_error(msg): @@ -213,49 +229,56 @@ def raise_error(msg): msg += f"Error detail: {error}" raise_error(msg) - def _get(self, key): + def _get(self, key: str) -> str: """ - ### Summary - Get the value of a key from the ``file_info`` dictionary. + # Summary - ### Raises - - ``ValueError`` if: - - ``file_info`` has not been set before calling _get. - - ``key`` is not in the target dictionary. - """ - method_name = inspect.stack()[0][3] + Get the value of a key from the `file_info` dictionary. - def raise_error(msg): - raise ValueError(f"{self.class_name}.{method_name}: {msg}") + ## Raises - if self.file_info is None: - msg = "file_info must be set before calling ``_get()``." - raise_error(msg) + ### ValueError + + - `file_info` has not been set before calling _get. + - `key` is not in the target dictionary. + """ + method_name: str = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + + if not self.file_info: + msg += "file_info must be set before calling _get()." + raise ValueError(msg) if key not in self.file_info: - msg = f"Missing key {key} in file_info: {self.file_info}." - raise_error(msg) + msg += f"Missing key {key} in file_info: {self.file_info}." + raise ValueError(msg) - return self.file_info.get(key) + return self.file_info.get(key, "") @property - def file_info(self): + def file_info(self) -> dict[str, str]: """ - ### Summary + # Summary + A single file dictionary from the bootflash-info endpoint response. - ### Raises - - ``ValueError`` if: - - ``file_info`` is not a dictionary. - - ``file_info`` does not contain the requisite keys. + ## Raises + + ### ValueError + + - `file_info` is not a dictionary. + - `file_info` does not contain the requisite keys. + + ## Expected Structure - ### Expected Structure This class uses the following keys from the file_info dictionary: - - fileName - - filePath - - bootflash_type - ### Example + - fileName + - filePath + - bootflash_type + + ## Example + ```json { "bootflash_type": "active", @@ -273,24 +296,27 @@ def file_info(self): return self._file_info @file_info.setter - def file_info(self, value): + def file_info(self, value: dict[str, str]) -> None: self._file_info = value @property - def date(self): + def date(self) -> datetime: """ - ### Summary - The value of ``date`` from the ``file_info`` dictionary - converted to a ``datetime`` object. The string representation of - this object will be "YYYY-MM-DD HH:MM:SS". - - ### Raises - - ``ValueError`` if: - - ``file_info`` has not been set before accessing. - - ``date`` is not in the ``file_info`` dictionary. - - ``date`` cannot be converted to a datetime object. + # Summary + + The value of `date` from the `file_info` dictionary converted to a `datetime` object. + + The string representation of this object will be "YYYY-MM-DD HH:MM:SS". + + ## Raises + + ### ValueError + + - `file_info` has not been set before accessing. + - `date` is not in the `file_info` dictionary. + - `date` cannot be converted to a datetime object. """ - method_name = inspect.stack()[0][3] + method_name: str = inspect.stack()[0][3] try: _date = datetime.strptime(self._get("date"), self.timestamp_format) except (TypeError, ValueError) as error: @@ -302,125 +328,152 @@ def date(self): return _date @property - def device_name(self): + def device_name(self) -> str: """ - ### Summary - The value of ``deviceName`` from the ``file_info`` dictionary. + # Summary + + The value of `deviceName` from the `file_info` dictionary. + + ## Raises + + ### ValueError - ### Raises - ``ValueError`` if: - - ``file_info`` has not been set before accessing. - - ``deviceName`` is not in the ``file_info`` dictionary. + - `file_info` has not been set before accessing. + - `deviceName` is not in the `file_info` dictionary. """ return self._get("deviceName") @property - def filename(self): + def filename(self) -> str: """ - ### Summary - The value of ``fileName`` from the ``file_info`` dictionary. + # Summary - ### Raises - ``ValueError`` if: - - ``file_info`` has not been set before accessing. - - ``fileName`` is not in the ``file_info`` dictionary. + The value of `fileName` from the `file_info` dictionary. + + ## Raises + + ### ValueError + + - `file_info` has not been set before accessing. + - `fileName` is not in the `file_info` dictionary. """ return self._get("fileName") @property - def filepath(self): + def filepath(self) -> str: """ - ### Summary - The value of ``filePath`` from the ``file_info`` dictionary. + # Summary + + The value of `filePath` from the `file_info` dictionary. - ### Raises - ``ValueError`` if: - - ``file_info`` has not been set before accessing. - - ``filePath`` is not in the ``file_info`` dictionary. + ## Raises + + ### ValueError + + - `file_info` has not been set before accessing. + - `filePath` is not in the `file_info` dictionary. """ return self._get("filePath") @property - def ip_address(self): + def ip_address(self) -> str: """ - ### Summary - The stripped value of ``ipAddr`` from the ``file_info`` dictionary. + # Summary + + The stripped value of `ipAddr` from the `file_info` dictionary. + + ## Raises - ### Raises - ``ValueError`` if: - - ``file_info`` has not been set before accessing. - - ``ipAddr`` is not in the ``file_info`` dictionary. + ### ValueError + + - `file_info` has not been set before accessing. + - `ipAddr` is not in the `file_info` dictionary. """ return self._get("ipAddr").strip() @property - def name(self): + def name(self) -> str: """ - ### Summary - The value of ``name`` from the ``file_info`` dictionary. + # Summary + + The value of `name` from the `file_info` dictionary. + + ## Raises + + ### ValueError - ### Raises - ``ValueError`` if: - - ``file_info`` has not been set before accessing. - - ``name`` is not in the ``file_info`` dictionary. + - `file_info` has not been set before accessing. + - `name` is not in the `file_info` dictionary. """ return self._get("name") @property - def serial_number(self): + def serial_number(self) -> str: """ - ### Summary - The value of ``serialNumber`` from the ``file_info`` dictionary. + # Summary - ### Raises - ``ValueError`` if: - - ``file_info`` has not been set before accessing. - - ``serialNumber`` is not in the ``file_info`` dictionary. + The value of `serialNumber` from the `file_info` dictionary. + + ## Raises + + ### ValueError + + - `file_info` has not been set before accessing. + - `serialNumber` is not in the `file_info` dictionary. """ return self._get("serialNumber") @property - def size(self): + def size(self) -> str: """ - ### Summary - The value of ``size`` from the ``file_info`` dictionary. + # Summary + + The value of `size` from the `file_info` dictionary. - ### Raises - ``ValueError`` if: - - ``file_info`` has not been set before accessing. - - ``size`` is not in the ``file_info`` dictionary. + ## Raises + + ### ValueError + + - `file_info` has not been set before accessing. + - `size` is not in the `file_info` dictionary. """ return self._get("size") @property - def target(self): + def target(self) -> dict[str, str]: """ - ### Summary - The target dictionary built from the ``file_info`` dictionary. + # Summary + + The target dictionary built from the `file_info` dictionary. + + ## Raises - ### Raises - ``ValueError`` if: - - ``commit()`` has not been called before accessing. + ### ValueError + + - `commit()` has not been called before accessing. """ - if self._target is None: + if not self._target: msg = f"{self.class_name}.target: " msg += "target has not been built. Call commit() before accessing." raise ValueError(msg) return self._target @target.setter - def target(self, value): + def target(self, value: dict[str, str]): self._target = value @property - def supervisor(self): + def supervisor(self) -> str: """ - ### Summary - The value of ``bootflash_type`` from the ``file_info`` dictionary. + # Summary + + The value of `bootflash_type` from the `file_info` dictionary. + + ## Raises + + ### ValueError - ### Raises - ``ValueError`` if: - - ``file_info`` has not been set before accessing. - - ``bootflash_type`` is not in the ``file_info`` dictionary. + - `file_info` has not been set before accessing. + - `bootflash_type` is not in the `file_info` dictionary. """ return self._get("bootflash_type") diff --git a/plugins/module_utils/bootflash/convert_target_to_params.py b/plugins/module_utils/bootflash/convert_target_to_params.py index fad8abeab..e6a2d48b9 100644 --- a/plugins/module_utils/bootflash/convert_target_to_params.py +++ b/plugins/module_utils/bootflash/convert_target_to_params.py @@ -11,9 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +""" +Parse `target` into its constituent API parameters +""" from __future__ import absolute_import, division, print_function -__metaclass__ = type +__metaclass__ = type # pylint: disable=invalid-name __author__ = "Allen Robel" import inspect @@ -22,17 +25,22 @@ class ConvertTargetToParams: """ - ### Summary - Parse ``target`` into its consituent API parameters. + # Summary - ### Raises - - ``ValueError`` if: - - ``filepath`` is not set in the target dict. - - ``supervisor`` is not set in the target dict. + Parse `target` into its constituent API parameters. + + ## Raises + + ### ValueError + + - `filepath` is not set in the target dict. + - `supervisor` is not set in the target dict. + + ## Usage + + ### Example 1, file in directory. - ### Usage ```python - # Example 1, file in directory. target = { "filepath": "bootflash:/myDir/foo.txt", "supervisor": "active" @@ -44,70 +52,84 @@ class ConvertTargetToParams: print(instance.filepath) # bootflash:/myDir/ print(instance.filename) # foo.txt print(instance.supervisor) # active + ``` + + ### Example 2, file in root of bootflash partition. - # Example 2, file in root of bootflash partition. + ```python target = { "filepath": "bootflash:/foo.txt", "supervisor": "active" } + instance = ConvertTargetToParams() instance.target = target instance.commit() print(instance.partition) # bootflash: print(instance.filepath) # bootflash: print(instance.filename) # foo.txt print(instance.supervisor) # active - ``` """ def __init__(self) -> None: - self.class_name = self.__class__.__name__ - self.action = "convert_target_to_params" - self.committed = False - self.valid_supervisor = ["active", "standby"] - - self._filename = None - self._filepath = None - self._target = None - self._partition = None - self._supervisor = None - - self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.class_name: str = self.__class__.__name__ + self.action: str = "convert_target_to_params" + self._committed: bool = False + self.valid_supervisor: list[str] = ["active", "standby"] + + self._filename: str = "" + self._filepath: str = "" + self._target: dict = {} + self._partition: str = "" + self._supervisor: str = "" + + self.log: logging.Logger = logging.getLogger(f"dcnm.{self.class_name}") msg = "ENTERED ConvertTargetToParams(): " self.log.debug(msg) - def commit(self): + def commit(self) -> None: """ - ### Summary + # Summary + Commit the target to be parsed. - ### Raises - - ``ValueError`` if: - - target is not set before calling commit. + ## Raises + + ### ValueError + + - `target` is not set before calling commit. """ - if self.target is None: + if not self.target: msg = f"{self.class_name}.commit: " msg += "target must be set before calling commit." raise ValueError(msg) self.parse_target() - self.committed = True + self._committed = True def parse_target(self) -> None: """ - ### Summary - Parse target into its consituent API parameters. + # Summary + + Parse `target` into its constituent API parameters. + + ## Raises - ### Raises - - ``ValueError`` if: - - ``filepath`` is not set in the target dict. - - ``supervisor`` is not set in the target dict. + ### ValueError - ### Target Structure + - `filepath` is not set in the target dict. + - `supervisor` is not set in the target dict. + + ## Target Structure + + ```json { filepath: bootflash:/myDir/foo.txt supervisor: active } + ``` + + ## API Parameters Set the following API parameters from the above structure: @@ -116,7 +138,8 @@ def parse_target(self) -> None: - self.filename: foo.txt - self.supervisor: active - ### Notes + ## Notes + - While this method is written to support files in directories, the NDFC API does not support listing files within a directory. Hence, we currently support only files in the root directory of the @@ -133,21 +156,25 @@ def parse_target(self) -> None: bootflash:/myDirfoo.txt which, of course, will not match (or worse yet, match and delete the wrong file). """ - method_name = inspect.stack()[0][3] + method_name: str = inspect.stack()[0][3] - def raise_error(msg): - raise ValueError(f"{self.class_name}.{method_name}: {msg}") + msg = f"{self.class_name}.{method_name}: " - if self.target.get("filepath", None) is None: - msg = "Expected filepath in target dict. " + if not self._target: + msg += "Expected target to be set before calling parse_target." + raise ValueError(msg) + + if self._target.get("filepath") is None: + msg += "Expected filepath in target dict. " msg += f"Got {self.target}." - raise_error(msg) - if self.target.get("supervisor", None) is None: - msg = "Expected supervisor in target dict. " + raise ValueError(msg) + + if self._target.get("supervisor") is None: + msg += "Expected supervisor in target dict. " msg += f"Got {self.target}." - raise_error(msg) + raise ValueError(msg) - parts = self.target.get("filepath").split("/") + parts: list[str] = self._target.get("filepath", "").split("/") self.partition = parts[0] # If len(parts) == 2, the file is located in the root directory of the # partition. In this case we DO NOT want to add a trailing slash to @@ -161,56 +188,62 @@ def raise_error(msg): # Result: self.filepath == bootflash:/myDir/ self.filepath = "/".join(parts[0:-1]) + "/" self.filename = parts[-1] - self.supervisor = self.target.get("supervisor") + self.supervisor = self._target.get("supervisor", "") @property - def filename(self): + def filename(self) -> str: """ - ### Summary - Return the filename parsed from ``target``. + # Summary + + Return the filename parsed from `target`. + + ## Raises + + ### ValueError - ### Raises - ``ValueError`` if: - - ``commit()`` has not been called before accessing this property. + - `commit()` has not been called before accessing this property. """ method_name = inspect.stack()[0][3] - if not self.committed: + if not self._committed: msg = f"{self.class_name}.{method_name}: " msg += f"commit() must be called before accessing {method_name}." raise ValueError(msg) return self._filename @filename.setter - def filename(self, value): + def filename(self, value: str) -> None: self._filename = value @property - def filepath(self): + def filepath(self) -> str: """ - ### Summary - Return the filepath parsed from ``target``. + # Summary - ### Raises - ``ValueError`` if: - - ``commit()`` has not been called before accessing this property. + Return the filepath parsed from `target`. + + ## Raises + + ### ValueError + + - `commit()` has not been called before accessing this property. """ method_name = inspect.stack()[0][3] - if not self.committed: + if not self._committed: msg = f"{self.class_name}.{method_name}: " msg += f"commit() must be called before accessing {method_name}." raise ValueError(msg) return self._filepath @filepath.setter - def filepath(self, value): + def filepath(self, value: str) -> None: self._filepath = value @property - def target(self): + def target(self) -> dict: """ - ### Summary - The target to be parsed. This is a dictionary with the following - structure: + # Summary + + The target to be parsed. This is a dictionary with the following structure: ```json { @@ -222,28 +255,31 @@ def target(self): return self._target @target.setter - def target(self, value): + def target(self, value: dict) -> None: self._target = value @property - def partition(self): + def partition(self) -> str: """ - ### Summary - Return the partition parsed from ``target``. + # Summary + + Return the partition parsed from `target`. - ### Raises - ``ValueError`` if: - - ``commit()`` has not been called before accessing this property. + ## Raises + + ### ValueError + + - `commit()` has not been called before accessing this property. """ method_name = inspect.stack()[0][3] - if not self.committed: + if not self._committed: msg = f"{self.class_name}.{method_name}: " msg += f"commit() must be called before accessing {method_name}." raise ValueError(msg) return self._partition @partition.setter - def partition(self, value): + def partition(self, value: str) -> None: method_name = inspect.stack()[0][3] if not str(value).endswith(":"): msg = f"{self.class_name}.{method_name}: " @@ -253,26 +289,29 @@ def partition(self, value): self._partition = value @property - def supervisor(self): + def supervisor(self) -> str: """ - ### Summary - Return the supervisor parsed from ``target``. This is the state + # Summary + + Return the supervisor parsed from `target`. This is the state (active or standby) of the supervisor that hosts the file described - in ``target``. + in `target`. + + ## Raises + + ### ValueError - ### Raises - ``ValueError`` if: - - ``commit()`` has not been called before accessing this property. + - `commit()` has not been called before accessing this property. """ method_name = inspect.stack()[0][3] - if not self.committed: + if not self._committed: msg = f"{self.class_name}.{method_name}: " msg += f"commit() must be called before accessing {method_name}." raise ValueError(msg) return self._supervisor @supervisor.setter - def supervisor(self, value): + def supervisor(self, value: str) -> None: method_name = inspect.stack()[0][3] if value not in self.valid_supervisor: msg = f"{self.class_name}.{method_name}: " diff --git a/plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/bootflash/bootflash.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/bootflash/bootflash.py index ae9a7c1a7..a46709964 100644 --- a/plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/bootflash/bootflash.py +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/bootflash/bootflash.py @@ -11,10 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +""" +Common methods and properties for Bootflash() subclasses +""" from __future__ import absolute_import, division, print_function -__metaclass__ = type +__metaclass__ = type # pylint: disable=invalid-name __author__ = "Allen Robel" import logging diff --git a/plugins/modules/dcnm_bootflash.py b/plugins/modules/dcnm_bootflash.py index e3deac445..83537d00f 100644 --- a/plugins/modules/dcnm_bootflash.py +++ b/plugins/modules/dcnm_bootflash.py @@ -13,6 +13,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +""" +Bootflash management for Nexus switches. +""" # pylint: disable=wrong-import-position from __future__ import absolute_import, division, print_function @@ -293,10 +296,10 @@ def raise_error(msg): def get_want(self) -> None: """ - ### Summary + # Summary + 1. Validate the playbook configs - 2. Convert the validated configs to the structure required by the - the Delete() and Query() classes. + 2. Convert the validated configs to the structure required by the the Delete() and Query() classes. 3. Update self.want with this list of payloads If a switch in the switches list does not have a targets key, add the @@ -304,22 +307,28 @@ def get_want(self) -> None: playbook. Else, use the switch's targets info (i.e. the switch's targets info overrides the global targets info). - ### Raises - - ValueError if: - - ``ip_address`` is missing from a switch dict. - - ``filepath`` is missing from a target dict. - - TypeError if: - - The value of ``targets`` is not a list of dictionaries. - - ### ``want`` Structure - - A list of dictionaries. Each dictionary contains the following keys: - - ip_address: The ip address of the switch. - - targets: A list of dictionaries. Each dictionary contains the - following keys: - - filepath: The path to the file to be deleted or queried. - - supervisor: The supervisor containing the filepath. - - ### Example ``want`` Structure + ## Raises + + ### ValueError + + - `ip_address` is missing from a switch dict. + - `filepath` is missing from a target dict. + + ### TypeError + + - The value of `targets` is not a list of dictionaries. + + ## ``want`` structure + + A list of dictionaries. Each dictionary contains the following keys: + + - ip_address: The ip address of the switch. + - targets: A list of dictionaries. Each dictionary contains the following keys: + - filepath: The path to the file to be deleted or queried. + - supervisor: The supervisor containing the filepath. + + ## Example ``want`` structure + ```json [ { @@ -377,18 +386,20 @@ def raise_type_error(msg): @property def rest_send(self) -> RestSend: """ - ### Summary - An instance of the RestSend class. + # Summary - ### Raises - - getter: `ValueError` if rest_send has not been properly initialized (missing params). - - setter: ``TypeError`` if the value is not an instance of RestSend. + Get/set an instance of the RestSend class. - ### getter - Return a properly initialized instance of the RestSend class. + ## Raises + + ### ValueError + + - getter: `rest_send` has not been properly initialized (missing params). + + ### TypeError + + - setter: The value is not an instance of `RestSend`. - ### setter - Set an instance of the RestSend class. """ if not self._rest_send.params: msg = f"{self.class_name}.rest_send: " @@ -416,12 +427,15 @@ def rest_send(self, value: RestSend): class Deleted(Common): """ - ### Summary + # Summary + Handle deleted state - ### Raises - - ValueError if: - - ``Common.__init__()`` raises TypeError or ValueError. + ## Raises + + ### ValueError + + - `Common.__init__()` raises TypeError or ValueError. """ def __init__(self, params: dict[str, Any]) -> None: @@ -445,22 +459,27 @@ def __init__(self, params: dict[str, Any]) -> None: def populate_files_to_delete(self, switch) -> None: """ - ### Summary - Populate the ``files_to_delete`` dictionary with files - the user intends to delete. - - ### Raises - - ``ValueError`` if: - - ``supervisor`` is not one of: - - active - - standby - - ### ``files_to_delete`` Structure - files_to_delete is a dictionary containing - - key: switch ip address. - - value: a list of dictionaries containing the files to delete. - - ### ``files_to_delete`` Example + # Summary + + Populate the `files_to_delete` dictionary with files the user intends to delete. + + ## Raises + + ### ValueError + + - `supervisor` is not one of + - active + - standby + + ## `files_to_delete` structure + + `files_to_delete` is a dictionary containing + + - key: switch ip address. + - value: a list of dictionaries containing the files to delete. + + ### Example + ```json { "172.22.150.112": [ @@ -495,15 +514,20 @@ def populate_files_to_delete(self, switch) -> None: def update_bootflash_files(self, ip_address: str, target: dict[str, str]) -> None: """ - ### Summary - Call ``BootflashFiles().add_file()`` to add the file associated with - ``ip_address`` and ``target`` to the list of files to be deleted. - - ### Raises - - ``TypeError`` if: - - ``target`` is not a dictionary. - - ``ValueError`` if: - - ``BootflashFiles().add_file`` raises ``ValueError``. + # Summary + + Call `BootflashFiles().add_file()` to add the file associated with + `ip_address` and `target` to the list of files to be deleted. + + ## Raises + + ### TypeError + + - `target` is not a dictionary. + + ### ValueError + + - `BootflashFiles().add_file` raises `ValueError`. """ method_name: str = inspect.stack()[0][3] @@ -543,16 +567,18 @@ def update_bootflash_files(self, ip_address: str, target: dict[str, str]) -> Non def commit(self) -> None: """ - ### Summary + # Summary + Delete the specified files if they exist. - ### Raises + ## Raises + None. While this method does not directly raise exceptions, it calls other methods that may raise the following exceptions: - - ControllerResponseError - - TypeError - - ValueError + - ControllerResponseError + - TypeError + - ValueError """ # Populate self.switches self.get_want() @@ -594,12 +620,15 @@ def commit(self) -> None: class Query(Common): """ - ### Summary + # Summary + Handle query state. - ### Raises - - ValueError if: - - ``Common.__init__()`` raises TypeError or ValueError. + ## Raises + + ### ValueError + + - `Common.__init__()` raises TypeError or ValueError. """ def __init__(self, params: dict[str, Any]) -> None: @@ -625,10 +654,12 @@ def __init__(self, params: dict[str, Any]) -> None: def register_null_result(self) -> None: """ - ### Summary + # Summary + Register a null result when there are no switches to query. - ### Raises + ## Raises + None """ response_dict: dict[str, dict[str, Any]] = {} @@ -654,12 +685,15 @@ def commit(self) -> None: ## Raises - None. While this method does not directly raise exceptions, it - calls other methods that may raise the following exceptions: + ### ValueError - - ControllerResponseError - - TypeError - - ValueError + - Missing `ip_address` in switch dict. + + This method calls other methods that may also raise the following exceptions: + + - ControllerResponseError + - TypeError + - ValueError """ method_name: str = inspect.stack()[0][3] @@ -701,7 +735,11 @@ def commit(self) -> None: # Use the file info from the controller as the diff. diff_current: dict[str, list[dict[str, Any]]] = {} for switch in self.switches: - ip_address = switch.get("ip_address") + ip_address = switch.get("ip_address", "") + if not ip_address: + msg = f"{self.class_name}.{method_name}: " + msg += "Missing ip_address in switch dict." + raise ValueError(msg) self.bootflash_info.filter_switch = ip_address if ip_address not in diff_current: diff_current[ip_address] = [] diff --git a/tests/unit/modules/dcnm/dcnm_bootflash/test_bootflash_deleted.py b/tests/unit/modules/dcnm/dcnm_bootflash/test_bootflash_deleted.py index 3d9c94597..e5b6252e0 100644 --- a/tests/unit/modules/dcnm/dcnm_bootflash/test_bootflash_deleted.py +++ b/tests/unit/modules/dcnm/dcnm_bootflash/test_bootflash_deleted.py @@ -20,7 +20,7 @@ from __future__ import absolute_import, division, print_function -__metaclass__ = type +__metaclass__ = type # pylint: disable=invalid-name __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" diff --git a/tests/unit/modules/dcnm/dcnm_bootflash/test_bootflash_info.py b/tests/unit/modules/dcnm/dcnm_bootflash/test_bootflash_info.py index d1374092b..05921791e 100644 --- a/tests/unit/modules/dcnm/dcnm_bootflash/test_bootflash_info.py +++ b/tests/unit/modules/dcnm/dcnm_bootflash/test_bootflash_info.py @@ -17,7 +17,9 @@ # Due to the above, we also need to disable unused-import # Also, fixtures need to use *args to match the signature of the function they are mocking # pylint: disable=unused-import, protected-access, use-implicit-booleaness-not-comparison - +""" +Unit tests for BootflashInfo class +""" from __future__ import absolute_import, division, print_function __metaclass__ = type # pylint: disable=invalid-name @@ -176,8 +178,8 @@ def test_bootflash_info_00110() -> None: instance.results = Results() instance.switches = ["192.168.1.1"] - match = r"BootflashInfo\.rest_send: " - match += r"RestSend.params must be set before accessing\." + match = r"BootflashInfo\.validate_refresh_parameters: " + match += r"rest_send must be set prior to calling refresh\." with pytest.raises(ValueError, match=match): instance.refresh() @@ -276,7 +278,7 @@ def test_bootflash_info_00200() -> None: """ with does_not_raise(): instance = BootflashInfo() - instance.rest_send = RestSend({}) + instance.rest_send = RestSend(params=params_query) instance.results = Results() instance.switches = ["172.22.150.112", "172.22.150.113"] @@ -509,7 +511,7 @@ def test_bootflash_info_00300(filter_filepath, filepath, expected) -> None: """ with does_not_raise(): instance = BootflashInfo() - instance.rest_send = RestSend({}) + instance.rest_send = RestSend(params=params_query) instance.results = Results() instance.switches = ["172.22.150.112"] if filter_filepath is not None: @@ -549,7 +551,7 @@ def test_bootflash_info_00310(filter_supervisor, supervisor, expected) -> None: """ with does_not_raise(): instance = BootflashInfo() - instance.rest_send = RestSend({}) + instance.rest_send = RestSend(params=params_query) instance.results = Results() instance.switches = ["172.22.150.112"] if filter_supervisor is not None: @@ -592,7 +594,7 @@ def test_bootflash_info_00320(filter_switch, switch, expected) -> None: """ with does_not_raise(): instance = BootflashInfo() - instance.rest_send = RestSend({}) + instance.rest_send = RestSend(params=params_query) instance.results = Results() instance.switches = ["172.22.150.112"] if filter_switch is not None: @@ -622,7 +624,7 @@ def test_bootflash_info_00400() -> None: """ with does_not_raise(): instance = BootflashInfo() - instance.rest_send = RestSend({}) + instance.rest_send = RestSend(params=params_query) instance.results = Results() instance.switches = ["172.22.150.112"] match = r"BootflashInfo\.filter_supervisor\.setter:\s+" @@ -649,7 +651,7 @@ def test_bootflash_info_00500() -> None: """ with does_not_raise(): instance = BootflashInfo() - instance.rest_send = RestSend({}) + instance.rest_send = RestSend(params=params_query) instance.results = Results() instance.switches = ["172.22.150.112"] match = r"BootflashInfo.switch_details:\s+" @@ -678,7 +680,7 @@ def test_bootflash_info_00510() -> None: """ with does_not_raise(): instance = BootflashInfo() - instance.rest_send = RestSend({}) + instance.rest_send = RestSend(params=params_query) instance.results = Results() instance.switches = ["172.22.150.112"] match = r"BootflashInfo.switch_details:\s+" @@ -704,7 +706,7 @@ def test_bootflash_info_00600() -> None: """ with does_not_raise(): instance = BootflashInfo() - instance.rest_send = RestSend({}) + instance.rest_send = RestSend(params=params_query) instance.results = Results() match = r"BootflashInfo\.switches:\s+" match += r"switches must be a list\. got str for value foo\." @@ -728,7 +730,7 @@ def test_bootflash_info_00610() -> None: """ with does_not_raise(): instance = BootflashInfo() - instance.rest_send = RestSend({}) + instance.rest_send = RestSend(params=params_query) instance.results = Results() match = r"BootflashInfo.switches:\s+" match += r"switches must be a list with at least one ip address\.\s+" @@ -753,7 +755,7 @@ def test_bootflash_info_00620() -> None: """ with does_not_raise(): instance = BootflashInfo() - instance.rest_send = RestSend({}) + instance.rest_send = RestSend(params=params_query) instance.results = Results() match = r"BootflashInfo\.switches:\s+" match += r"switches must be a list of ip addresses\.\s+" diff --git a/tests/unit/modules/dcnm/dcnm_bootflash/test_bootflash_query.py b/tests/unit/modules/dcnm/dcnm_bootflash/test_bootflash_query.py index fcb9c795b..20f95c73f 100644 --- a/tests/unit/modules/dcnm/dcnm_bootflash/test_bootflash_query.py +++ b/tests/unit/modules/dcnm/dcnm_bootflash/test_bootflash_query.py @@ -16,11 +16,14 @@ # https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html # Due to the above, we also need to disable unused-import # Also, fixtures need to use *args to match the signature of the function they are mocking +""" +Unit tests for Query class +""" # pylint: disable=unused-import, protected-access, use-implicit-booleaness-not-comparison, unused-variable from __future__ import absolute_import, division, print_function -__metaclass__ = type +__metaclass__ = type # pylint: disable=invalid-name __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" @@ -163,6 +166,9 @@ def responses(): instance.results.diff[0]["172.22.150.113"][0]["filepath"] == "bootflash:/black.txt" ) + assert instance.results is not None + assert instance.results.response is not None + assert instance.results.result is not None assert instance.results.metadata[0]["action"] == "bootflash_info" assert instance.results.metadata[0]["check_mode"] is False assert instance.results.metadata[0]["sequence_number"] == 1 @@ -211,6 +217,9 @@ def test_bootflash_query_01010() -> None: instance.rest_send = rest_send instance.commit() + assert instance.results is not None + assert instance.results.response is not None + assert instance.results.result is not None assert len(instance.results.diff) == 1 assert instance.results.diff[0]["sequence_number"] == 1 assert instance.results.metadata[0]["action"] == "bootflash_info" diff --git a/tests/unit/modules/dcnm/dcnm_bootflash/test_convert_file_info_to_target.py b/tests/unit/modules/dcnm/dcnm_bootflash/test_convert_file_info_to_target.py index 5c2eeef39..84f72572c 100644 --- a/tests/unit/modules/dcnm/dcnm_bootflash/test_convert_file_info_to_target.py +++ b/tests/unit/modules/dcnm/dcnm_bootflash/test_convert_file_info_to_target.py @@ -16,11 +16,14 @@ # https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html # Due to the above, we also need to disable unused-import # Also, fixtures need to use *args to match the signature of the function they are mocking +""" +Unit tests for ConvertFileInfoToTarget class +""" # pylint: disable=unused-import, protected-access, use-implicit-booleaness-not-comparison from __future__ import absolute_import, division, print_function -__metaclass__ = type +__metaclass__ = type # pylint: disable=invalid-name __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" @@ -53,13 +56,13 @@ def test_convert_file_info_to_target_00000() -> None: assert instance.action == "convert_file_info_to_target" assert instance.class_name == "ConvertFileInfoToTarget" - assert instance._file_info is None - assert instance._filename is None - assert instance._filepath is None - assert instance._ip_address is None - assert instance._serial_number is None - assert instance._supervisor is None - assert instance._target is None + assert instance._file_info == {} + assert instance._filename == "" + assert instance._filepath == "" + assert instance._ip_address == "" + assert instance._serial_number == "" + assert instance._supervisor == "" + assert instance._target == {} assert instance.timestamp_format == "%b %d %H:%M:%S %Y" @@ -115,8 +118,8 @@ def test_convert_file_info_to_target_00110() -> None: """ with does_not_raise(): instance = ConvertFileInfoToTarget() - match = r"ConvertFileInfoToTarget\.validate_commit_parameters:\s+" - match += r"file_info must be set before calling commit\(\)\." + match = r"ConvertFileInfoToTarget\._get:\s+" + match += r"file_info must be set before calling _get\(\)\." with pytest.raises(ValueError, match=match): instance.commit() @@ -198,7 +201,7 @@ def test_convert_file_info_to_target_00200() -> None: instance = ConvertFileInfoToTarget() match = r"ConvertFileInfoToTarget\._get:\s+" - match += r"file_info must be set before calling ``_get\(\)``\." + match += r"file_info must be set before calling _get\(\)\." with pytest.raises(ValueError, match=match): instance.date # pylint: disable=pointless-statement diff --git a/tests/unit/modules/dcnm/dcnm_bootflash/test_convert_target_to_params.py b/tests/unit/modules/dcnm/dcnm_bootflash/test_convert_target_to_params.py index aa7e8a8cb..6eeca5c2d 100644 --- a/tests/unit/modules/dcnm/dcnm_bootflash/test_convert_target_to_params.py +++ b/tests/unit/modules/dcnm/dcnm_bootflash/test_convert_target_to_params.py @@ -16,11 +16,14 @@ # https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html # Due to the above, we also need to disable unused-import # Also, fixtures need to use *args to match the signature of the function they are mocking +""" +Unit tests for ConvertTargetToParams class +""" # pylint: disable=unused-import, protected-access, use-implicit-booleaness-not-comparison from __future__ import absolute_import, division, print_function -__metaclass__ = type +__metaclass__ = type # pylint: disable=invalid-name __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" @@ -52,12 +55,12 @@ def test_convert_target_to_params_00000() -> None: assert instance.action == "convert_target_to_params" assert instance.class_name == "ConvertTargetToParams" - assert instance._filename is None - assert instance._filepath is None - assert instance._target is None - assert instance._partition is None - assert instance._supervisor is None - assert instance.committed is False + assert instance._filename == "" + assert instance._filepath == "" + assert instance._target == {} + assert instance._partition == "" + assert instance._supervisor == "" + assert instance._committed is False def test_convert_target_to_params_00100() -> None: