Skip to content

Commit

Permalink
dcnm_image_upgrade (ready to merge). (#269)
Browse files Browse the repository at this point in the history
* More robust handling of response_data

* Let the user know which device is failing.

* More unit tests

* run thru black/isort

* Convert user input to boolean if it's boolean-like

* Add testcase, standardize testcase names, more...

Run thru black/isort

* standardize test names

* Fix docstring

* Rename "module" to something more descriptive

* Consider the upgrade status when determining idempotence

If have upgrade == "Failed" we should try to upgrade again, else the user would have to use the GUI (or policy management playbook) to reset the upgrade status.

* Determine valid EPLD module differently, more...

If options.epld.golden is True, then upgrade.nxos must be false or NX-OS throws an error which doesn't appear in any CLI output after the event.  So, catch this combination, and fail_json with an explanation.

* Remove log messages

* More robust handling of missing keys

* Remove log messages

* method_name should be local to each method

If method_name is global to a class, the wrong method name is logged if a method calls another method.

* Remove fail_json from validate_devices.

* Update test_image_mgmt_upgrade_00005 based on last commit

* Add testcases for ImageUpgrade.commit(), more...

Add logging to image_upgrade.py to obtain controller responses for future tests.  Remove these later...

* Add unit tests, more...

epld_module - Accept a module number in str() form and silently convert to int()

* Add unit tests for _merge_defaults_to_switch_config

* build_payload: Add another unit test, more...

ImageUpgrade.nxos_mode: sort valid_nxos_mode so that the match in corresponding unit-test works consistently.

* Remove check for install_options.success, more...

Removing since install_options will fail_json in the event of failure.

* ImageUpgrade.commit - add more unit tests

* run through black/isort

* build_payload: convert boolean-like strings to boolean

Give the user some leeway in setting boolean-like items e.g. instead of True, let the user enter "true" or "TRUE"

* Fix bad method name make_bool -> make_boolean

* Remove duplicate testcase 00004

This testcase didn't used to be a duplicate, but we changed the code and then changed this testcase without realizing that it's now a duplicate of 00005.

Renumber all subsequent testcases to make room for one additional testcase (next commit)

* ImageUpgrade: verify upgrade.nxos/upgrade.epld, more...

test_image_upgrade_ImageUpgrade.py
- Add summary for all tests
- Add tests for:
  - upgrade.nxos invalid value (00018)
  - upgrade.epld invalid value (00033)
- Fix 00015 (options.reboot.config_reload) test description

* upgrade.epld - remove redundant assignment/check

* Add unit tests, more...

- ImageUpgrade._wait_for_current_actions_to_complete:  Add unit tests 00080 and 00081
- ImageUpgrade._wait_for_current_actions_to_complete:  self.ipv4_todo should be a set()
- ImageUpgrade._wait_for_current_actions_to_complete:  ipv4_todo/ipv4_done should be sorted for unit tests

- Run class and test .py through black/isort

- ImageUpgrade.check_interval - add setter for unit test
- ImageUpgrade.check_timeout - add setter for unit test

* Add unit test for _wait_for_image_upgrade_to_complete

Add unit test test_image_mgmt_upgrade_00090

* Add unit test for _wait_for_image_upgrade_to_complete

Add unit test test_image_mgmt_upgrade_00091

* Sort set difference for unit tests, more...

Also remove unused var status.  This was used in log_msg() previously but these log_msg() have since been removed.

* _get() run return value thru make_boolean()

* test_image_upgrade_ImagePolicyAction: Standardize test names, more...

Rename mock_ image_policy_action to image_policy_action
Rename mock_issu_details to issu_details
Rename mock_image_policies to image_policies
Rename match, if if is defined globally, to match_<testcase_ID>
Add more consistent docstrings

* image_policy_action: rename build_attach_payload to build_payload

* Run thru black/isort

* Don't hardcode method names

* Provide user's value in fail_json message

* Add unit tests, run thru black/isort, more...

Add unit tests for setters:
- action
- serial_numbers

* Standardize test names

* Run thru black/isort

* Standardize test names, more...

image_upgrade_responsees_SwitchDetails.json - Remove key/values which are not used in tests.

* Run thru black/isort, doctstring cleanup

* Add property vpc_role2, more...

Two properties exist for vpc_role in the controller response.
- vpc_role2 corresponds to vpc_role.
- vpc_role corresponds to vpcRole

method_name made local in both __init__ and _init_properties

* Standardize test names

* Standardize test names, run thru black/isort

* Standardize docstrings

* Run thru black/isort

* Add TEST_NOTES and remove unused entries

* Make global var name unique

* Add copyright and attribution

* Remove unused import

* Standardize test names, remove unused fixtures, more...

image_upgrade_common.py
    - Changed self.debug to False
    - Changed logfile name to match collection name

* Add TEST_NOTES to _handle_delete_post_put_response fixtures

* Remove unused file

* Detach policy only if have policy matches want policy

* Run thru pylint

* Run thru black/isort

* Run thru black/isort/pylint

* Run thru black, isort, pylint, make method_name local to functions

* Update docstrings

* Removed unused fixtures file

* Remove unused payloads file

* Remove unused configs file

* Add a TODO for a future test

* Update docstrings

* Update docstrings, rename error_message to match

* Update docstrings

* Run thru black, isort, pylint

* Add docstrings

* Add docstrings

* Modify boolean asserts to satisfy pylint

* Appease linters and ansible sanity (part 1)

* Fix imports

* Fixes, hardening, reduce unit test code duplication, more...

- Consolidate fixtures and file loaders into image_upgrade_utils.py
- Harden fixture.py
- Fix EPLD idempotence issue
- Fix merge of switch and global configs
- Add initial unit tests for ImageUpgradeTask (to verify above fix for merging configs)
- Modifications to appease pylint
- More work toward passing Ansible sanity (more TODO though)
- Temporarily add debugging statements throughout

* Use load_playbook_config for all inputs, more...

- More cleanup for pylint
- image_upgrade.py - Add comment about key misspelling
- Update TEST_NOTES for some inputs
- Run thru black, isort, pylint

* merge_dicts() handle case where no suboptions are specified in options dict

There's a case where the user specifies an options dict (e.g. "upgrade"), but does not include the suboptions.  This results in the options dict given a value of null when converting from YAML to a Python dict.  Rewrite merge_dicts() to handle this case.

Also, moved merge_dicts to image_upgrade_common.py so it can be shared in the future.

Lastly, so that all switch config processing is performed in one place, moved _merge_defaults_to_switch_config() from image_upgrade.py (ImageUpgrade class) to dcnm_image_upgrade.py (ImageUpgradeTask class).  These changes required modifying/moving several unit tests.

* ansible-sanity related cleanup

* Ignore missing-gplv3-license for dcnm_image_upgrade.py

* Move all parameter validation to ParamsValidator class

The goal here is to separate parameter validation from things like applying defaults and to isolate it so that it's separately testable.

A few testcases need to be moved/rewritten as a result of this commit.  These are now commented out:

test_image_mgmt_upgrade_task_00003
test_image_mgmt_upgrade_task_00004
test_image_mgmt_upgrade_task_00005

* Decouple _merge_defaults_to_switch_configs from _merge_global_and_switch_configs

Make _merge_global_and_switch_configs self-contained
ParamsValidator: Add ability for a param to match more than one type

These changes required changes to the unit tests associated with _merge_defaults_to_switch_configs

* Add integer range validation

* Use copy.copy() when assigning need to self.need

* sentence should be on a single line

Addressing mikeweibe review comment

* Expanded on meaning of validate

Addressing mikewiebe review comment.

* Fix copywrite

Addressing mikewiebe review comment

* Align epld description with current playbook structure

Addressing mikewiebe review comments.

* Fix nxos options in two places in example playbook

Addressing mikewiebe review comments.

* Leverage ansible.module_utils.common validation

* Decouple ParamsValidate from ImageUpgradeCommon

For now, copy log_msg() into ParamsValidate, along with self.debug and self.file_handle.

Eventually, we should create a common logging infra that contains log_msg(), a var to enable/disable logging, and a common destination log file.

* Run thru black/isort

* isolate self.need from need using copy.copy()

* rename test file (remove extraneous "image_upgrade")

* Initial set of unit tests for ParamsValidate, more...

Also, hardening of ParamsValidate class:

ParamsValidate.params_spec - Add basic verifications
ParamsValidate.parameters - Add basic verifications
ParamsValidate.verify_type() - fix unbound local variable error
ParamsValidate.verify_multitype() - no need for var 'error', removed it

* add assert for mandatory_param_spec_keys

* Update or add copyright header, more...

Also:

dcnm_image_upgrade/fixture.py
- change "f" to "file_handle" to appease pylint.

All unit tests:
- Run thru black / isort / pylint linters

* Align fail_json messages to suggest similar debugging steps.

* Add unit tests for multitype and integer range, more...

Run through black/isort/pylint and address some pylint issues.

* Fix assert expectation

After modifying ParamsValidate to leverage validate's conversions, the assert needed to check for int 1 rather than str "1"

* Add required 'preferred_type' param for multi-type, more...

1. params_validate.py - If 'type' param is a list of types (i.e. multi-type), require that the caller has set 'preferred_type' to indicate what type they prefer param to be.  We then do a best-effort attempt to conver param to that type.  If this conversion fails, we  call _verify_multitype() so that param will be converted to whatever type succeeds first.  If no types succeed, then we fail_json.

2. params_validate.py - Added function ipaddress_guard() to guard against the ipaddress module doing undesired conversions of int and bool types.

3. dcnm_image_upgrade.py - added 'preferred_type' to params_spec, per addtions in item 1 above.

4. modified some unit-tests and fixtures, per additions in item 1 above.

* Harden ParamsValidate and add more unit tests, more...

This should be the last modification to ParamsValidate.  It turns out that, while Github Co-Pilot generated a "correct" version of self._verify_multitype(), it was anything but elegant.  This has been rewritten to (hopefully) be more understandable and maintainable.  Same goes for self._verify_type()

* Run thru black/isort.

* ParamsValidate hardening, add type check, more...

ParamsValidate:
- verify that range_min and range_max are integers before using them.
- update example usage to include range_min and range_max.
- privatize a few formerly-public methods.

test_image_upgrade_params_validate.py
- Add negative test to verify non-integer handling for range_min and range_max.
- Modify match for a few tests to match private method names
- Modify a couple mark.parametrize structures to include range_min and range_max

* Add properties to enable/disable debugging and set the logfile name, more...

ParamsValidate:
- Add ability to enable/disable logging on the fly
- Add more reserved parameters to self.reserved_params (length_max and no_log)
- Refactor __init__()

test_image_upgrade_params_validate.py
- Modify asserts in test_params_validate_00001 to reflect the above changes

* Run thru black and isort

* Appease ansible sanity tests, more...

Fix pep8 and missing __metaclass__ Ansible sanity errors.

I guess there's a disagreement between Ansible sanity and pylint regarding __metaclass__ = type.  To appease Ansible sanity, I'll ignore local pylint errors regarding this and modify my pylint config to ignore the error.

Also, remove all module docstrings since these seem not to have a good place to go without causing Ansible sanity errors (per pylint, a module docstring is supposed to go at the very top of the file, but this causes Ansible sanity to complain about boilerplate).

* ImageValidate: add unit tests for instance.commit()

* ImageValidate: additional unit tests, more...

ImageValidate
- isinstance(True, int) ends up being True, so we needed to add a check for isinstance(value, bool) to a couple properties.
- Added unit tests for commit() and for several properties

* ImageUpgradeTask: leverage ParamsMergeDefault class

1. A few boilerplate lines were updated.

2. ImageUpgradeTask
- Previously, we were creating a defaults dictionary which we used to update the merged config.  This was not optimal since we were, in essence, duplicating information about default values for parameters that is already in a properly-written params_spec.

Wrote ParamsMergeDefaults class which takes a params_spec, and a playbook config and updates the playbook config to add any missing parameters that have a default value.

This required re-writing some unit tests.  And, as a result of using this, a errors were found in the params_spec for image_upgrade (the following were not specified: reboot, upgrade.epld, upgrade.nxos).

* Copy dcnm_inventory action plugin and modify for dcnm_image_upgrade

* Appease pycodestyle pep8 issues in sanity run

* Fix pylint disallowed-name error in sanity tests

* Fixes for Undefined variable '__class__' pylint errors

* Remove pylint disable=consider-using-with which doesn't seem compatible with earlier pylint

* Add docs for dcnm_image_uupgrade

* Fix typo in validate() fail_json message, add related unit tests

* Use a common class for log messages

* Move merge_dicts() to a common class MergeDicts, more...

Move merge_dicts() out of ImageUpgradeCommon() and into its own class that can be shared with other modules.

ImageUpgradeTask now leverages MergeDicts()

Also:

- ImageUpgradeTask(): remove unused self.mandatory_global_keys

- ImageUpgradeCommon(): remove self.method_name from a couple functions (these should have been non-global assignments and were obscuring the method names of functions that called these).

- ImageValidate: Update unit tests to expect the proper method names (per above change)

- test_image_mgmt_upgrade_task_00001: remove verifications for items no longer present in __init__.

- Cleanup a few comments here and there.

* Move unit tests for common classes out of image_upgrade

ControllerVersion and Log were initially written as specific to image_upgrade, and their unit tests lived in:

tests/unit/modules/dcnm/dcnm_image_upgrade

These classes have moved to plugins/module_utils/common and so we've moved their unit test scripts, fixtures, and other supporting utilities to:

tests/unit/module_utils/common

* Don't abbreviate import

* MergeDicts: add unit tests, simplify logic

* Additional unit test

* ImageValidaate: move refresh() out of for loop

ImageValidate.prune_serial_numbers: issu_detail.refresh() should be called only once outside the for loop.

* ParamsMergeDefaults: use dict.get() rather than "not in"

For reasons I don't understand, using:

"if <key> not in <dict>"

was failing idempotence for keys with null values that have a default value of dict i.e. key["default"] = {}.  After changing this to use:

"if dict.get(<key>, None) is None"

things are working for this specific case.

* ImageInstallOptions: Avoid NDFC error

NDFC returns an error when all of issu, epld, and package_install are False.

Avoid this by returning a mocked NDFC response indicating that nothing needs to be done.  This makes the change transparant to anything outside this class.

While we're modifying this class, fix a couple pylint errors and inappropriate assignments of self.method_name.

* Add unit test to cover the changes in the last commit

* rename ParamsValidate.validate() to ParamsValidate.commit()

For consistency with other classes...

* Changing behavior of ImagePolicies, more...

ImagePolicies() called fail_json() when the caller asked for a policy_name that did not exist on the controller.

During initial work on dcnm_image_policy module, we need this to return None instead.  Hence, changing the behavior.

This does not require any other changes to dcnm_image_upgrade, other than modifying one of the unit tests to expect the new behavior.

* Add a failed_result to all fail_json calls, more...

Add failed_result property to ImageUpgradeCommon.  Modify all fail_json() calls to add this result.  This is the first phase of adding proper results to the dcnm_image_upgrade module.  Phase 2 will add a Result() class and the needed infra to return proper success results.

image_policies.py in class ImagePolicies, add property all_policies which return the current image policies on the controller.  This was needed for dcnm_image_policy module.

* Fix mocked fail_json() to add **kwargs required by last commit.

* Fix ansible-sanity complaints

* Fix ansible-sanity errors

* SwitchIssuDetails: change filtering key name, more...

In preparation for refactoring, we are changing the key name used to set which device's information we retrieve.

Previously, each of the subclasses used a different key name:

SwitchIssuDetailsByIpAddress used ip_address
SwitchIssuDetailsBySerial_number used serial_number
SwitchIssuDetailsByDeviceName used device_name

We have changed the above such that all of them now use "retrieval_key" to set the filter.  This will allow us to refactor now only this class, but also we'll be able to refactor _wait_for_current_actions_to_complete() from ImageStage(), ImageUpgrade() and ImageValidate() into a single function in ImageUpgradeCommon()

* Change "retrieval_key" to "filter", more...

Since these classes already have a "filtered_data" property which is used to return the data filtered by "retrieval_key", it's more intuitive to change the name of  "retrieval_key" to "filter".

* New logging system. New result system, more...

1. Added a common logging facility leveraging Ansible's logging.config.dictConfig so that logging can be changed based on the contents of a YAML file.  Default (log.config = None) is not to log (i.e. the root logger is disabled).  An example log.yaml config file is provided in module_utils/common/log.yaml.

2. Added a Result() class (using this also for the dcnm_image_policy module) and made the necessary modifications to leverage this.

3. Various other cleanup.

* Forgot to add module_utils/common/result.py

* Switching to JSON for the logging config, more...

Opps.  YAML is not natively supported by Python.   While it would be nicer to be able to use a YAML file for configuring the logging system, we'd have to update requirements.txt to include PyYAML, which wouldn't be good.

Using JSON instead since it's build-in.

* Appease the linters

* Fix pylint error for a couple earlier python versions

Looks like bare __class__.__name__ doesn't work for some  python versions.

In all classes, changed:

self.class_name = __class__.__name__

To:

self.class_name = type(self).__name__

* Fix a few things found with local linter, more...

Also:

1. After some testing, it appears that the following are equivalent

self.class_name = type(self).__name__
self.class_name = self.__class__.__name__

The second one seems more readable to me, so I changed things back.

The issue I was trying to fix was that, when subclasses call super() on their parent class(es), the parent logs the subclass's class name.  After a lot of testing, I think this is just the way things work.  To make the logs more clear, I added an explicit (hardcoded) class name in the "ENTERED" debug log each class's __init__() method.  This way, it's much clearer when a subclass calls super() on it's parent class.

2. Added encoding="utf-8" to the open() call in log.py to satisfy (local) pylint.

3. Added better docstring to the Log().commit() method.

4. Added logging to some classes that I'd missed (mostly in module_utils/common/).

* Remove unit test for MergeDicts.debug @Property

* Modify unit tests for the new logger implementation

1. Remove log tests from test_params_validate.py

2. Rewrite test in test_log.py

* Fix pylint f-string errors

* Enhance playbook results

1. Enhance result for "deleted" state.  Per-switch result for "deleted" state now includes action, ip_address, logical_name, policy, and serial_number.

2. Add response list to Result class and update "deleted" state responses to include per-policy response.

3. Update "query" state results to include controller responses.

TODO: Need to update merged state results...

* Fix pylint error

* Result handling for merged state

* Fix unit tests to reflect changes in the last commit

* Remove one call to issu_details.refresh() in _build_payload(), more...

Also:

1. Add debug logs in for all refresh() calls (for all instances that touch the controller) in image_upgrade.py

2. Fix typo in docstring for filtered_data property

3. Update key for unit test: test_image_mgmt_upgrade_00004

4. Comment out log.config assignment in dcnm_image_upgrade.py to disable logging

* Remove a TODO that was harder than I thought

1. Getting rid of one extra call to issu_detail.refresh() isn't worth the effort given that we call issu_detail.refresh() many times within the for loops (by nesessity) in both _wait_for_current_actions_to_complete() and _wait_for_image_upgrade_to_complete().

2. Rearranged lines in test_image_mgmt_upgrade_00004 (cosmetic change).

* Modified returned result not to use ansible state names

In preparation for writing integration tests, I realized that the returned results weren't very good.

There wasn't a clean way to categorize the result sections by Ansible state (at least I couldn't think of one).

