From 7f53777b191e9417c46946c5c2c69c919ff3f9c2 Mon Sep 17 00:00:00 2001 From: Arun Saravanan Balachandran <52521751+ArunSaravananBalachandran@users.noreply.github.com> Date: Tue, 27 Feb 2024 02:26:26 +0530 Subject: [PATCH] Add Dell Enterprise SONiC 'image_management' module (#311) * Add Dell Enterprise SONiC 'image_management' module * Add warning for ignored options --- plugins/modules/sonic_image_management.py | 434 ++++++++++++++++++ .../sonic_image_management/defaults/main.yml | 3 + .../sonic_image_management/meta/main.yml | 5 + .../tasks/firmware_cancel.yml | 22 + .../tasks/firmware_get_list.yml | 21 + .../tasks/firmware_get_status.yml | 21 + .../tasks/firmware_install.yml | 30 ++ .../tasks/image_cancel.yml | 22 + .../tasks/image_get_list.yml | 24 + .../tasks/image_get_status.yml | 21 + .../tasks/image_install.yml | 30 ++ .../image_management.test.facts.report.yml | 9 + .../tasks/image_remove.yml | 30 ++ .../tasks/image_set_default.yml | 24 + .../sonic_image_management/tasks/main.yml | 28 ++ .../tasks/patch_get_history.yml | 21 + .../tasks/patch_get_list.yml | 21 + .../tasks/patch_get_status.yml | 21 + .../tasks/patch_install.yml | 30 ++ .../tasks/patch_rollback.yml | 30 ++ .../tasks/preparation_tests.yml | 9 + .../templates/regression_html_report.j2 | 15 + tests/regression/test.yaml | 1 + .../fixtures/sonic_image_management.yaml | 433 +++++++++++++++++ .../sonic/test_sonic_image_management.py | 275 +++++++++++ 25 files changed, 1580 insertions(+) create mode 100644 plugins/modules/sonic_image_management.py create mode 100644 tests/regression/roles/sonic_image_management/defaults/main.yml create mode 100644 tests/regression/roles/sonic_image_management/meta/main.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/firmware_cancel.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/firmware_get_list.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/firmware_get_status.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/firmware_install.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/image_cancel.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/image_get_list.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/image_get_status.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/image_install.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/image_management.test.facts.report.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/image_remove.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/image_set_default.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/main.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/patch_get_history.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/patch_get_list.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/patch_get_status.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/patch_install.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/patch_rollback.yml create mode 100644 tests/regression/roles/sonic_image_management/tasks/preparation_tests.yml create mode 100644 tests/unit/modules/network/sonic/fixtures/sonic_image_management.yaml create mode 100644 tests/unit/modules/network/sonic/test_sonic_image_management.py diff --git a/plugins/modules/sonic_image_management.py b/plugins/modules/sonic_image_management.py new file mode 100644 index 000000000..131ba869b --- /dev/null +++ b/plugins/modules/sonic_image_management.py @@ -0,0 +1,434 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2023 Dell Inc. or its subsidiaries. All Rights Reserved +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for sonic_image_management +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_image_management +version_added: '2.4.0' +short_description: Manage installation of Enterprise SONiC image, software patch and firmware updater +description: + - Manage installation of Enterprise SONiC image, software patch and firmware updater. +author: 'Arun Saravanan Balachandran (@ArunSaravananBalachandran)' + +options: + image: + description: + - Manage installation of Enterprise SONiC image. + type: dict + suboptions: + command: + description: + - Specifies the image manangement operation to be performed. + - C(install) - Install image specified by I(path). + - C(cancel) - Cancel image installation. + - C(remove) - Remove image specified by I(name). + - C(set-default) - Set the image specified by I(name) as default boot image. + - C(get-list) - Retrieve list of installed images. + - C(get-status) - Retrieve image installation status. + type: str + choices: + - install + - cancel + - remove + - set-default + - get-list + - get-status + required: true + path: + description: + - When I(command=install), specifies the path of the image to be installed. + - Path can be a file in the device (file://filepath) or URL (http:// or https://). + type: str + name: + description: + - When I(command=remove) or I(command=set-default), specifies the name of the image. + - When I(command=remove), name can be specified as C(all) to remove all images which are not current or next. + type: str + patch: + description: + - Manage installation of software patch. + type: dict + suboptions: + command: + description: + - Specifies the patch manangement operation to be performed. + - C(install) - Install patch specified by I(path). + - C(rollback) - Remove an installed patch specified by I(name). + - C(get-history) - Retrieve history of patches applied/rolled back. + - C(get-list) - Retrieve list of installed patches. + - C(get-status) - Retrieve patch installation/removal status. + type: str + choices: + - install + - rollback + - get-history + - get-list + - get-status + required: true + path: + description: + - When I(command=install), specifies the path of the patch to be installed. + - Path can be a file in the device (file://filepath) or URL (http:// or https://). + type: str + name: + description: + - When I(command=rollback), specifies the name of the patch. + type: str + firmware: + description: + - Manage installation of Firmware updater + type: dict + suboptions: + command: + description: + - Specifies the firmware updater manangement operation to be performed. + - C(install) - Stage firmware updater specified by I(path). + - C(cancel) - Cancel a pending firmware updater. + - C(get-list) - Retrieve details of pending firmware updater and result of installed firmware updater. + - C(get-status) - Retrieve firmware updater staging status. + type: str + choices: + - install + - cancel + - get-list + - get-status + required: true + path: + description: + - When I(command=install), specifies the path of the firmware updater to be staged. + - Path can be a file in the device (file://filepath) or URL (http:// or https://). + type: str +""" + +EXAMPLES = """ + +- name: Install Enterprise SONiC image + dellemc.enterprise_sonic.sonic_image_management: + image: + command: install + path: 'file://home/admin/sonic.bin' + +- name: Get image installation status + dellemc.enterprise_sonic.sonic_image_management: + image: + command: get-status + +- name: Get list of installed images + dellemc.enterprise_sonic.sonic_image_management: + image: + command: get-list + +- name: Stage a firmware updater + dellemc.enterprise_sonic.sonic_image_management: + firmware: + command: install + path: 'file://home/admin/onie-update-full.bin' + +""" + +RETURN = """ +status: + description: Status of the operation performed. + returned: when I(command) is not C(get-status), C(get-list) and C(get-history) + type: str + sample: SUCCESS +info: + description: Details returned by the specified get operation. + returned: when I(command=get-status) or I(command=get-list) or I(command=get-history) + type: dict + sample: > + { + "file-download-speed" : "106200", + "file-progress" : 100, + "file-size" : "1304997870", + "file-transfer-bytes" : "1304997870", + "install-end-time" : "1695714740", + "install-start-time" : "1695714698", + "install-status" : "INSTALL_STATE_SUCCESS", + "install-status-detail" : "Image install success", + "operation-status" : "GLOBAL_STATE_SUCCESS", + "transfer-end-time" : "1695714669", + "transfer-start-time" : "1695714657", + "transfer-status" : "TRANSFER_STATE_SUCCESS", + "transfer-status-detail" : "DOWNLOADING IMAGE" + } +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import ConnectionError +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) + + +def validate_and_retrieve_params(module, warnings): + """Validates the module parameters""" + params = {} + for category in ('image', 'patch', 'firmware'): + if module.params.get(category) and module.params[category].get('command'): + if params.get('category') is None: + params['category'] = category + params.update(module.params[category]) + else: + module.fail_json(msg="Only one image management operation can be performed at a time") + + if module.check_mode and not params['command'].startswith('get-'): + module.fail_json(msg='Only get commands are supported while using check mode, but {0} was provided'.format(params['command'])) + + if params['command'] == 'install': + if not params.get('path'): + module.fail_json(msg="{0} -> path is required when {0} -> command = install".format(params['category'])) + if params.get('name'): + warnings.append("{0} -> name is ignored when {0} -> command = install".format(params['category'])) + elif params['command'] in ('remove', 'set-default', 'rollback'): + if not params.get('name'): + module.fail_json(msg="{0} -> name is required when {0} -> command = {1}".format(params['category'], params['command'])) + if params.get('path'): + warnings.append("{0} -> path is ignored when {0} -> command = {1}".format(params['category'], params['command'])) + + return params + + +def execute_command(module, params, result): + """Executes the specified command and updates the result""" + command_map = { + 'image': { + 'install': { + 'path': 'operations/openconfig-image-management:image-install', + 'status': 'Check image -> command = get-status for image install progress' + }, + 'cancel': { + 'path': 'operations/openconfig-image-management:image-install-cancel' + }, + 'remove': { + 'path': 'operations/openconfig-image-management:image-remove' + }, + 'set-default': { + 'path': 'operations/openconfig-image-management:image-default' + }, + 'get-status': { + 'path': 'data/openconfig-image-management:image-management/install/state', + 'response_key': 'openconfig-image-management:state' + }, + 'get-list': { + 'path': 'data/openconfig-image-management:image-management', + 'response_key': 'openconfig-image-management:image-management' + } + }, + 'patch': { + 'install': { + 'path': 'operations/openconfig-image-management:do-patch-install', + 'status': 'Check patch -> command = get-status for patch install progress' + }, + 'rollback': { + 'path': 'operations/openconfig-image-management:do-patch-rollback', + 'status': 'Check patch -> command = get-status for patch rollback progress' + }, + 'get-history': { + 'path': 'data/openconfig-image-management:patch-management/patch-history', + 'response_key': 'openconfig-image-management:patch-history' + }, + 'get-status': { + 'path': 'data/openconfig-image-management:patch-management/patch-install', + 'response_key': 'openconfig-image-management:patch-install' + }, + 'get-list': { + 'path': 'data/openconfig-image-management:patch-management/patch-list', + 'response_key': 'openconfig-image-management:patch-list' + } + }, + 'firmware': { + 'install': { + 'path': 'operations/openconfig-image-management:do-fwpkg-install', + 'status': 'Check firmware -> command = get-status for firmware package staging progress' + }, + 'cancel': { + 'path': 'operations/openconfig-image-management:do-fwpkg-install-cancel' + }, + 'get-list': { + 'path': 'data/openconfig-image-management:fwpkg-management', + 'response_key': 'openconfig-image-management:fwpkg-management' + }, + 'get-status': { + 'path': 'data/openconfig-image-management:fwpkg-management/fwpkg-install', + 'response_key': 'openconfig-image-management:fwpkg-install' + } + } + } + + path = command_map[params['category']][params['command']]['path'] + if params['command'].startswith('get-'): + method = 'GET' + request = [{'path': path, 'method': method}] + + try: + response = edit_config(module, to_request(module, request)) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + + info = {} + response = response[0][1].get(command_map[params['category']][params['command']]['response_key']) + if response: + if params['category'] == 'image': + if params['command'] == 'get-list': + if response.get('global') and response['global'].get('state'): + if response['global']['state'].get('current'): + info['current'] = response['global']['state']['current'] + if response['global']['state'].get('next-boot'): + info['next'] = response['global']['state']['next-boot'] + if response.get('images') and response['images'].get('image'): + info['available'] = [] + for element in response['images']['image']: + if element.get('image-name'): + info['available'].append(element['image-name']) + + elif params['command'] == 'get-status': + keys = list(response.keys()) + info.update(response) + install_status = info.get('install-status', 'IDLE') + transfer_status = info.get('transfer-status', 'IDLE') + for key in keys: + if ((key.startswith(('file', 'transfer')) and 'IDLE' in transfer_status) + or (key.startswith('install') and 'IDLE' in install_status)): + del info[key] + + elif params['category'] == 'patch': + if params['command'] in ('get-history', 'get-list'): + info_key = params['command'].split('-')[1] + if response.get('patch'): + patches = sorted(response['patch'], key=lambda item: (item['patch-time']), reverse=True) + info[info_key] = [] + for patch in patches: + if patch.get('state'): + info[info_key].append(patch['state']) + + elif params['command'] == 'get-status': + install_state = response.get('install-state', {}) + download_state = response.get('download-state', {}) + if install_state.get('trigger') == 'install' and 'IDLE' not in download_state.get('transfer-status', 'IDLE'): + info.update(download_state) + + for oper_type in ('install', 'rollback', 'recovery'): + if 'IDLE' not in install_state.get('{0}-status'.format(oper_type), 'IDLE'): + for key in install_state.keys(): + if key.startswith(oper_type): + info[key] = install_state[key] + + elif params['category'] == 'firmware': + if params['command'] == 'get-list': + for info_key in ('pending', 'result'): + key = 'fwpkg-{0}'.format(info_key) + if response.get(key) and response[key].get('fwpkg'): + info[info_key] = [] + for entry in response[key]['fwpkg']: + info[info_key].append(entry['state']) + + elif params['command'] == 'get-status': + if response.get('download-state') and 'IDLE' not in response['download-state'].get('transfer-status', 'IDLE'): + info.update(response['download-state']) + if response.get('stage-state') and 'IDLE' not in response['stage-state'].get('stage-status', 'IDLE'): + info.update(response['stage-state']) + + result['info'] = info + + else: + method = 'POST' + payload = {'openconfig-image-management:input': {}} + if params['category'] == 'image': + if params['command'] == 'install': + payload['openconfig-image-management:input'] = {'image-name': params['path']} + elif (params['command'] == 'remove' and params['name'] != 'all') or params['command'] == 'set-default': + payload['openconfig-image-management:input'] = {'image-name': params['name']} + elif params['category'] == 'patch': + if params['command'] == 'install': + payload['openconfig-image-management:input'] = {'patch-name': params['path'], 'skip-image-check': ''} + elif params['command'] == 'rollback': + payload['openconfig-image-management:input'] = {'patch-name': params['name']} + elif params['category'] == 'firmware': + if params['command'] == 'install': + payload['openconfig-image-management:input'] = {'fwpkg-name': params['path']} + + request = [{'path': path, 'method': method, 'data': payload}] + try: + response = edit_config(module, to_request(module, request)) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + + status = '' + response = response[0][1].get('openconfig-image-management:output') + if response: + if response['status'] != 0: + status = response['status-detail'] + else: + status = command_map[params['category']][params['command']].get('status', response['status-detail']) + + result['status'] = status + + +def main(): + """ + Main entry point for module execution + """ + argument_spec = { + 'image': { + 'type': 'dict', + 'options': { + 'command': { + 'type': 'str', + 'required': True, + 'choices': ['install', 'cancel', 'remove', 'set-default', 'get-list', 'get-status'] + }, + 'name': {'type': 'str'}, + 'path': {'type': 'str'} + } + }, + 'patch': { + 'type': 'dict', + 'options': { + 'command': { + 'type': 'str', + 'required': True, + 'choices': ['install', 'rollback', 'get-history', 'get-list', 'get-status'] + }, + 'name': {'type': 'str'}, + 'path': {'type': 'str'} + } + }, + 'firmware': { + 'type': 'dict', + 'options': { + 'command': { + 'type': 'str', + 'required': True, + 'choices': ['install', 'cancel', 'get-list', 'get-status'] + }, + 'path': {'type': 'str'} + } + } + } + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + warnings = [] + result = {'changed': False, 'warnings': warnings} + + params = validate_and_retrieve_params(module, warnings) + execute_command(module, params, result) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/tests/regression/roles/sonic_image_management/defaults/main.yml b/tests/regression/roles/sonic_image_management/defaults/main.yml new file mode 100644 index 000000000..9862ff62e --- /dev/null +++ b/tests/regression/roles/sonic_image_management/defaults/main.yml @@ -0,0 +1,3 @@ +--- +ansible_connection: httpapi +module_name: image_management diff --git a/tests/regression/roles/sonic_image_management/meta/main.yml b/tests/regression/roles/sonic_image_management/meta/main.yml new file mode 100644 index 000000000..d0ceaf6f5 --- /dev/null +++ b/tests/regression/roles/sonic_image_management/meta/main.yml @@ -0,0 +1,5 @@ +--- +collections: + - dellemc.enterprise_sonic +dependencies: + - { role: common } diff --git a/tests/regression/roles/sonic_image_management/tasks/firmware_cancel.yml b/tests/regression/roles/sonic_image_management/tasks/firmware_cancel.yml new file mode 100644 index 000000000..36a08b853 --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/firmware_cancel.yml @@ -0,0 +1,22 @@ +--- +- name: Test case - firmware cancel + dellemc.enterprise_sonic.sonic_image_management: + firmware: + command: 'cancel' + register: result + ignore_errors: yes + +- ansible.builtin.assert: + that: + - result.failed == false + - result.status is defined + - result.status == 'SUCCESS' + register: assert_result + ignore_errors: yes + +- ansible.builtin.include_tasks: image_management.test.facts.report.yml + vars: + test_case_name: 'firmware_cancel' + test_case_input: + firmware: + command: 'cancel' diff --git a/tests/regression/roles/sonic_image_management/tasks/firmware_get_list.yml b/tests/regression/roles/sonic_image_management/tasks/firmware_get_list.yml new file mode 100644 index 000000000..87578ffff --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/firmware_get_list.yml @@ -0,0 +1,21 @@ +--- +- name: Test case - firmware get-result + dellemc.enterprise_sonic.sonic_image_management: + firmware: + command: 'get-list' + register: result + ignore_errors: yes + +- ansible.builtin.assert: + that: + - result.failed == false + - result.info is defined + register: assert_result + ignore_errors: yes + +- ansible.builtin.include_tasks: image_management.test.facts.report.yml + vars: + test_case_name: 'firmware_get_list' + test_case_input: + firmware: + command: 'get-list' diff --git a/tests/regression/roles/sonic_image_management/tasks/firmware_get_status.yml b/tests/regression/roles/sonic_image_management/tasks/firmware_get_status.yml new file mode 100644 index 000000000..f28832023 --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/firmware_get_status.yml @@ -0,0 +1,21 @@ +--- +- name: Test case - firmware get-status + dellemc.enterprise_sonic.sonic_image_management: + firmware: + command: 'get-status' + register: result + ignore_errors: yes + +- ansible.builtin.assert: + that: + - result.failed == false + - result.info is defined + register: assert_result + ignore_errors: yes + +- ansible.builtin.include_tasks: image_management.test.facts.report.yml + vars: + test_case_name: 'firmware_get_status' + test_case_input: + firmware: + command: 'get-status' diff --git a/tests/regression/roles/sonic_image_management/tasks/firmware_install.yml b/tests/regression/roles/sonic_image_management/tasks/firmware_install.yml new file mode 100644 index 000000000..4d691615c --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/firmware_install.yml @@ -0,0 +1,30 @@ +--- +- name: Test case - firmware install + dellemc.enterprise_sonic.sonic_image_management: + firmware: + command: 'install' + path: 'file://tmp/test.bin' + register: result + ignore_errors: yes + +- ansible.builtin.set_fact: + result_msg: "{{ result.msg | from_yaml }}" + when: result.msg is defined + +- ansible.builtin.assert: + that: + - result.failed == true + - result.msg is defined + - result_msg['code'] == 400 + - result_msg['ietf-restconf:errors']['error'][0]['error-type'] == 'application' + - result_msg['ietf-restconf:errors']['error'][0]['error-tag'] == 'invalid-value' + register: assert_result + ignore_errors: yes + +- ansible.builtin.include_tasks: image_management.test.facts.report.yml + vars: + test_case_name: 'firmware_install' + test_case_input: + firmware: + command: 'install' + path: 'file://tmp/test.bin' diff --git a/tests/regression/roles/sonic_image_management/tasks/image_cancel.yml b/tests/regression/roles/sonic_image_management/tasks/image_cancel.yml new file mode 100644 index 000000000..9c609af57 --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/image_cancel.yml @@ -0,0 +1,22 @@ +--- +- name: Test case - image cancel + dellemc.enterprise_sonic.sonic_image_management: + image: + command: 'cancel' + register: result + ignore_errors: yes + +- ansible.builtin.assert: + that: + - result.failed == false + - result.status is defined + - result.status == 'SUCCESS' + register: assert_result + ignore_errors: yes + +- ansible.builtin.include_tasks: image_management.test.facts.report.yml + vars: + test_case_name: 'image_cancel' + test_case_input: + image: + command: 'cancel' diff --git a/tests/regression/roles/sonic_image_management/tasks/image_get_list.yml b/tests/regression/roles/sonic_image_management/tasks/image_get_list.yml new file mode 100644 index 000000000..f922164bf --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/image_get_list.yml @@ -0,0 +1,24 @@ +--- +- name: Test case - image get-list + dellemc.enterprise_sonic.sonic_image_management: + image: + command: 'get-list' + register: result + ignore_errors: yes + +- ansible.builtin.assert: + that: + - result.failed == false + - result.info is defined + - result.info.available is defined + - result.info.current is defined and result.info.current in result.info.available + - result.info.next is defined and result.info.next in result.info.available + register: assert_result + ignore_errors: yes + +- ansible.builtin.include_tasks: image_management.test.facts.report.yml + vars: + test_case_name: 'image_get_list' + test_case_input: + image: + command: 'get-list' diff --git a/tests/regression/roles/sonic_image_management/tasks/image_get_status.yml b/tests/regression/roles/sonic_image_management/tasks/image_get_status.yml new file mode 100644 index 000000000..d253e69f6 --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/image_get_status.yml @@ -0,0 +1,21 @@ +--- +- name: Test case - image get-status + dellemc.enterprise_sonic.sonic_image_management: + image: + command: 'get-status' + register: result + ignore_errors: yes + +- ansible.builtin.assert: + that: + - result.failed == false + - result.info is defined + register: assert_result + ignore_errors: yes + +- ansible.builtin.include_tasks: image_management.test.facts.report.yml + vars: + test_case_name: 'image_get_status' + test_case_input: + image: + command: 'get-status' diff --git a/tests/regression/roles/sonic_image_management/tasks/image_install.yml b/tests/regression/roles/sonic_image_management/tasks/image_install.yml new file mode 100644 index 000000000..0c51b61aa --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/image_install.yml @@ -0,0 +1,30 @@ +--- +- name: Test case - image install + dellemc.enterprise_sonic.sonic_image_management: + image: + command: 'install' + path: 'file://tmp/test.bin' + register: result + ignore_errors: yes + +- ansible.builtin.set_fact: + result_msg: "{{ result.msg | from_yaml }}" + when: result.msg is defined + +- ansible.builtin.assert: + that: + - result.failed == true + - result.msg is defined + - result_msg['code'] == 400 + - result_msg['ietf-restconf:errors']['error'][0]['error-type'] == 'application' + - result_msg['ietf-restconf:errors']['error'][0]['error-tag'] == 'invalid-value' + register: assert_result + ignore_errors: yes + +- ansible.builtin.include_tasks: image_management.test.facts.report.yml + vars: + test_case_name: 'image_install' + test_case_input: + image: + command: 'install' + path: 'file://tmp/test.bin' diff --git a/tests/regression/roles/sonic_image_management/tasks/image_management.test.facts.report.yml b/tests/regression/roles/sonic_image_management/tasks/image_management.test.facts.report.yml new file mode 100644 index 000000000..71fb56236 --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/image_management.test.facts.report.yml @@ -0,0 +1,9 @@ +- ansible.builtin.set_fact: + ansible_facts: + test_reports: "{{ ansible_facts['test_reports'] | default({}) | combine({module_name: {test_case_name: { + 'status': 'Passed' if (assert_result.failed == false) else 'Failed', + 'module_stderr': result.module_stderr | default(result.msg | default('No Error')), + 'configs': test_case_input | default('Not defined'), + 'result_info': result.info | default('N/A'), + 'result_status': result.status | default('N/A'), + }}}, recursive=True) }}" diff --git a/tests/regression/roles/sonic_image_management/tasks/image_remove.yml b/tests/regression/roles/sonic_image_management/tasks/image_remove.yml new file mode 100644 index 000000000..58c97651e --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/image_remove.yml @@ -0,0 +1,30 @@ +--- +- name: Test case - image remove - current image + dellemc.enterprise_sonic.sonic_image_management: + image: + command: 'remove' + name: '{{ current_image_name }}' + register: result + ignore_errors: yes + +- ansible.builtin.set_fact: + result_msg: "{{ result.msg | from_yaml }}" + when: result.msg is defined + +- ansible.builtin.assert: + that: + - result.failed == true + - result.msg is defined + - result_msg['code'] == 400 + - result_msg['ietf-restconf:errors']['error'][0]['error-type'] == 'application' + - result_msg['ietf-restconf:errors']['error'][0]['error-tag'] == 'invalid-value' + register: assert_result + ignore_errors: yes + +- ansible.builtin.include_tasks: image_management.test.facts.report.yml + vars: + test_case_name: 'image_remove' + test_case_input: + image: + command: 'remove' + name: '{{ current_image_name }}' diff --git a/tests/regression/roles/sonic_image_management/tasks/image_set_default.yml b/tests/regression/roles/sonic_image_management/tasks/image_set_default.yml new file mode 100644 index 000000000..c6e3a2ace --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/image_set_default.yml @@ -0,0 +1,24 @@ +--- +- name: Test case - image set-default - current image + dellemc.enterprise_sonic.sonic_image_management: + image: + command: 'set-default' + name: '{{ current_image_name }}' + register: result + ignore_errors: yes + +- ansible.builtin.assert: + that: + - result.failed == false + - result.status is defined + - result.status == 'SUCCESS' + register: assert_result + ignore_errors: yes + +- ansible.builtin.include_tasks: image_management.test.facts.report.yml + vars: + test_case_name: 'image_set_default' + test_case_input: + image: + command: 'set-default' + name: '{{ current_image_name }}' diff --git a/tests/regression/roles/sonic_image_management/tasks/main.yml b/tests/regression/roles/sonic_image_management/tasks/main.yml new file mode 100644 index 000000000..beb7fa3ba --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/main.yml @@ -0,0 +1,28 @@ +--- +- ansible.builtin.debug: + msg: "sonic_image_management Test started ..." + +- name: Preparations for image_management + ansible.builtin.include_tasks: preparation_tests.yml + +- ansible.builtin.include_tasks: image_install.yml +- ansible.builtin.include_tasks: image_cancel.yml +- ansible.builtin.include_tasks: image_remove.yml +- ansible.builtin.include_tasks: image_set_default.yml +- ansible.builtin.include_tasks: image_get_list.yml +- ansible.builtin.include_tasks: image_get_status.yml + +- ansible.builtin.include_tasks: patch_install.yml +- ansible.builtin.include_tasks: patch_rollback.yml +- ansible.builtin.include_tasks: patch_get_history.yml +- ansible.builtin.include_tasks: patch_get_list.yml +- ansible.builtin.include_tasks: patch_get_status.yml + +- ansible.builtin.include_tasks: firmware_install.yml +- ansible.builtin.include_tasks: firmware_cancel.yml +- ansible.builtin.include_tasks: firmware_get_list.yml +- ansible.builtin.include_tasks: firmware_get_status.yml + +- name: Display all variables/facts known for a host + ansible.builtin.debug: + var: hostvars[inventory_hostname].ansible_facts.test_reports diff --git a/tests/regression/roles/sonic_image_management/tasks/patch_get_history.yml b/tests/regression/roles/sonic_image_management/tasks/patch_get_history.yml new file mode 100644 index 000000000..9b87f424a --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/patch_get_history.yml @@ -0,0 +1,21 @@ +--- +- name: Test case - patch get-history + dellemc.enterprise_sonic.sonic_image_management: + patch: + command: 'get-history' + register: result + ignore_errors: yes + +- ansible.builtin.assert: + that: + - result.failed == false + - result.info is defined + register: assert_result + ignore_errors: yes + +- ansible.builtin.include_tasks: image_management.test.facts.report.yml + vars: + test_case_name: 'patch_get_history' + test_case_input: + patch: + command: 'get-history' diff --git a/tests/regression/roles/sonic_image_management/tasks/patch_get_list.yml b/tests/regression/roles/sonic_image_management/tasks/patch_get_list.yml new file mode 100644 index 000000000..0be1118a4 --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/patch_get_list.yml @@ -0,0 +1,21 @@ +--- +- name: Test case - patch get-list + dellemc.enterprise_sonic.sonic_image_management: + patch: + command: 'get-list' + register: result + ignore_errors: yes + +- ansible.builtin.assert: + that: + - result.failed == false + - result.info is defined + register: assert_result + ignore_errors: yes + +- ansible.builtin.include_tasks: image_management.test.facts.report.yml + vars: + test_case_name: 'patch_get_list' + test_case_input: + patch: + command: 'get-list' diff --git a/tests/regression/roles/sonic_image_management/tasks/patch_get_status.yml b/tests/regression/roles/sonic_image_management/tasks/patch_get_status.yml new file mode 100644 index 000000000..edf5311bf --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/patch_get_status.yml @@ -0,0 +1,21 @@ +--- +- name: Test case - patch get-status + dellemc.enterprise_sonic.sonic_image_management: + patch: + command: 'get-status' + register: result + ignore_errors: yes + +- ansible.builtin.assert: + that: + - result.failed == false + - result.info is defined + register: assert_result + ignore_errors: yes + +- ansible.builtin.include_tasks: image_management.test.facts.report.yml + vars: + test_case_name: 'patch_get_status' + test_case_input: + patch: + command: 'get-status' diff --git a/tests/regression/roles/sonic_image_management/tasks/patch_install.yml b/tests/regression/roles/sonic_image_management/tasks/patch_install.yml new file mode 100644 index 000000000..3cf7ae02e --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/patch_install.yml @@ -0,0 +1,30 @@ +--- +- name: Test case - patch install + dellemc.enterprise_sonic.sonic_image_management: + patch: + command: 'install' + path: 'file://tmp/test.patch' + register: result + ignore_errors: yes + +- ansible.builtin.set_fact: + result_msg: "{{ result.msg | from_yaml }}" + when: result.msg is defined + +- ansible.builtin.assert: + that: + - result.failed == true + - result.msg is defined + - result_msg['code'] == 400 + - result_msg['ietf-restconf:errors']['error'][0]['error-type'] == 'application' + - result_msg['ietf-restconf:errors']['error'][0]['error-tag'] == 'invalid-value' + register: assert_result + ignore_errors: yes + +- ansible.builtin.include_tasks: image_management.test.facts.report.yml + vars: + test_case_name: 'patch_install' + test_case_input: + patch: + command: 'install' + path: 'file://tmp/test.patch' diff --git a/tests/regression/roles/sonic_image_management/tasks/patch_rollback.yml b/tests/regression/roles/sonic_image_management/tasks/patch_rollback.yml new file mode 100644 index 000000000..6980f5f69 --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/patch_rollback.yml @@ -0,0 +1,30 @@ +--- +- name: Test case - patch rollback + dellemc.enterprise_sonic.sonic_image_management: + patch: + command: 'rollback' + name: 'test.patch' + register: result + ignore_errors: yes + +- ansible.builtin.set_fact: + result_msg: "{{ result.msg | from_yaml }}" + when: result.msg is defined + +- ansible.builtin.assert: + that: + - result.failed == true + - result.msg is defined + - result_msg['code'] == 400 + - result_msg['ietf-restconf:errors']['error'][0]['error-type'] == 'application' + - result_msg['ietf-restconf:errors']['error'][0]['error-tag'] == 'invalid-value' + register: assert_result + ignore_errors: yes + +- ansible.builtin.include_tasks: image_management.test.facts.report.yml + vars: + test_case_name: 'patch_rollback' + test_case_input: + patch: + command: 'rollback' + path: 'test.patch' diff --git a/tests/regression/roles/sonic_image_management/tasks/preparation_tests.yml b/tests/regression/roles/sonic_image_management/tasks/preparation_tests.yml new file mode 100644 index 000000000..1af488cae --- /dev/null +++ b/tests/regression/roles/sonic_image_management/tasks/preparation_tests.yml @@ -0,0 +1,9 @@ +--- +- name: Get current image name + dellemc.enterprise_sonic.sonic_image_management: + image: + command: get-list + register: image_list + +- ansible.builtin.set_fact: + current_image_name: '{{ image_list.info.current }}' diff --git a/tests/regression/roles/test_reports/templates/regression_html_report.j2 b/tests/regression/roles/test_reports/templates/regression_html_report.j2 index 6937eb8a9..f028a6d0f 100644 --- a/tests/regression/roles/test_reports/templates/regression_html_report.j2 +++ b/tests/regression/roles/test_reports/templates/regression_html_report.j2 @@ -242,9 +242,14 @@ color: red; Testcase name Status User Input +{% if module_name == 'image_management' %} + result['info'] + result['status'] +{% else %} Commands Before After +{% endif %} Module exception @@ -256,9 +261,14 @@ color: red; {% if 'Passed' in test_data.status %} {% if ansible_verbosity >= 3 %}
Input: {{ test_data.configs | default('Template Error') | to_nice_json(indent=3) }}
+{% if module_name == 'image_management' %} +
info: {{ test_data.result_info | default('Template Error') | to_nice_json(indent=3) }}
+
status: {{ test_data.result_status | default('Template Error') }}
+{% else %}
Commands: {{ test_data.commands | default('Template Error') | to_nice_json(indent=3) }}
Before: {{ test_data.before | default('Template Error') | to_nice_json(indent=3) }}
After: {{ test_data.after | default('Template Error') | to_nice_json(indent=3) }}
+{% endif %}
Error: {{ test_data.module_stderr | default('Template Error') | to_nice_json(indent=3) }}
{% else %} @@ -269,9 +279,14 @@ color: red; {% endif %} {% else %}
Input: {{ test_data.configs | default('Template Error') | to_nice_json(indent=3) }}
+{% if module_name == 'image_management' %} +
info: {{ test_data.result_info | default('Template Error') | to_nice_json(indent=3) }}
+
status: {{ test_data.result_status | default('Template Error') }}
+{% else %}
Commands: {{ test_data.commands | default('Template Error') | to_nice_json(indent=3) }}
Before: {{ test_data.before | default('Template Error') | to_nice_json(indent=3) }}
After: {{ test_data.after | default('Template Error') | to_nice_json(indent=3) }}
+{% endif %}
Error: {{ test_data.module_stderr | default('Template Error') | to_nice_json(indent=3) }}
{% endif %} diff --git a/tests/regression/test.yaml b/tests/regression/test.yaml index f2540127d..44011401a 100644 --- a/tests/regression/test.yaml +++ b/tests/regression/test.yaml @@ -11,6 +11,7 @@ #- sonic_api #- sonic_command #- sonic_config + - sonic_image_management - sonic_system - sonic_interfaces diff --git a/tests/unit/modules/network/sonic/fixtures/sonic_image_management.yaml b/tests/unit/modules/network/sonic/fixtures/sonic_image_management.yaml new file mode 100644 index 000000000..ee1e5563a --- /dev/null +++ b/tests/unit/modules/network/sonic/fixtures/sonic_image_management.yaml @@ -0,0 +1,433 @@ +--- +image_install: + module_args: + image: + command: 'install' + path: 'file://home/admin/sonic-broadcom.bin' + requests: + - path: 'operations/openconfig-image-management:image-install' + method: 'post' + data: + openconfig-image-management:input: + image-name: 'file://home/admin/sonic-broadcom.bin' + response: + code: 200 + value: + openconfig-image-management:output: + status: 0 + status-detail: 'SUCCESS' + +image_cancel: + module_args: + image: + command: 'cancel' + requests: + - path: 'operations/openconfig-image-management:image-install-cancel' + method: 'post' + data: + openconfig-image-management:input: {} + response: + code: 200 + value: + openconfig-image-management:output: + status: 0 + status-detail: 'SUCCESS' + +image_remove: + module_args: + image: + command: 'remove' + name: 'SONiC-OS-x.y.z-Enterprise' + requests: + - path: 'operations/openconfig-image-management:image-remove' + method: 'post' + data: + openconfig-image-management:input: + image-name: 'SONiC-OS-x.y.z-Enterprise' + response: + code: 200 + value: + openconfig-image-management:output: + status: 0 + status-detail: 'SUCCESS' + +image_set_default: + module_args: + image: + command: 'set-default' + name: 'SONiC-OS-x.y.z-Enterprise' + requests: + - path: 'operations/openconfig-image-management:image-default' + method: 'post' + data: + openconfig-image-management:input: + image-name: 'SONiC-OS-x.y.z-Enterprise' + response: + code: 200 + value: + openconfig-image-management:output: + status: 0 + status-detail: 'SUCCESS' + +image_get_list: + module_args: + image: + command: 'get-list' + requests: + - path: 'data/openconfig-image-management:image-management' + method: 'get' + response: + code: 200 + value: + openconfig-image-management:image-management: + global: + state: + current: 'SONiC-OS-a.b.c-Enterprise' + next-boot: 'SONiC-OS-x.y.z-Enterprise' + images: + image: + - image-name: 'SONiC-OS-a.b.c-Enterprise' + state: + image-name: 'SONiC-OS-a.b.c-Enterprise' + - image-name: 'SONiC-OS-x.y.z-Enterprise' + state: + image-name: 'SONiC-OS-z.y.z-Enterprise' + info_output: + current: 'SONiC-OS-a.b.c-Enterprise' + next: 'SONiC-OS-x.y.z-Enterprise' + available: + - 'SONiC-OS-a.b.c-Enterprise' + - 'SONiC-OS-x.y.z-Enterprise' + +image_get_status_01: + module_args: + image: + command: 'get-status' + requests: + - path: 'data/openconfig-image-management:image-management/install/state' + method: 'get' + response: + code: 200 + value: + openconfig-image-management:state: + file-download-speed: '76774' + file-progress: 100 + file-size: '1336485870' + file-transfer-bytes: '1336485870' + install-end-time: '1698914005' + install-start-time: '1698913960' + install-status: 'INSTALL_STATE_SUCCESS' + install-status-detail: 'Image install success' + operation-status: 'GLOBAL_STATE_SUCCESS' + transfer-end-time: '1698913929' + transfer-start-time: '1698913912' + transfer-status: 'TRANSFER_STATE_SUCCESS' + transfer-status-detail: 'DOWNLOADING IMAGE' + info_output: + file-download-speed: '76774' + file-progress: 100 + file-size: '1336485870' + file-transfer-bytes: '1336485870' + install-end-time: '1698914005' + install-start-time: '1698913960' + install-status: 'INSTALL_STATE_SUCCESS' + install-status-detail: 'Image install success' + operation-status: 'GLOBAL_STATE_SUCCESS' + transfer-end-time: '1698913929' + transfer-start-time: '1698913912' + transfer-status: 'TRANSFER_STATE_SUCCESS' + transfer-status-detail: 'DOWNLOADING IMAGE' + +image_get_status_02: + module_args: + image: + command: 'get-status' + requests: + - path: 'data/openconfig-image-management:image-management/install/state' + method: 'get' + response: + code: 200 + value: + openconfig-image-management:state: + file-download-speed: '110192' + file-progress: 45 + file-size: '10737418240' + file-transfer-bytes: '4851998720' + install-status: 'INSTALL_IDLE' + install-status-detail: 'INSTALL_IDLE' + operation-status: 'GLOBAL_STATE_DOWNLOAD' + transfer-start-time: '1699351876' + transfer-status: 'TRANSFER_DOWNLOAD' + transfer-status-detail: 'DOWNLOADING IMAGE' + info_output: + file-download-speed: '110192' + file-progress: 45 + file-size: '10737418240' + file-transfer-bytes: '4851998720' + operation-status: 'GLOBAL_STATE_DOWNLOAD' + transfer-start-time: '1699351876' + transfer-status: 'TRANSFER_DOWNLOAD' + transfer-status-detail: 'DOWNLOADING IMAGE' + +patch_install: + module_args: + patch: + command: 'install' + path: 'file://home/admin/sonic-broadcom.patch' + requests: + - path: 'operations/openconfig-image-management:do-patch-install' + method: 'post' + data: + openconfig-image-management:input: + patch-name: 'file://home/admin/sonic-broadcom.patch' + skip-image-check: '' + response: + code: 200 + value: + openconfig-image-management:output: + status: 0 + status-detail: 'SUCCESS' + +patch_rollback: + module_args: + patch: + command: 'rollback' + name: 'sonic-broadcom.patch' + requests: + - path: 'operations/openconfig-image-management:do-patch-rollback' + method: 'post' + data: + openconfig-image-management:input: + patch-name: 'sonic-broadcom.patch' + response: + code: 200 + value: + openconfig-image-management:output: + status: 0 + status-detail: 'SUCCESS' + +patch_get_list: + module_args: + patch: + command: 'get-list' + requests: + - path: 'data/openconfig-image-management:patch-management/patch-list' + method: 'get' + response: + code: 200 + value: + openconfig-image-management:patch-list: + patch: + - patch-time: '2023.11.03-06:04:16' + state: + dependency: '[]' + id: '21' + patch-time: '2023.11.03-06:04:16' + state: 'apply' + tag: '30.11.22-0001-patch-framework-verification-patch' + info_output: + list: + - dependency: '[]' + id: '21' + patch-time: '2023.11.03-06:04:16' + state: 'apply' + tag: '30.11.22-0001-patch-framework-verification-patch' + +patch_get_history: + module_args: + patch: + command: 'get-history' + requests: + - path: 'data/openconfig-image-management:patch-management/patch-history' + method: 'get' + response: + code: 200 + value: + openconfig-image-management:patch-history: + patch: + - patch-time: '2023.11.03-05:50:53' + state: + end: '2023.11.03-05:52:12' + id: '22' + patch-time: '2023.11.03-05:50:53' + start: '2023.11.03-05:50:53' + state: 'apply' + status: 'complete' + tag: '01.12.22-0002-patch-framework-verification-patch' + - patch-time: '2023.11.03-06:00:26' + state: + end: '2023.11.03-06:04:23' + id: '21' + patch-time: '2023.11.03-06:00:26' + start: '2023.11.03-06:00:26' + state: 'apply' + status: 'complete' + tag: '30.11.22-0001-patch-framework-verification-patch' + - patch-time: '2023.11.06-09:21:13' + state: + end: '2023.11.06-09:22:38' + id: '22' + patch-time: '2023.11.06-09:21:13' + start: '2023.11.06-09:21:13' + state: 'rollback' + status: 'complete' + tag: '01.12.22-0002-patch-framework-verification-patch' + info_output: + history: + - end: '2023.11.06-09:22:38' + id: '22' + patch-time: '2023.11.06-09:21:13' + start: '2023.11.06-09:21:13' + state: 'rollback' + status: 'complete' + tag: '01.12.22-0002-patch-framework-verification-patch' + - end: '2023.11.03-06:04:23' + id: '21' + patch-time: '2023.11.03-06:00:26' + start: '2023.11.03-06:00:26' + state: 'apply' + status: 'complete' + tag: '30.11.22-0001-patch-framework-verification-patch' + - end: '2023.11.03-05:52:12' + id: '22' + patch-time: '2023.11.03-05:50:53' + start: '2023.11.03-05:50:53' + state: 'apply' + status: 'complete' + tag: '01.12.22-0002-patch-framework-verification-patch' + +patch_get_status: + module_args: + patch: + command: 'get-status' + requests: + - path: 'data/openconfig-image-management:patch-management/patch-install' + method: 'get' + response: + code: 200 + value: + openconfig-image-management:patch-install: + download-state: + file-progress: 100 + file-size: '0' + file-transfer-bytes: '0' + transfer-end-time: '1698991214' + transfer-start-time: '1698991214' + transfer-status: 'TRANSFER_STATE_SUCCESS' + transfer-status-detail: 'DOWNLOADING IMAGE' + install-state: + install-end-time: '2023.11.03-06:04:23' + install-start-time: '2023.11.03-06:00:26' + install-status: 'INSTALL_STATE_SUCCESS' + recovery-end-time: '-' + recovery-start-time: '-' + recovery-status: 'RECOVER_IDLE' + rollback-end-time: '-' + rollback-start-time: '-' + rollback-status: 'ROLLBACK_IDLE' + trigger: 'install' + info_output: + file-progress: 100 + file-size: '0' + file-transfer-bytes: '0' + transfer-end-time: '1698991214' + transfer-start-time: '1698991214' + transfer-status: 'TRANSFER_STATE_SUCCESS' + transfer-status-detail: 'DOWNLOADING IMAGE' + install-end-time: '2023.11.03-06:04:23' + install-start-time: '2023.11.03-06:00:26' + install-status: 'INSTALL_STATE_SUCCESS' + +firmware_install: + module_args: + firmware: + command: 'install' + path: 'file://home/admin/firmware.bin' + requests: + - path: 'operations/openconfig-image-management:do-fwpkg-install' + method: 'post' + data: + openconfig-image-management:input: + fwpkg-name: 'file://home/admin/firmware.bin' + response: + code: 200 + value: + openconfig-image-management:output: + status: 0 + status-detail: 'SUCCESS' + +firmware_get_status: + module_args: + firmware: + command: 'get-status' + requests: + - path: 'data/openconfig-image-management:fwpkg-management/fwpkg-install' + method: 'get' + response: + code: 200 + value: + openconfig-image-management:fwpkg-install: + download-state: + file-download-speed: '77380' + file-progress: 100 + file-size: '79237120' + file-transfer-bytes: '79237120' + transfer-end-time: '1698730798' + transfer-start-time: '1698730797' + transfer-status: 'TRANSFER_STATE_SUCCESS' + transfer-status-detail: 'Download complete' + stage-state: + stage-end-time: '1698730799' + stage-start-time: '1698730798' + stage-status: 'STAGE_STATE_SUCCESS' + stage-status-detail: 'Firmware package staging success' + info_output: + file-download-speed: '77380' + file-progress: 100 + file-size: '79237120' + file-transfer-bytes: '79237120' + transfer-end-time: '1698730798' + transfer-start-time: '1698730797' + transfer-status: 'TRANSFER_STATE_SUCCESS' + transfer-status-detail: 'Download complete' + stage-end-time: '1698730799' + stage-start-time: '1698730798' + stage-status: 'STAGE_STATE_SUCCESS' + stage-status-detail: 'Firmware package staging success' + +firmware_get_list: + module_args: + firmware: + command: 'get-list' + requests: + - path: 'data/openconfig-image-management:fwpkg-management' + method: 'get' + response: + code: 200 + value: + openconfig-image-management:fwpkg-management: + fwpkg-pending: + fwpkg: + - name: 'onie-update-full-x86_64-dellemc_z9400_c3758-r0.3.51.5.1-17.tar' + state: + date: '2023-10-31 05:39:59' + name: 'onie-update-full-x86_64-dellemc_z9400_c3758-r0.3.51.5.1-17.tar' + version: '3.51.5.1-17' + fwpkg-result: + fwpkg: + - name: 'onie-update-full-x86_64-dellemc_z9400_c3758-r0.3.51.5.1-17.tar' + state: + date: '2023-10-27 06:31:50' + name: 'onie-update-full-x86_64-dellemc_z9400_c3758-r0.3.51.5.1-17.tar' + result: 'Success' + version: '3.51.5.1-17' + info_output: + pending: + - date: '2023-10-31 05:39:59' + name: 'onie-update-full-x86_64-dellemc_z9400_c3758-r0.3.51.5.1-17.tar' + version: '3.51.5.1-17' + result: + - date: '2023-10-27 06:31:50' + name: 'onie-update-full-x86_64-dellemc_z9400_c3758-r0.3.51.5.1-17.tar' + result: 'Success' + version: '3.51.5.1-17' diff --git a/tests/unit/modules/network/sonic/test_sonic_image_management.py b/tests/unit/modules/network/sonic/test_sonic_image_management.py new file mode 100644 index 000000000..49121f663 --- /dev/null +++ b/tests/unit/modules/network/sonic/test_sonic_image_management.py @@ -0,0 +1,275 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ansible_collections.dellemc.enterprise_sonic.tests.unit.compat.mock import ( + patch, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.modules import ( + sonic_image_management, +) +from ansible_collections.dellemc.enterprise_sonic.tests.unit.modules.utils import ( + set_module_args, +) +from .sonic_module import TestSonicModule + + +class TestSonicImageManagementModule(TestSonicModule): + module = sonic_image_management + + @classmethod + def setUpClass(cls): + cls.mock_module_edit_config = patch( + "ansible_collections.dellemc.enterprise_sonic.plugins.modules.sonic_image_management.edit_config" + ) + cls.fixture_data = cls.load_fixtures('sonic_image_management.yaml') + + def setUp(self): + super(TestSonicImageManagementModule, self).setUp() + self.module_edit_config = self.mock_module_edit_config.start() + + def tearDown(self): + super(TestSonicImageManagementModule, self).tearDown() + self.mock_module_edit_config.stop() + + def test_sonic_image_management_image_install(self): + set_module_args(self.fixture_data['image_install']['module_args']) + self.initialize_config_requests(self.fixture_data['image_install']['requests']) + self.module_edit_config.side_effect = self.config_side_effect + status = 'Check image -> command = get-status for image install progress' + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('info', result) + self.assertIn('status', result) + self.assertEqual(status, result['status']) + + def test_sonic_image_management_image_install_ignore_name(self): + module_args = self.fixture_data['image_install']['module_args'].copy() + module_args['image']['name'] = 'test.bin' + set_module_args(module_args) + + self.initialize_config_requests(self.fixture_data['image_install']['requests']) + self.module_edit_config.side_effect = self.config_side_effect + status = 'Check image -> command = get-status for image install progress' + warnings = ['image -> name is ignored when image -> command = install'] + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('info', result) + self.assertIn('status', result) + self.assertEqual(status, result['status']) + self.assertEqual(warnings, result['warnings']) + + def test_sonic_image_management_image_cancel(self): + set_module_args(self.fixture_data['image_cancel']['module_args']) + self.initialize_config_requests(self.fixture_data['image_cancel']['requests']) + self.module_edit_config.side_effect = self.config_side_effect + status = 'SUCCESS' + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('info', result) + self.assertIn('status', result) + self.assertEqual(status, result['status']) + + def test_sonic_image_management_image_remove(self): + set_module_args(self.fixture_data['image_remove']['module_args']) + self.initialize_config_requests(self.fixture_data['image_remove']['requests']) + self.module_edit_config.side_effect = self.config_side_effect + status = 'SUCCESS' + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('info', result) + self.assertIn('status', result) + self.assertEqual(status, result['status']) + + def test_sonic_image_management_image_remove_ignore_path(self): + module_args = self.fixture_data['image_remove']['module_args'].copy() + module_args['image']['path'] = 'file://home/admin/test.bin' + set_module_args(module_args) + + self.initialize_config_requests(self.fixture_data['image_remove']['requests']) + self.module_edit_config.side_effect = self.config_side_effect + status = 'SUCCESS' + warnings = ['image -> path is ignored when image -> command = remove'] + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('info', result) + self.assertIn('status', result) + self.assertEqual(status, result['status']) + self.assertEqual(warnings, result['warnings']) + + def test_sonic_image_management_image_set_default(self): + set_module_args(self.fixture_data['image_set_default']['module_args']) + self.initialize_config_requests(self.fixture_data['image_set_default']['requests']) + self.module_edit_config.side_effect = self.config_side_effect + status = 'SUCCESS' + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('info', result) + self.assertIn('status', result) + self.assertEqual(status, result['status']) + + def test_sonic_image_management_image_get_list(self): + set_module_args(self.fixture_data['image_get_list']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['image_get_list']['requests']) + self.module_edit_config.side_effect = self.facts_side_effect + info = self.fixture_data['image_get_list']['info_output'] + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('status', result) + self.assertIn('info', result) + self.assertEqual(info, result['info']) + + def test_sonic_image_management_image_get_status_01(self): + set_module_args(self.fixture_data['image_get_status_01']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['image_get_status_01']['requests']) + self.module_edit_config.side_effect = self.facts_side_effect + info = self.fixture_data['image_get_status_01']['info_output'] + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('status', result) + self.assertIn('info', result) + self.assertEqual(info, result['info']) + + def test_sonic_image_management_image_get_status_02(self): + set_module_args(self.fixture_data['image_get_status_02']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['image_get_status_02']['requests']) + self.module_edit_config.side_effect = self.facts_side_effect + info = self.fixture_data['image_get_status_02']['info_output'] + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('status', result) + self.assertIn('info', result) + self.assertEqual(info, result['info']) + + def test_sonic_image_management_patch_install(self): + set_module_args(self.fixture_data['patch_install']['module_args']) + self.initialize_config_requests(self.fixture_data['patch_install']['requests']) + self.module_edit_config.side_effect = self.config_side_effect + status = 'Check patch -> command = get-status for patch install progress' + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('info', result) + self.assertIn('status', result) + self.assertEqual(status, result['status']) + + def test_sonic_image_management_patch_rollback(self): + set_module_args(self.fixture_data['patch_rollback']['module_args']) + self.initialize_config_requests(self.fixture_data['patch_rollback']['requests']) + self.module_edit_config.side_effect = self.config_side_effect + status = 'Check patch -> command = get-status for patch rollback progress' + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('info', result) + self.assertIn('status', result) + self.assertEqual(status, result['status']) + + def test_sonic_image_management_patch_get_list(self): + set_module_args(self.fixture_data['patch_get_list']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['patch_get_list']['requests']) + self.module_edit_config.side_effect = self.facts_side_effect + info = self.fixture_data['patch_get_list']['info_output'] + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('status', result) + self.assertIn('info', result) + self.assertEqual(info, result['info']) + + def test_sonic_image_management_patch_get_history(self): + set_module_args(self.fixture_data['patch_get_history']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['patch_get_history']['requests']) + self.module_edit_config.side_effect = self.facts_side_effect + info = self.fixture_data['patch_get_history']['info_output'] + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('status', result) + self.assertIn('info', result) + self.assertEqual(info, result['info']) + + def test_sonic_image_management_patch_get_status(self): + set_module_args(self.fixture_data['patch_get_status']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['patch_get_status']['requests']) + self.module_edit_config.side_effect = self.facts_side_effect + info = self.fixture_data['patch_get_status']['info_output'] + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('status', result) + self.assertIn('info', result) + self.assertEqual(info, result['info']) + + def test_sonic_image_management_firmware_install(self): + set_module_args(self.fixture_data['firmware_install']['module_args']) + self.initialize_config_requests(self.fixture_data['firmware_install']['requests']) + self.module_edit_config.side_effect = self.config_side_effect + status = 'Check firmware -> command = get-status for firmware package staging progress' + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('info', result) + self.assertIn('status', result) + self.assertEqual(status, result['status']) + + def test_sonic_image_management_firmware_get_list(self): + set_module_args(self.fixture_data['firmware_get_list']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['firmware_get_list']['requests']) + self.module_edit_config.side_effect = self.facts_side_effect + info = self.fixture_data['firmware_get_list']['info_output'] + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('status', result) + self.assertIn('info', result) + self.assertEqual(info, result['info']) + + def test_sonic_image_management_firmware_get_status(self): + set_module_args(self.fixture_data['firmware_get_status']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['firmware_get_status']['requests']) + self.module_edit_config.side_effect = self.facts_side_effect + info = self.fixture_data['firmware_get_status']['info_output'] + + result = self.execute_module() + self.validate_config_requests() + self.assertNotIn('status', result) + self.assertIn('info', result) + self.assertEqual(info, result['info']) + + def test_sonic_image_management_invalid_args_01(self): + set_module_args({ + 'image': {'command': 'install', 'path': 'file://home/admin/sonic.bin'}, + 'firmware': {'command': 'install', 'path': 'file://home/admin/firmware.bin'}, + }) + msg = 'Only one image management operation can be performed at a time' + + result = self.execute_module(failed=True) + self.assertEqual(msg, result['msg']) + + def test_sonic_image_management_invalid_args_02(self): + set_module_args({ + 'image': {'command': 'remove', 'path': 'file://home/admin/sonic.bin'} + }) + msg = 'image -> name is required when image -> command = remove' + + result = self.execute_module(failed=True) + self.assertEqual(msg, result['msg']) + + def test_sonic_image_management_invalid_args_03(self): + set_module_args({ + 'image': {'command': 'install', 'name': 'sonic.bin'} + }) + msg = 'image -> path is required when image -> command = install' + + result = self.execute_module(failed=True) + self.assertEqual(msg, result['msg'])