1. Deleted state detaches image policies from switches
2. Merged state attaches policies, stages and validates images, and upgrades switches.
3. Query state simply returns the current ISSU state of the switch(es).

Given that the above don't really fix neatly into similar per-state actions, I removed Ansible state names from result section names and, instead, used names that describe what is being returned.

So, we have the following subsections in both the "diff" and "response" sections:

attach_policy
detach_policy
issu_status
stage
upgrade
validate

* Fix linter shebang error

* Fix trailing whitespace linter error

* Improve ImageStage results and logging, more...

1. Sort todo and done sets when logging them so that serial_numbers/ip_addresses line up within the log.

2. Improve ImageStage diff to include the same info as other diffs (ip, serial, logical name, action).

3. TODO: need the same diff mods for ImageValidate...next commit...

4. Remove failed_result propery from ImageUpgradeCommon since we're using ImageUpgradeTaskResult() for that now.

* Consistent diff between ImageStage and ImageValidate

1. Align the diff returned by ImageStage and ImageValidate

2. ImageStage, improve the response when no serial numbers need to be processed.

3. ImageValidate, return a response when no serial numbers need to be processed (similar code to 2, above).

4. ImageValidate, update unit test to reflect different response from ImageValidate in the case where no serial_numbers need to be processed.

* Refactor, more...

1. ImageUpgradeTask: refactor _build_policy_attach_payload(), _self_policy_attach_payload(), and handle_deleted_state() into a single method _attach_or_detach_policy()

2. ImageUpgradeTask: Fix _failure() and remove docstring contents that described the issue.

3. Modify test_image_mgmt_upgrade_task_00001 to remove assert for instance.payloads as this is no longer needed

4. test_image_upgrade_validate.py: run through black and isort

5. ImagePolicyAction() : modify to store list of diffs and leverage this in ImageUpgradeTask() when generating results.

6. ImagePolicyAction().validate_request() - add check for image policy does not exist on the controller.

7. ImagePolicyAction() - Add typing for all dict assignments.

* Add type defininitions for dicts, more...

1. ImagePolicyAction: Add type definitions for dicts

2. ImageStage, ImageValidate, ImageUpgrade: Add clarifying comments

* Fix linter pycodestyle linter error

* Initial integration test for "merged" state

* Appease YAML linter

* IT: Fix jinja2 warning, add "deleted" state test, more...

1. Fixed the following with the merged.yaml role:

[WARNING]: conditional statements should not include jinja2 templating delimiters ...

2. Adding deleted.yaml role

3. Fix ImageUpgradeTask detach_policy result (found with integration test for "deleted" state)

* skip query state diffs when determining if anything changed

* Add "query" state integration test

* Add loop around dcnm_send to wait for successful response.

We've seen cases were a 500 response is returned if install-options is called too early.  Add a loop to try for self.timeout  seconds with 5 second poll time.  Update unit tests to bypass the sleep so that the tests run faster.

* Remove test_image_mgmt_validate_00020

We''re now initializing self.serial_numbers to [] instead of None and silently returning if self.serial_numbers is [] in commit().  Previously, we would fail_json if self.serial_numbers was still set to None in commit().  We no longer need the unit tests that verifies fail_json is called.

* ImageUpgradeTaskResult: Remove redundant class definition.

* Break "merged" state integration tests into two files, more...

Rename ansible_switchX to ansible_switch_X
Update comments and task names to include test stage

* ImageUpgradeTask: Fix idempotence for merged state

Fix an intermittent idempotence issue found by the integration tests.

have.status was sometimes reported as "Not-In-Sync" which caused upgrade.nxos to be set to True, which cause unnecessary upgrade.  The fix was to remove have.status from consideration.  We now consider only have.reason, have.policy, and have.upgrade.

* Move timeout and unit_test properties to ImageUpgradeCommon

As a first step, in preparation for moving safe_send() to ImageUpgradeCommon, move two properties that safe_send() relies on.

* Add safe_send() method to call dcnm_send with a retry and timeout mechanism

We were experiencing timeouts and intermittent 500 responses when using dcnm_send() directly.  Hence, adding a safe_send() method to retry dcnm_send() until we get a 200 response or until a timeout is exceeded.

Will move this to ImageUpgradeCommon later.

* Add recent runtimes to integration tests

* Forgot to commit changes to unit test.

* Fix PEP8 issue with inline comment

* Cleanup result names

Be explicit with result names.

1. ImageUpgradeTask: rename self.result to self.task_result and update related unit test.

2. ImageUpgradeCommon.failed_result: No need to instantiate ImageUpgradeTaskResult since we're only using one of its properties.

3. Move module_utils/common/result.py to ImagePolicy module. It wasn't really a good idea to try to develop a generic result class given the differences in results between the different modules.  common/result.py is currently specific to ImagePolicy

* Move ImagePolicyAction.safe_send to ImageUpgradeCommon.dcnm_send_with_retry

Moving to dcnm_send_with_retry to ImageUpgradeCommon so that other classes can leverage it.

* ImagePolicies() - initialize response data to {}, more...

Also, update some comments throughout.

* Use dcnm_send_with_retry in more classes

Use ImageUpgradeCommon().dcnm_send_with_retry() in two more classes.

1. ImageUpgrade()
2. ImagePolicyAction()

- Update ImageUpgradeTask() to use ImagePolicyAction().response_current

- Update unit tests for ImageUpgrade() and ImagePolicyAction

- ImageUpgrade: remove result and response properties and inherit from ImageUpgradeCommon.

- ImageUpgrade: move devices verification from commit() to _validate_devices()

- ImageUpgradeTaskResult: minor docstring edit to fix typo

- ImageUpgradeCommon: add response_current and result_current properties.

- ImagePolicyAction: Remove local response and result property definitions and inherit from ImageUpgradeCommmon

- ImagePolicyAction: Use response_current and result_current as appropriate

- ImagePolicyAction: Add a setter for query_result property

* Fix PEP8 error (need two black lines)

* Remove unused dcnm_send import

* Add class RestSend(), more...

Replacing ImageUpgradeCommon.dcnm_send_with_retry() with RestSend() class.  The problem with the former has to do with pytest patching.  For some tests, we need to patch dcnm_send() to mock different responses for each of the subclasses of ImageUpgradeCommon.  Since the patch path was identical, there was no way to do this.

The solution is to move this to a separate class (RestSend) which is then imported into subclasses of ImageUpgradeCommon.  This provides the different patch paths that we need to provide appropriate mock responses to each subclass.

Other changes:

1. The above required extensive changes for unit tests of ImageUpgrade (the first subclass we've modified to use RestSend).  Other subclasses will be similarly modified later.

2. Run test_image_upgrade_image_policy_action.py through black/isort

3. InstallOptions:  Move validation of refresh parameters out of refresh() and into a separate method.  Modify unit tests appropriately.

4. InstallOptions: Fix use of self.result, self.result_current, self.response, self.response_current

5. InstallOptions: Remove timeout and unit_test properties (these are moved to RestSend).  Remove response and result properties (these are inherited from ImageUpgradeCommon).  Fix raw_response property to alias response_current.

6. ImageUprade: Use RestSend.

7. ImageUpgradeCommon: Added a lot of debugging to dcnm_send_with_retry()

* Fix PEP8 whitespace error

* Fix PEP8 import handling error

* Fix PEP8 linespace error, move MockRestSend to image_mgmt for now

* Move MockRestSend into the image management unit test directory

* Fix f-string error

* Remove need for MockRestSend

Figured out a working patch path.

* ImageUpgrade: instantiate RestSend in __init__, more...

ImageUpgrade.commit(): populate self.result, self.result_current, self.response, self.response_current, self.response_data with data received from RestSend.

* ImageStage: Leverage RestSend(), more...

- ImageStage: remove response, response_data, result properties (inherit from ImageUpgradeCommon)

- ImageStage: modify debug statements to use json.dumps for prettier output

- ImageStage: Use RestSend() class

- ImageUpgradeCommon: Add response_data getter/setter properties

- ImageUpgradeTask: harden handling of ImageStage.response

- test_image_upgrade_image_stage.py: modify unit tests to reflect the above changes

* Add unit test for ImageStage for len(serial_num) == 0

* Fix PEP8 errors in sanity tests

* ImageStage: Add unit test for 500 response

* ImageStage: add unit tests, more...

Add unit tests for the following properties:

- check_interval
- check_timeout
- serial_numbers

* ImageValidate: leverage RestSend, more...

Also:

ImageValidate: hardening for check_interval and check_timeout setters.
ImageValidate: Remove response_data, result, and response propoerties (these are now inherited from ImageUpgradeCommon)

test_image_upgrade_image_validate.py: modifications for changes made above.

* SwitchDetails: Leverage RestSend

Also:

RestSend: Add getter/setter property for payload
RestSend: Remove a couple debug statements

ImageStage: improve fail_json messages for check_interval and check_timeout properties

Update the following unit test files to reflect the above changes:
- test_image_upgrade_switch_details.py
- test_image_upgrade_image_policy_action.py

Minor update to docstring for test_image_mgmt_stasge_00010 in:
- test_image_upgrade_image_stage.py

* response and result properties should be inherited

- SwitchIssuDetails: inherit response* and result* properties from ImageUpgradeCommon

- ImageUpgrade: inherit response_data from ImageUpgradeCommon

- ImageUpgrade: reorder some items in __init__()

- ImageUpgrade: add some debug statements for result, response, response_data, etc

- ImageUpgrade: Modify fail_json conditional to read self.result_current rather than rest_send.result_current

- ImageUpgradeTask: pop "DATA" only if present in response_current

- ImageStage: move debug message

- ImagePolicies: fix typo in fail_json message

- Update unit tests to reflect above changes

- Run everthing through black/isort

* Fix ansible-sanity PEP8 issues

* Increase example logging_config.json maxBytes from 500K to 50M

* Update comments to include correct testcase name

* Fix reference to non-existing property

* Disable logging

* SwitchIssuDetails: rename refresh() to refresh_super(), more...

SwitchIssuDetails: rename refresh() to refresh_supper() so that subclasses can call it from within their local refresh() methods (previously, they called super().refresh()).

ImageValidate.commit(): don't upgrade property values directly, use inherited properties instead.

Update unit tests to reflect the above changes.

* ImagePolicies: do not fail_json if length of DATA.lastOperDataObject == 0

While writing unit tests for dcnm_image_policy module, found that we cannot fail_json if no policies are present on the controller.  Modified ImagePolicies() class accordingly, as well as unit test for this case.

* Don't call refresh() more than needed.

- ImageStage.prune_serial_numbers: Call refresh() outside of for loop.
- ImageStage.validate_serial_numbers: Call refresh() outside of for loop.

The above could be made even more efficient by calling refresh() in commit().  This would require rethinking a couple unit tests, so will leave this for a future commit.

Also:

- test_image_upgrade_image_stage:  Add a Summary section which provides the "gist" of a test case.

* Fix pep8 too many blank lines

* rename image_mgmt directory to image_upgrade

Background:

Initially, image_mgmt was going to hold the module_utils for image_policy, image_upgrade, and image_upload.  I never coordinated with Mallik on this, and later decided it was a bit of organizational overkill.

This commit comprises the following:

1. Renaming directory module_utils/image_mgmt to module_utils/image_upgrade.
2. Modifying all python imports that point to module_utils/image_mgmt, to point to module_utils/image_upgrade.
3. Modifying all unit test names to replace image_mgmt with image_upgrade
4. Changing key names for all the unit tests to replace image_mgmt with image_upgrade.

* ImageStage: Add unit test for successful commit, more...

This commit brings ImageStage to 98% coverage.

Added unit test "test_image_stage_00075"

Also:

- Run unit test scripts thru black/isort/pylint

- Renumber ImageStage.commit() unit tests to be in range 00070 - 00079

- SwitchIssuDetails: Add debug message to .refresh() in all subclasses.

- ImageStage.commit: modify debug message

* Remove unused mocks

* Remove unused mocks

* Remove unused mocks, more...

Fix a couple pylint errors that were not showing up in sanity tests (redefined-builtin and invalid-name).

* Add unit test for bad result due to 500 response

* Fix pylint invalid-name errors

* Remove unused fixture, more...

controller_version_fixture was moved to tests/unit/module_utils/common many commits ago, and I forgot to remove the original.

* Fix test_log.py import of MockAnsibleModule

* Expand unit test execution to all files under tests/unit/.

.github/workflows/main.yml is currently pointing to tests/unit/modules/dcnm/.   Modified this to point to tests/unit/. instead so that unit tests in tests/unit/module_utils/common will be executed.

* ImageStage.check_interval unit test: parameter for bool case

This brings us to 99% coverage for image_stage.py

* ImageStage.check_timeout unit test: parameter for bool case

* Fix comment typos

* ImageUpgradeCommon: Add unit tests, more...

ImageUpgradeCommon: Add unit tests for dcnm_send_with_retry.

To facilitate the above, we've modified ImageUpgradeCommon.__init__() to add self.dcnm_send.  This required also modifying the patch for ImageUpgradeCommon.dcnm_send in unit test test_image_upgrade_image_policy_action_00021 .

Also:

- Added Summary to a couple test cases and other docstring improvements.
- Updated test_image_upgrade_image_upgrade_common_00001 with more asserts
- Removed old commented logging unit test code that is no longer relevant.

* Update version_added to "3.5.0"

* image_upgrade.py: 100% unit test coverage

test_image_upgrade_image_upgrade.py: Rewrote some unit tests to simplify logic and provide greater coverage.

test_image_upgrade_image_upgrade.py: Renumbered many unit tests.

image_upgrade.py: Move issu_detail.refresh() out of tight for loops in:
- ImageUpgrade()._wait_for_image_upgrade_to_complete
- ImageUpgrade()_wait_for_current_actions_to_complete

- ImageUpgrade().check_interval: Add check for bool due to isinstance() treating bool as int.
- ImageUpgrade().check_timeout: Add check for bool due to isinstance() treating bool as int.

Ran all module_utils/image_upgrade files through black/isort

* Move issu_detail.refresh() out of inner for loops

* Remove caplog from unit tests, update docstrings.

Update unit test docstrings to include class name.
Removing caplog from all unit tests where this was present (two tests).

* ImageUpgradeCommon: add unit tests

Brings ImageUpgradeCommon unit test coverage to 99%

- Updated setters which expect an int() to test first for bool().

- Added unit tests for:
  - changed
  - diff
  - failed
  - response_current
  - response
  - result_current
  - result
  - send_interval
  - timeout
  - unit_test

* ImagePolicies: Add unit tests, update docstrings, remove inherited properties

Removed the following properties which are inherited from ImageUpgradeCommon:

- response_data
- response
- result

Add unit tests for:

- ImagePolicies._get (two tests)
- ImagePolicies.all_policies (two tests)

Improve docstrings for existing tests.

* Replace (pylint) disallowed name "foo"

* Move rest_send.py to module_utils/common

RestSend will be used by both dcnm_image_upgrade and dcnm_image_policy in the future, so moving it into module_utils/common for sharing between these modules.

* Add path and verb properties, fix whitespace issue.

Also fix some debug lines that were too long.

* Use do_not_raise() to verify that fail_json isn't called.

Also:

test_image_upgrade_switch_details.py:
- Improve test docstrings

switch_details.py (SwitchDetails class):
- initialize self.path and self.verb in __init__() and use these in refresh()

* ImageUpgradeTaskResult: 100% unit test coverage

Also:

test_image_upgrade_switch_details.py: Run thru black

* Fix PEP8 sanity issue

* ImagePolicyAction: 100% unit test coverage

Also:

ImagePolicyAction._attach_policy: fix KeyError in fail_json message.

ImagePolicyAction.query_result: remove checking for value == dict since we don't know 100% what the controller will return.

ImagePolicyAction.serial_numbers: enforce list must be at least one element.

* ImageValidate: remove unused unit test

We moved initialization of self.path and self.verb to __init__(), which is covered by test_image_upgrade_validate_00001

Hence, we no longer need test_image_upgrade_validate_00021

* SwitchDetails: 100% unit test coverage

Also:

SwitchDetails.info: self.propertiies should be self.properties

test_image_upgrade_switch_details.py: Fix a few docstring typos

* SwitchIssuDetailsBy*: 100% unit test coverage

* Shaved a couple seconds off unit test runtime

* Fix PEP8 whitespace errors

* Fix PEP8 whitespace error

* ImageStage: Instantiate path and verb in __init__()

test_image_upgrade_stage_00001: add path,verb asserts

test_image_upgrade_stage_00071: remove this test case now that it's covered in 00001 above

* ImageValidate: 98% unit test coverage

ImageValidate.commit: Add unit test to verify diff
ImageValidate: Improve unit test docstrings

image_upgrade_responses_imageStage.json: Fix unit test 00075 key

ImageStage.commit: remove self.path and self.verb initializations (these are initialized in __init__()
ImageStage.commit: add return value type hint

* ImageInstallOptions: 99% unit test coverage

InstallOptions.policy_name: minor edit to fail_json message.

ImageInstallOptions.policy_name: Add unit test

test_image_upgrade_image_install_options.py:
  - rename unit test test_image_upgrade_install_options_00003 to test_image_upgrade_install_options_00070

Run all other test_image_*.py files through black/isort

* Disable logging

* Update integration tests with new include syntax

'include' is deprecated in Ansible roles and is now 'include_tasks'

* Initial check_mode changes (needs more work)

* Fix PEP8 too many blank lines

* Add support for check_mode

Also:

dcnm_image_upgrade.py: Add a boolean, enable_logging, so that users don't have to comment/uncomment lines to disable/enable logging.

Rename image_upgrade_utils.py to utils.py to align with the convention in dcnm_image_policy

Modify the match regex in several unit test cases to align with the check_mode changes.

* Fix pylint f-string interpolation errors.

* Pull in changes from dcnm_image_policy

Since dcnm_image_upgrade is the single source of truth (prior to commiting these new modules) for everything in module_utils/common, we need to update a couple files that were enhanced during development of dcnm_image_policy.

This commit removes the dependency that rest_send.py had on module_utils/image_upgrade/image_upgrade_task_result, since this file will go away in a future version of this repo.  rest_send.py now imports failed_result from module_utils/common/results.py

Also:

module_utils/params_validate.py : fix a sanity test error with type hinting

* Apply changes to results.py from dcnm_image_policy

This adds an "ok_result" property to results.py Results() class.

* Align result output with existing DCNM collection modules

This aligns the result output with existing modules.  However, it doesn't leverage the Result() class.

For expediency's sake, I'll save those changes to a later version of this module since it'll be a larger effort that would require changes to many unit tests.

* Fix yamllint "too many blank lines"
  • Loading branch information
allenrobel authored Mar 27, 2024
1 parent 0b0dc30 commit 43459d1
Show file tree
Hide file tree
Showing 80 changed files with 32,951 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ jobs:
run: ansible-galaxy collection install .cache/collection-tarballs/*.tar.gz

- name: Run DCNM Unit tests
run: coverage run --source=. -m pytest tests/unit/modules/dcnm/. -vvvv
run: coverage run --source=. -m pytest tests/unit/. -vvvv
working-directory: /home/runner/.ansible/collections/ansible_collections/cisco/dcnm
env:
PYTHONPATH: /home/runner/.ansible/collections
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Name | Description
### Modules
Name | Description
--- | ---
[cisco.dcnm.dcnm_image_upgrade](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_image_upgrade_module.rst)|Image management for Nexus switches
[cisco.dcnm.dcnm_interface](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_interface_module.rst)|DCNM Ansible Module for managing interfaces.
[cisco.dcnm.dcnm_inventory](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_inventory_module.rst)|Add and remove Switches from a DCNM managed VXLAN fabric.
[cisco.dcnm.dcnm_links](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_links_module.rst)|DCNM ansible module for managing Links.
Expand Down
Empty file added dcnm-ut
Empty file.
1,053 changes: 1,053 additions & 0 deletions docs/cisco.dcnm.dcnm_image_upgrade_module.rst

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions plugins/action/dcnm_image_upgrade.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Copyright (c) 2024 Cisco and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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.

from __future__ import absolute_import, division, print_function

__metaclass__ = type

from ansible_collections.ansible.netcommon.plugins.action.network import (
ActionModule as ActionNetworkModule,
)
from ansible.utils.display import Display

display = Display()


class ActionModule(ActionNetworkModule):
def run(self, tmp=None, task_vars=None):

connection = self._connection
persistent_connect_timeout = connection.get_option("persistent_connect_timeout")
persistent_command_timeout = connection.get_option("persistent_command_timeout")

timeout = 1000

if (persistent_command_timeout < timeout or persistent_connect_timeout < timeout):
display.warning(
"PERSISTENT_COMMAND_TIMEOUT is %s"
% str(persistent_command_timeout),
self._play_context.remote_addr,
)
display.warning(
"PERSISTENT_CONNECT_TIMEOUT is %s"
% str(persistent_connect_timeout),
self._play_context.remote_addr,
)
msg = (
"PERSISTENT_COMMAND_TIMEOUT and PERSISTENT_CONNECT_TIMEOUT"
)
msg += " must be set to {0} seconds or higher when using dcnm_image_upgrade module.".format(timeout)
msg += " Current persistent_command_timeout setting:" + str(
persistent_command_timeout
)
msg += " Current persistent_connect_timeout setting:" + str(
persistent_connect_timeout
)
return {"failed": True, "msg": msg}

if self._task.args.get('state') == 'merged':
display.warning("Upgrading switches can take a while. Please be patient...")
self.result = super(ActionModule, self).run(task_vars=task_vars)
return self.result
Empty file.
283 changes: 283 additions & 0 deletions plugins/module_utils/common/controller_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
"""
Class to retrieve and return information about an NDFC controller
"""
#
# Copyright (c) 2024 Cisco and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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.

from __future__ import absolute_import, division, print_function

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

import logging

from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \
ApiEndpoints
from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \
ImageUpgradeCommon
from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \
dcnm_send


class ControllerVersion(ImageUpgradeCommon):
"""
Return image version information from the Controller
NOTES:
1. considered using dcnm_version_supported() but it does not return
minor release info, which is needed due to key changes between
12.1.2e and 12.1.3b. For example, see ImageStage().commit()
Endpoint:
/appcenter/cisco/ndfc/api/v1/fm/about/version
Usage (where module is an instance of AnsibleModule):
instance = ControllerVersion(module)
instance.refresh()
if instance.version == "12.1.2e":
do 12.1.2e stuff
else:
do other stuff
Response:
{
"version": "12.1.2e",
"mode": "LAN",
"isMediaController": false,
"dev": false,
"isHaEnabled": false,
"install": "EASYFABRIC",
"uuid": "f49e6088-ad4f-4406-bef6-2419de914ff1",
"is_upgrade_inprogress": false
}
"""

def __init__(self, ansible_module):
super().__init__(ansible_module)
self.class_name = self.__class__.__name__

self.log = logging.getLogger(f"dcnm.{self.class_name}")
self.log.debug("ENTERED ControllerVersion()")

self.endpoints = ApiEndpoints()
self._init_properties()

def _init_properties(self):
self.properties = {}
self.properties["data"] = None
self.properties["result"] = None
self.properties["response"] = None

def refresh(self):
"""
Refresh self.response_data with current version info from the Controller
"""
path = self.endpoints.controller_version.get("path")
verb = self.endpoints.controller_version.get("verb")
self.properties["response"] = dcnm_send(self.ansible_module, verb, path)
self.properties["result"] = self._handle_response(self.response, verb)

if self.result["success"] is False or self.result["found"] is False:
msg = f"{self.class_name}.refresh() failed: {self.result}"
self.ansible_module.fail_json(msg)

self.properties["response_data"] = self.response.get("DATA")
if self.response_data is None:
msg = f"{self.class_name}.refresh() failed: response "
msg += "does not contain DATA key. Controller response: "
msg += f"{self.response}"
self.ansible_module.fail_json(msg)

def _get(self, item):
return self.make_boolean(self.make_none(self.response_data.get(item)))

@property
def dev(self):
"""
Return True if the Controller is running a development release.
Return False if the Controller is not running a development release.
Return None otherwise
Possible values:
True
False
None
"""
return self._get("dev")

@property
def install(self):
"""
Return the value of install, if it exists.
Return None otherwise
Possible values:
EASYFABRIC
(probably other values)
None
"""
return self._get("install")

@property
def is_ha_enabled(self):
"""
Return True if Controller is high-availability enabled.
Return False if Controller is not high-availability enabled.
Return None otherwise
Possible values:
True
False
None
"""
return self.make_boolean(self._get("isHaEnabled"))

@property
def is_media_controller(self):
"""
Return True if Controller is a media controller.
Return False if Controller is not a media controller.
Return None otherwise
Possible values:
True
False
None
"""
return self.make_boolean(self._get("isMediaController"))

@property
def is_upgrade_inprogress(self):
"""
Return True if a Controller upgrade is in progress.
Return False if a Controller upgrade is not in progress.
Return None otherwise
Possible values:
True
False
None
"""
return self.make_boolean(self._get("is_upgrade_inprogress"))

@property
def response_data(self):
"""
Return the data retrieved from the request
"""
return self.properties.get("response_data")

@property
def result(self):
"""
Return the GET result from the Controller
"""
return self.properties.get("result")

@property
def response(self):
"""
Return the GET response from the Controller
"""
return self.properties.get("response")

@property
def mode(self):
"""
Return the controller mode, if it exists.
Return None otherwise
Possible values:
LAN
None
"""
return self._get("mode")

@property
def uuid(self):
"""
Return the value of uuid, if it exists.
Return None otherwise
Possible values:
uuid e.g. "f49e6088-ad4f-4406-bef6-2419de914df1"
None
"""
return self._get("uuid")

@property
def version(self):
"""
Return the controller version, if it exists.
Return None otherwise
Possible values:
version, e.g. "12.1.2e"
None
"""
return self._get("version")

@property
def version_major(self):
"""
Return the controller major version, if it exists.
Return None otherwise
We are assuming semantic versioning based on:
https://semver.org
Possible values:
if version is 12.1.2e, return 12
None
"""
if self.version is None:
return None
return (self._get("version").split("."))[0]

@property
def version_minor(self):
"""
Return the controller minor version, if it exists.
Return None otherwise
We are assuming semantic versioning based on:
https://semver.org
Possible values:
if version is 12.1.2e, return 1
None
"""
if self.version is None:
return None
return (self._get("version").split("."))[1]

@property
def version_patch(self):
"""
Return the controller minor version, if it exists.
Return None otherwise
We are assuming semantic versioning based on:
https://semver.org
Possible values:
if version is 12.1.2e, return 2e
None
"""
if self.version is None:
return None
return (self._get("version").split("."))[2]
Loading

0 comments on commit 43459d1

Please sign in to comment.