From 33554d29476eab2447234528c8aed186e2b6423d Mon Sep 17 00:00:00 2001 From: jw-msft <84477130+jw-msft@users.noreply.github.com> Date: Tue, 1 Nov 2022 11:14:25 -0700 Subject: [PATCH] Merge Release 1.0.0 Changes to main (#235) * squash and merge since 0.8.0 Co-authored-by: Nox-MSFT <55153324+Nox-MSFT@users.noreply.github.com> Co-authored-by: Shiyi Peng Co-authored-by: shiyi-peng <71347127+shiyi-peng@users.noreply.github.com> --- .gitattributes | 5 + .gitignore | 5 + CMakeLists.txt | 162 +- SUPPORT.md | 10 +- azurepipelines/build/README.md | 39 + .../build/docker/adu-debian-amd64-build.yml | 6 +- .../build/docker/adu-debian-arm32-build.yml | 16 +- .../build/docker/adu-debian-arm64-build.yml | 6 +- .../build/docker/adu-ubuntu-amd64-build.yml | 71 - .../build/native/adu-ubuntu-amd64-build.yml | 25 +- .../adu-ubuntu-arm64-build.yml | 28 +- .../templates/adu-docker-build-steps.yml | 1 - .../templates/adu-native-build-steps.yml | 33 +- azurepipelines/e2e_test/README.md | 228 +++ azurepipelines/e2e_test/e2etest.yaml | 509 +++++ .../debian-10-amd64/Bundle-update.py | 160 ++ .../debian-10-amd64/Multi-Component-Update.py | 160 ++ .../add_device_to_adu_group.py | 60 + .../debian-10-amd64/apt_deployment.py | 158 ++ .../debian-10-amd64/delete_device.py | 45 + .../scenarios/debian-10-amd64/diagnostics.py | 95 + .../debian-10-amd64/scenario_definitions.py | 39 + .../debian-10-amd64/vm_setup/devicesetup.py | 78 + .../debian-10-amd64/vm_setup/setup.sh | 112 ++ .../scenarios/testingtoolkit/README.md | 122 ++ .../scenarios/testingtoolkit/__init__.py | 21 + .../testingtoolkit/_adu_test_toolkit.py | 571 ++++++ .../scenarios/testingtoolkit/_version.py | 7 + .../scenarios/testingtoolkit/requirements.txt | 6 + .../ubuntu-18.04-amd64/Bundle-update.py | 163 ++ .../Multi-Component-Update.py | 164 ++ .../add_device_to_adu_group.py | 60 + .../ubuntu-18.04-amd64/apt_deployment.py | 161 ++ .../ubuntu-18.04-amd64/delete_device.py | 43 + .../ubuntu-18.04-amd64/diagnostics.py | 99 + .../scenario_definitions.py | 39 + .../vm_setup/devicesetup.py | 82 + .../ubuntu-18.04-amd64/vm_setup/setup.sh | 93 + .../ubuntu-18.04-arm64/Bundle-update.py | 163 ++ .../Multi-Component-Update.py | 164 ++ .../add_device_to_adu_group.py | 60 + .../ubuntu-18.04-arm64/apt_deployment.py | 161 ++ .../ubuntu-18.04-arm64/delete_device.py | 43 + .../ubuntu-18.04-arm64/diagnostics.py | 96 + .../scenario_definitions.py | 39 + .../vm_setup/devicesetup.py | 82 + .../ubuntu-18.04-arm64/vm_setup/setup.sh | 93 + .../ubuntu-20.04-amd64/Bundle-update.py | 164 ++ .../Multi-Component-Update.py | 165 ++ .../add_device_to_adu_group.py | 60 + .../ubuntu-20.04-amd64/apt_deployment.py | 156 ++ .../ubuntu-20.04-amd64/delete_device.py | 43 + .../ubuntu-20.04-amd64/diagnostics.py | 95 + .../scenario_definitions.py | 41 + .../vm_setup/devicesetup.py | 84 + .../ubuntu-20.04-amd64/vm_setup/setup.sh | 91 + .../terraform/host/DeviceUpdateHost.tf | 194 ++ .../e2e_test/terraform/host/variables.tf | 51 + .../terraform/resource_group/ResourceGroup.tf | 36 + .../terraform/resource_group/output.tf | 8 + .../terraform/resource_group/variables.tf | 26 + daemon/CMakeLists.txt | 2 +- ...ent.service => deviceupdate-agent.service} | 2 +- daemon/install.sh | 9 +- docs/agent-reference/configurations-file.md | 9 - .../device-diagnostic-service.md | 0 ...vice-update-agent-extended-result-codes.md | 665 +------ ...evice-update-agent-extensibility-points.md | 133 ++ .../extension-contract-versions.md | 30 + docs/agent-reference/goal-state-support.md | 24 +- .../how-to-build-agent-code.md | 202 +- ...wupdate-for-swupdate-handler-unit-tests.md | 187 ++ .../how-to-implement-custom-update-handler.md | 26 +- .../how-to-modify-the-agent-code.md | 16 +- docs/agent-reference/how-to-run-agent.md | 4 +- .../how-to-simulate-update-result.md | 69 +- .../multi-component-updating.md | 4 +- .../registering-device-update-extensions.md | 130 ++ .../update-manifest-v4-schema.md | 198 +- docs/agent-reference/upgrade-guide.md | 180 -- docs/agent-reference/whats-new.md | 35 - .../cancel-or-retry-deployment.md | 51 - .../create-or-update-deployment.md | 94 - .../delete-deployment.md | 34 - .../get-available-device-tags.md | 44 - .../get-deployment-details.md | 45 - .../get-deployment-device-states.md | 77 - .../get-deployment-devices.md | 50 - .../get-deployment-status.md | 51 - .../get-deployments.md | 65 - .../get-device-class-details.md | 51 - .../get-device-classes-for-a-device.md | 35 - .../get-device-classes.md | 55 - .../get-device-ids-in-device-class.md | 38 - .../get-device-tag.md | 36 - docs/management-api-reference/get-device.md | 52 - docs/management-api-reference/get-devices.md | 87 - .../get-group-best-updates.md | 61 - .../get-group-update-compliance.md | 39 - docs/management-api-reference/get-group.md | 40 - docs/management-api-reference/get-groups.md | 50 - ...get-installable-update-for-device-class.md | 51 - .../get-update-compliance.md | 38 - .../management-api-overview.md | 46 - ...e-package-update-1.0.1-importManifest.json | 4 +- ...e-package-update-1.0.2-importManifest.json | 4 +- ...package-update-2-2.0.1-importManifest.json | 4 +- licenses/cgmanifest.json | 32 +- packages/CMakeLists.txt | 2 +- packages/debian/postinst | 70 +- packages/debian/postrm | 18 +- packages/debian/prerm | 8 +- scripts/adu-diag.sh | 14 +- scripts/build.sh | 77 +- scripts/clang-format.sh | 28 +- scripts/cmake-format.sh | 38 +- scripts/config-integration.sh | 7 +- scripts/debug_agent.sh | 6 +- scripts/docker/README.md | 87 + scripts/docker/build_docker_image.sh | 411 ++++ scripts/docker/dbg_docker.sh | 10 + scripts/docker/setup_docker_container.sh | 27 + scripts/docker/templates/Dockerfile.template | 19 + ...content_downloader.extension.template.json | 7 + .../templates/content_handler.template.json | 8 + .../docker/templates/du-config.template.json | 22 + .../du-diagnostics-config.template.json | 9 + scripts/docker/templates/setup_container.sh | 105 + .../error_code_defs_generator.py | 701 +++++++ .../result_codes.json | 1025 ++++++++++ scripts/generate_error_code_defs.sh | 90 + scripts/githooks/pre-commit.sh | 41 + scripts/install-deps.sh | 537 ++++- scripts/sh-format.sh | 122 +- src/CMakeLists.txt | 6 +- src/adu-shell/scripts/adu-swupdate.sh | 4 +- src/adu-shell/src/main.cpp | 3 +- src/adu-shell/src/swupdate_tasks.cpp | 6 +- src/adu_types/CMakeLists.txt | 4 +- src/adu_types/inc/aduc/adu_types.h | 38 +- src/adu_types/inc/aduc/types/adu_core.h | 84 +- src/adu_types/inc/aduc/types/update_content.h | 66 +- src/adu_types/inc/aduc/types/workflow.h | 75 +- src/adu_types/src/adu_types.c | 71 +- src/adu_workflow/CMakeLists.txt | 20 +- src/adu_workflow/inc/aduc/agent_workflow.h | 17 +- src/adu_workflow/src/agent_workflow.c | 493 +++-- src/agent/CMakeLists.txt | 41 +- .../adu_core_export_helpers/CMakeLists.txt | 21 + .../inc/aduc/adu_core_export_helpers.h | 4 +- .../src/adu_core_export_helpers.c | 20 +- src/agent/adu_core_interface/CMakeLists.txt | 54 +- .../inc/aduc/adu_core_interface.h | 25 +- .../inc/aduc/adu_core_json.h | 45 - .../src/adu_core_interface.c | 142 +- .../adu_core_interface/src/adu_core_json.c | 304 --- .../src/device_properties.c | 130 +- .../src/device_properties.h | 25 +- .../src/startup_msg_helper.c | 39 +- .../src/startup_msg_helper.h | 7 +- .../adu_core_interface/tests/CMakeLists.txt | 65 - .../tests/adu_core_export_helpers_ut.cpp | 183 -- .../tests/adu_core_interface_ut.cpp | 426 ---- .../tests/adu_core_json_ut.cpp | 634 ------ src/agent/adu_core_interface/tests/main.cpp | 41 - .../adu_core_interface/tests/result_ut.cpp | 80 - .../tests/startup_workflowdata_ut.cpp | 172 -- .../updateManifest.json | 2 +- .../updateActionForActionBundle.json | 2 +- .../tests/workflow_reboot_ut.cpp | 592 ------ .../tests/workflow_replacement_ut.cpp | 669 ------- .../tests/workflow_test_utils.cpp | 20 - .../tests/workflow_test_utils.h | 16 - .../adu_core_interface/tests/workflow_ut.cpp | 375 ---- src/agent/command_helper/CMakeLists.txt | 24 + .../command_helper/inc/aduc/command_helper.h | 64 + src/agent/command_helper/src/command_helper.c | 385 ++++ .../device_info_interface/CMakeLists.txt | 8 +- .../inc/aduc/device_info_interface.h | 4 +- .../src/device_info_interface.c | 92 +- src/agent/pnp_helper/src/pnp_protocol.c | 4 +- src/agent/src/health_management.c | 103 +- src/agent/src/main.c | 614 ++++-- src/agent_orchestration/CMakeLists.txt | 16 + .../inc/aduc/agent_orchestration.h | 6 +- .../src/agent_orchestration.c | 28 +- .../inc/aduc/client_handle_helper.h | 19 + .../src/client_handle_helper.c | 35 + src/content_handlers/CMakeLists.txt | 43 - .../inc/aduc/content_handler_factory.hpp | 39 - .../src/content_handler_factory.cpp | 266 --- src/deps/swupdate/.config | 121 ++ .../diagnostics_async_helper/CMakeLists.txt | 5 - .../diagnostics_interface/CMakeLists.txt | 10 +- .../inc/diagnostics_interface.h | 10 +- .../src/diagnostics_interface.c | 137 +- .../diagnostics_workflow/CMakeLists.txt | 9 +- .../inc/diagnostics_workflow.h | 61 +- .../src/diagnostics_workflow.c | 299 +-- .../utils/CMakeLists.txt | 1 + .../utils/config_utils/CMakeLists.txt | 19 + .../inc/diagnostics_config_utils.h | 73 + .../src/diagnostics_config_utils.c | 308 +++ .../config_utils}/tests/CMakeLists.txt | 14 +- .../tests/diagnostics_config_utils_ut.cpp} | 46 +- .../utils/config_utils/tests/main.cpp | 9 + .../file_info_utils/src/file_info_utils.c | 2 +- src/extensions/CMakeLists.txt | 10 +- .../CMakeLists.txt | 22 - .../demo/README.md | 118 -- .../CreateSampleMSOEUpdate-6.x.ps1 | 191 -- .../CreateSampleMSOEUpdate-8.x.ps1 | 327 ---- .../CMakeLists.txt | 17 + .../contoso_component_enumerator}/README.md | 10 +- ...toso-virtual-vacuum-components-diagram.svg | 0 .../contoso-virtual-vacuum-update-flow.svg | 0 .../contoso_component_enumerator.cpp} | 192 +- .../demo/README.md | 437 +++++ .../components-inventory-with-steamers.json} | 11 + .../contoso-devices/components-inventory.json | 0 .../vacuum-1/bootfs/diskimage.json | 0 .../contoso-camera-serial-00000/firmware.json | 0 .../contoso-camera-serial-00001/firmware.json | 0 .../vacuum-1/hostfw/firmware.json | 0 .../contoso-motor-serial-00000/firmware.json | 0 .../contoso-motor-serial-00001/firmware.json | 0 .../contoso-motor-serial-00002/firmware.json | 0 .../vacuum-1/rootfs/diskimage.json | 0 .../firmware.json | 4 + .../demo/sample-updates/AduUpdate.psm1 | 399 ++++ .../CreateSampleMSOEUpdate-1.x.ps1 | 32 +- .../CreateSampleMSOEUpdate-2.x.ps1 | 16 +- .../CreateSampleMSOEUpdate-3.x.ps1 | 16 +- .../CreateSampleMSOEUpdate-4.x.ps1 | 10 +- .../CreateSampleMSOEUpdate-5.x.ps1 | 10 +- .../CreateSampleMSOEUpdate-6.x.ps1} | 74 +- .../CreateSampleMSOEUpdate-7.x.ps1} | 218 ++- .../data-files/APT/apt-manifest-1.0.json | 0 .../data-files/APT/apt-manifest-4.0.json | 0 .../data-files/APT/apt-manifest-tree-1.0.json | 0 .../data-files/APT/bad-apt-manifest-1.0.json | 0 .../data-files/camera-firmware-1.1.json | 0 .../data-files/camera-firmware-2.1.json | 0 .../data-files/camera-firmware-2.2.json | 0 .../data-files/camera-firmware-3.0.json | 0 .../data-files/contoso-vacuum-apt-1.0.json | 0 .../data-files/host-firmware-1.1.json | 0 .../data-files/host-firmware-1.2.json | 0 .../data-files/host-firmware-1.3.json | 0 .../data-files/motor-firmware-1.1.json | 0 .../data-files/motor-firmware-1.2.json | 0 .../data-files/motor-firmware-1.3.json | 0 .../data-files/motor-firmware-1.4.json | 0 .../data-files/motor-firmware-1.5.json | 0 .../data-files/motor-firmware-3.1.json | 0 .../data-files/steamer-firmware-1.0.json | 0 .../data-files/steamer-firmware-2.0.json | 0 .../scripts/contoso-camera-installscript.sh | 110 +- .../contoso-diskimage-installscript.sh | 110 +- .../scripts/contoso-firmware-installscript.sh | 111 +- .../scripts/contoso-motor-installscript.sh | 111 +- .../scripts/contoso-steamer-installscript.sh | 111 +- .../scripts/contoso-update-simulatorscript.sh | 113 +- .../demo/tools/add-packages-microsoft-com.sh | 2 +- .../demo/tools/reset-demo-components.sh | 0 .../demo/tutorial1.md | 18 +- .../demo/tutorial1.ps1 | 12 +- .../content-downloaders/CMakeLists.txt | 6 - .../curl-downloader/CMakeLists.txt | 25 - .../content_downloaders/CMakeLists.txt | 6 + .../README.md | 0 .../curl_downloader/CMakeLists.txt | 23 + .../curl_content_downloader.EXPORTS.cpp | 53 + .../curl_content_downloader.cpp} | 28 +- .../curl_downloader/curl_content_downloader.h | 11 + .../CMakeLists.txt | 14 +- ...ptimization_content_downloader.EXPORTS.cpp | 104 + ...liveryoptimization_content_downloader.cpp} | 114 +- .../deliveryoptimization_content_downloader.h | 15 + .../download_handlers/CMakeLists.txt | 5 + src/extensions/download_handlers/README.md | 37 + .../download_handler_factory/CMakeLists.txt | 29 + .../inc/aduc/download_handler_factory.h | 14 + .../inc/aduc/download_handler_factory.hpp | 52 + .../src/download_handler_factory.cpp | 118 ++ .../download_handler_plugin/CMakeLists.txt | 24 + .../inc/aduc/download_handler_plugin.h | 26 + .../inc/aduc/download_handler_plugin.hpp | 42 + .../src/download_handler_plugin.cpp | 158 ++ .../plugin_examples/CMakeLists.txt | 3 + .../CMakeLists.txt | 4 + .../handler/CMakeLists.txt | 5 + .../handler/lib/CMakeLists.txt | 23 + .../aduc/microsoft_delta_download_handler.h | 51 + .../src/microsoft_delta_download_handler.c | 139 ++ .../handler/plugin/CMakeLists.txt | 19 + ...ft_delta_download_handler_plugin.EXPORTS.c | 99 + .../handler/utils/CMakeLists.txt | 29 + .../microsoft_delta_download_handler_utils.h | 107 + .../microsoft_delta_download_handler_utils.c | 337 ++++ ...microsoft_delta_download_handler_utils.cpp | 150 ++ .../handler/utils/tests/CMakeLists.txt | 22 + .../handler/utils/tests/main.cpp | 10 + ...rosoft_delta_download_handler_utils_ut.cpp | 151 ++ .../source_update_cache/CMakeLists.txt | 40 + .../inc/aduc/source_update_cache.h | 42 + .../inc/aduc/source_update_cache_utils.h | 31 + .../src/source_update_cache.c | 126 ++ .../src/source_update_cache_utils.c | 257 +++ .../src/source_update_cache_utils.cpp | 198 ++ .../source_update_cache/tests/CMakeLists.txt | 34 + .../source_update_cache/tests/main.cpp | 10 + .../tests/source_update_cache_utils_ut.cpp | 394 ++++ .../extension_manager/CMakeLists.txt | 17 +- .../inc/aduc/extension_manager.h | 28 +- .../inc/aduc/extension_manager.hpp | 34 +- .../aduc/extension_manager_download_options.h | 9 + .../src/extension_manager.cpp | 461 ++++- .../aduc/component_enumerator_extension.hpp | 5 +- .../inc/aduc/content_downloader_extension.hpp | 11 +- src/extensions/inc/aduc/content_handler.hpp | 29 +- .../exports/extension_common_export_symbols.h | 38 + ...sion_component_enumerator_export_symbols.h | 68 + ...ension_content_downloader_export_symbols.h | 48 + ...extension_content_handler_export_symbols.h | 26 + ...xtension_download_handler_export_symbols.h | 62 + .../aduc/exports/extension_export_symbols.h | 17 + src/extensions/shared_lib/CMakeLists.txt | 16 + .../inc/aduc/plugin_call_helper.hpp | 122 ++ .../shared_lib/inc/aduc/shared_lib.hpp | 31 + src/extensions/shared_lib/src/shared_lib.cpp | 58 + src/extensions/step_handlers/CMakeLists.txt | 12 + .../step_handlers}/apt_handler/CMakeLists.txt | 24 +- .../step_handlers}/apt_handler/README.md | 18 +- .../apt_handler/inc/aduc/apt_handler.hpp | 13 +- .../apt_handler/inc/aduc/apt_parser.hpp | 1 - .../apt_handler/src/apt_handler.cpp | 166 +- .../apt_handler/src/apt_parser.cpp | 0 .../apt_handler/tests/CMakeLists.txt | 4 +- .../apt_handler/tests/apt_parser_ut.cpp | 0 .../step_handlers}/apt_handler/tests/main.cpp | 0 .../tests/sample_apt_manifest.json | 0 .../script_handler/CMakeLists.txt | 25 +- .../step_handlers}/script_handler/README.md | 59 +- .../examples/example-installscript.sh | 68 +- .../images/script-handler-overview.svg | 0 .../inc/aduc/script_handler.hpp | 4 +- .../script_handler/src/script_handler.cpp | 118 +- .../script_handler/tests/CMakeLists.txt | 0 .../script_handler/tests/main.cpp | 0 .../tests/script_handler_ut.cpp | 0 .../simulator_handler/CMakeLists.txt | 16 +- .../simulator_handler/README.md | 0 .../du-simulator-data-template.jsonc | 16 + .../inc/aduc/simulator_handler.hpp | 2 + .../src/simulator_handler.cpp | 93 +- .../simulator_handler/tests/CMakeLists.txt | 9 +- .../simulator_handler/tests/main.cpp | 0 .../tests/simulator_handler_unit_tests.cpp | 66 + .../swupdate_handler/CMakeLists.txt | 11 +- .../step_handlers}/swupdate_handler/README.md | 0 .../inc/aduc/swupdate_handler.hpp | 2 + .../swupdate_handler/src/swupdate_handler.cpp | 147 +- .../swupdate_handler_v2/CMakeLists.txt | 54 + .../swupdate_handler_v2/README.md | 109 ++ ...vel-overview-swupdate-handler-workflow.svg | 3 + .../swupdate-script-options-and-arguments.svg | 3 + .../inc/aduc/swupdate_handler_v2.hpp | 63 + .../src/handler_create.cpp | 65 + .../src/swupdate_handler_v2.cpp | 899 +++++++++ .../swupdate_handler_v2/tests/CMakeLists.txt | 54 + .../swupdate_handler_v2}/tests/main.cpp | 3 +- .../tests/swupdate_handler_v2_ut.cpp | 440 +++++ .../adu-yocto-ab-rootfs-update/README.md | 47 + ...o-ab-rootfs-update-import-manifest-1.0.ps1 | 169 ++ .../payloads/adu-version | 1 + .../payloads/example-a-b-update.sh | 751 +++++++ .../payloads/my-update.swu | 1 + .../sample_swupdate_config.json | 5 + .../testdata/swupdate_filecopy/README.md | 15 + .../du-agent-swupdate-filecopy-test-1_1.0.swu | Bin 0 -> 1536 bytes .../example-du-swupdate-script.sh | 681 +++++++ .../swupdate_filecopy/installed-criteria.txt | 1 + .../mock-update-for-file-copy-test-1.txt | 1 + .../testdata/swupdate_filecopy/preinst.sh | 2 + .../testdata/swupdate_filecopy/sw-description | 20 + .../update_manifest_handlers/CMakeLists.txt | 1 + .../steps_handler/CMakeLists.txt | 25 +- .../steps_handler/README.md | 16 +- .../steps-handler-basic-sequence-overview.mmd | 0 .../steps-handler-basic-sequence-overview.svg | 0 ...handler-child-update-sequence-overview.mmd | 0 ...handler-child-update-sequence-overview.svg | 0 .../steps_handler/inc/aduc/steps_handler.hpp | 2 + .../steps_handler/src/handler_create.cpp | 64 + .../steps_handler/src/steps_handler.cpp | 534 +++-- src/inc/aduc/adu_core_exports.h | 5 +- src/inc/aduc/aduc_inode.h | 17 + src/inc/aduc/result.h | 1729 +++++++++++------ src/logging/zlog/inc/zlog-config.h | 2 +- src/logging/zlog/src/init.c | 2 +- src/logging/zlog/src/zlog.c | 167 +- .../linux_platform_layer/CMakeLists.txt | 35 +- .../src/linux_adu_core_impl.cpp | 237 +-- .../src/linux_adu_core_impl.hpp | 136 +- .../src/linux_device_info_exports.cpp | 163 +- .../src/os_release_info.hpp | 38 + .../linux_platform_layer/tests/CMakeLists.txt | 5 +- .../tests/mock_do_download.cpp | 30 +- .../tests/mock_do_download.hpp | 19 +- .../simulator_platform_layer/CMakeLists.txt | 1 - .../src/simulator_adu_core_impl.cpp | 19 +- .../src/simulator_adu_core_impl.hpp | 99 + src/utils/CMakeLists.txt | 9 + .../inc/aduc/connection_string_utils.h | 2 +- src/utils/c_utils/inc/aduc/string_c_utils.h | 2 + src/utils/c_utils/src/string_c_utils.c | 32 + src/utils/config_utils/CMakeLists.txt | 11 +- .../config_utils/inc/aduc/config_utils.h | 16 +- src/utils/config_utils/src/config_utils.c | 37 +- .../config_utils/tests/config_utils_ut.cpp | 219 ++- src/utils/contract_utils/CMakeLists.txt | 19 + .../contract_utils/inc/aduc/contract_utils.h | 31 + src/utils/contract_utils/src/contract_utils.c | 20 + src/utils/contract_utils/tests/CMakeLists.txt | 18 + .../tests/contract_utils_ut.cpp | 38 + src/utils/contract_utils/tests/main.cpp | 9 + src/utils/d2c_messaging/CMakeLists.txt | 26 + .../d2c_messaging/inc/aduc/d2c_messaging.h | 242 +++ src/utils/d2c_messaging/src/d2c_messaging.c | 618 ++++++ src/utils/d2c_messaging/tests/CMakeLists.txt | 24 + .../d2c_messaging/tests/d2c_messaging_ut.cpp | 718 +++++++ src/utils/d2c_messaging/tests/main.cpp | 9 + src/utils/eis_utils/CMakeLists.txt | 6 +- .../exception_utils/inc/aduc/exceptions.hpp | 2 +- src/utils/extension_utils/CMakeLists.txt | 21 +- .../inc/aduc/extension_utils.h | 11 +- .../extension_utils/src/extension_utils.c | 92 +- src/utils/file_utils/CMakeLists.txt | 17 + .../file_utils/inc/aduc/auto_opendir.hpp | 39 + src/utils/file_utils/inc/aduc/file_utils.hpp | 21 + src/utils/file_utils/src/auto_opendir.cpp | 32 + src/utils/file_utils/src/file_utils.cpp | 48 + src/utils/hash_utils/inc/aduc/hash_utils.h | 13 +- src/utils/hash_utils/src/hash_utils.c | 91 +- src/utils/hash_utils/tests/CMakeLists.txt | 1 - src/utils/https_proxy_utils/CMakeLists.txt | 24 + .../inc/aduc/https_proxy_utils.h | 35 + .../https_proxy_utils/src/https_proxy_utils.c | 186 ++ .../https_proxy_utils/tests/CMakeLists.txt | 20 + .../tests/https_proxy_utils_ut.cpp | 167 ++ src/utils/https_proxy_utils/tests/main.cpp | 9 + .../installed_criteria_utils/CMakeLists.txt | 12 +- .../tests/CMakeLists.txt | 18 +- src/utils/parser_utils/CMakeLists.txt | 17 +- .../parser_utils/inc/aduc/parser_utils.h | 36 +- src/utils/parser_utils/src/parser_utils.c | 166 +- src/utils/parser_utils/tests/CMakeLists.txt | 24 + src/utils/parser_utils/tests/main.cpp | 9 + .../parser_utils/tests/parser_utils_ut.cpp | 69 + src/utils/path_utils/CMakeLists.txt | 24 + src/utils/path_utils/inc/aduc/path_utils.h | 20 + src/utils/path_utils/src/path_utils.c | 61 + src/utils/path_utils/tests/CMakeLists.txt | 26 + src/utils/path_utils/tests/main.cpp | 9 + src/utils/path_utils/tests/path_utils_ut.cpp | 74 + src/utils/permission_utils/CMakeLists.txt | 6 +- .../inc/aduc/permission_utils.h | 2 + .../permission_utils/src/permission_utils.c | 26 + .../string_utils/inc/aduc/calloc_wrapper.hpp | 20 + .../inc/aduc/string_handle_wrapper.hpp | 20 +- .../string_utils/inc/aduc/string_utils.hpp | 18 + .../string_utils/tests/string_utils_ut.cpp | 47 + src/utils/system_utils/CMakeLists.txt | 12 +- .../system_utils/inc/aduc/system_utils.h | 6 +- src/utils/system_utils/src/system_utils.c | 79 +- src/utils/test_utils/CMakeLists.txt | 14 + src/utils/test_utils/inc/aduc/auto_dir.hpp | 38 + .../inc/aduc/auto_workflowhandle.hpp | 38 + .../test_utils/inc/aduc/file_test_utils.hpp | 21 + .../test_utils/inc/aduc/free_deleter.hpp | 27 + src/utils/test_utils/src/auto_dir.cpp | 42 + src/utils/test_utils/src/file_test_utils.cpp | 27 + src/utils/workflow_data_utils/CMakeLists.txt | 6 +- .../inc/aduc/workflow_data_utils.h | 40 - .../src/workflow_data_utils.c | 109 +- src/utils/workflow_utils/CMakeLists.txt | 14 +- .../inc/aduc/workflow_internal.h | 13 + .../workflow_utils/inc/aduc/workflow_utils.h | 154 +- src/utils/workflow_utils/src/workflow_utils.c | 811 +++++++- src/utils/workflow_utils/tests/CMakeLists.txt | 22 +- .../desired_template.json | 12 + ...anifest_downloadhandlerid_relatedfile.json | 55 + .../tests/workflow_get_update_file_ut.cpp | 135 ++ .../tests/workflow_utils_ut.cpp | 164 +- .../CreateSampleComplexUpdate.py | 177 ++ .../AduCmdlets-py/CreateSampleSimpleUpdate.py | 78 + tools/AduCmdlets-py/README.md | 26 + tools/AduCmdlets-py/scripts/aduupdate.py | 223 +++ tools/AduCmdlets/AduImportUpdate.psm1 | 7 +- tools/AduCmdlets/AduRestApi.psm1 | 2 +- tools/AduCmdlets/AduUpdate.psm1 | 14 +- .../AduCmdlets/CreateSampleComplexUpdate.ps1 | 6 +- .../AduCmdlets/ImportSampleComplexUpdate.ps1 | 8 +- tools/AduCmdlets/README.md | 8 +- .../AduCmdlets/create-adu-import-manifest.sh | 55 +- tools/selfhost/README.md | 25 + tools/selfhost/bootstrap.py | 639 ++++++ tools/selfhost/bootstrap.sh | 33 + tools/selfhost/bootstrap_config_example.json | 19 + tools/selfhost/bootstrap_config_schema.json | 78 + 511 files changed, 30742 insertions(+), 11158 deletions(-) create mode 100644 azurepipelines/build/README.md delete mode 100644 azurepipelines/build/docker/adu-ubuntu-amd64-build.yml rename azurepipelines/build/{docker => native}/adu-ubuntu-arm64-build.yml (76%) create mode 100644 azurepipelines/e2e_test/README.md create mode 100644 azurepipelines/e2e_test/e2etest.yaml create mode 100644 azurepipelines/e2e_test/scenarios/debian-10-amd64/Bundle-update.py create mode 100644 azurepipelines/e2e_test/scenarios/debian-10-amd64/Multi-Component-Update.py create mode 100644 azurepipelines/e2e_test/scenarios/debian-10-amd64/add_device_to_adu_group.py create mode 100644 azurepipelines/e2e_test/scenarios/debian-10-amd64/apt_deployment.py create mode 100644 azurepipelines/e2e_test/scenarios/debian-10-amd64/delete_device.py create mode 100644 azurepipelines/e2e_test/scenarios/debian-10-amd64/diagnostics.py create mode 100644 azurepipelines/e2e_test/scenarios/debian-10-amd64/scenario_definitions.py create mode 100644 azurepipelines/e2e_test/scenarios/debian-10-amd64/vm_setup/devicesetup.py create mode 100644 azurepipelines/e2e_test/scenarios/debian-10-amd64/vm_setup/setup.sh create mode 100644 azurepipelines/e2e_test/scenarios/testingtoolkit/README.md create mode 100644 azurepipelines/e2e_test/scenarios/testingtoolkit/__init__.py create mode 100644 azurepipelines/e2e_test/scenarios/testingtoolkit/_adu_test_toolkit.py create mode 100644 azurepipelines/e2e_test/scenarios/testingtoolkit/_version.py create mode 100644 azurepipelines/e2e_test/scenarios/testingtoolkit/requirements.txt create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/Bundle-update.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/Multi-Component-Update.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/add_device_to_adu_group.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/apt_deployment.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/delete_device.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/diagnostics.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/scenario_definitions.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/vm_setup/devicesetup.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/vm_setup/setup.sh create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/Bundle-update.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/Multi-Component-Update.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/add_device_to_adu_group.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/apt_deployment.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/delete_device.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/diagnostics.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/scenario_definitions.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/vm_setup/devicesetup.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/vm_setup/setup.sh create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/Bundle-update.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/Multi-Component-Update.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/add_device_to_adu_group.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/apt_deployment.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/delete_device.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/diagnostics.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/scenario_definitions.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/vm_setup/devicesetup.py create mode 100644 azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/vm_setup/setup.sh create mode 100644 azurepipelines/e2e_test/terraform/host/DeviceUpdateHost.tf create mode 100644 azurepipelines/e2e_test/terraform/host/variables.tf create mode 100644 azurepipelines/e2e_test/terraform/resource_group/ResourceGroup.tf create mode 100644 azurepipelines/e2e_test/terraform/resource_group/output.tf create mode 100644 azurepipelines/e2e_test/terraform/resource_group/variables.tf rename daemon/{adu-agent.service => deviceupdate-agent.service} (86%) delete mode 100644 docs/agent-reference/configurations-file.md delete mode 100644 docs/agent-reference/device-diagnostic-service.md create mode 100644 docs/agent-reference/device-update-agent-extensibility-points.md create mode 100644 docs/agent-reference/extension-contract-versions.md create mode 100644 docs/agent-reference/how-to-build-swupdate-for-swupdate-handler-unit-tests.md create mode 100644 docs/agent-reference/registering-device-update-extensions.md delete mode 100644 docs/agent-reference/upgrade-guide.md delete mode 100644 docs/agent-reference/whats-new.md delete mode 100644 docs/management-api-reference/cancel-or-retry-deployment.md delete mode 100644 docs/management-api-reference/create-or-update-deployment.md delete mode 100644 docs/management-api-reference/delete-deployment.md delete mode 100644 docs/management-api-reference/get-available-device-tags.md delete mode 100644 docs/management-api-reference/get-deployment-details.md delete mode 100644 docs/management-api-reference/get-deployment-device-states.md delete mode 100644 docs/management-api-reference/get-deployment-devices.md delete mode 100644 docs/management-api-reference/get-deployment-status.md delete mode 100644 docs/management-api-reference/get-deployments.md delete mode 100644 docs/management-api-reference/get-device-class-details.md delete mode 100644 docs/management-api-reference/get-device-classes-for-a-device.md delete mode 100644 docs/management-api-reference/get-device-classes.md delete mode 100644 docs/management-api-reference/get-device-ids-in-device-class.md delete mode 100644 docs/management-api-reference/get-device-tag.md delete mode 100644 docs/management-api-reference/get-device.md delete mode 100644 docs/management-api-reference/get-devices.md delete mode 100644 docs/management-api-reference/get-group-best-updates.md delete mode 100644 docs/management-api-reference/get-group-update-compliance.md delete mode 100644 docs/management-api-reference/get-group.md delete mode 100644 docs/management-api-reference/get-groups.md delete mode 100644 docs/management-api-reference/get-installable-update-for-device-class.md delete mode 100644 docs/management-api-reference/get-update-compliance.md delete mode 100644 docs/management-api-reference/management-api-overview.md create mode 100644 scripts/docker/README.md create mode 100755 scripts/docker/build_docker_image.sh create mode 100755 scripts/docker/dbg_docker.sh create mode 100755 scripts/docker/setup_docker_container.sh create mode 100644 scripts/docker/templates/Dockerfile.template create mode 100644 scripts/docker/templates/content_downloader.extension.template.json create mode 100644 scripts/docker/templates/content_handler.template.json create mode 100644 scripts/docker/templates/du-config.template.json create mode 100644 scripts/docker/templates/du-diagnostics-config.template.json create mode 100644 scripts/docker/templates/setup_container.sh create mode 100755 scripts/error_code_generator_defs/error_code_defs_generator.py create mode 100644 scripts/error_code_generator_defs/result_codes.json create mode 100755 scripts/generate_error_code_defs.sh create mode 100755 scripts/githooks/pre-commit.sh mode change 100755 => 100644 scripts/install-deps.sh create mode 100644 src/agent/adu_core_export_helpers/CMakeLists.txt rename src/agent/{adu_core_interface => adu_core_export_helpers}/inc/aduc/adu_core_export_helpers.h (97%) rename src/agent/{adu_core_interface => adu_core_export_helpers}/src/adu_core_export_helpers.c (89%) delete mode 100644 src/agent/adu_core_interface/inc/aduc/adu_core_json.h delete mode 100644 src/agent/adu_core_interface/src/adu_core_json.c delete mode 100644 src/agent/adu_core_interface/tests/CMakeLists.txt delete mode 100644 src/agent/adu_core_interface/tests/adu_core_export_helpers_ut.cpp delete mode 100644 src/agent/adu_core_interface/tests/adu_core_interface_ut.cpp delete mode 100644 src/agent/adu_core_interface/tests/adu_core_json_ut.cpp delete mode 100644 src/agent/adu_core_interface/tests/main.cpp delete mode 100644 src/agent/adu_core_interface/tests/result_ut.cpp delete mode 100644 src/agent/adu_core_interface/tests/startup_workflowdata_ut.cpp delete mode 100644 src/agent/adu_core_interface/tests/workflow_reboot_ut.cpp delete mode 100644 src/agent/adu_core_interface/tests/workflow_replacement_ut.cpp delete mode 100644 src/agent/adu_core_interface/tests/workflow_test_utils.cpp delete mode 100644 src/agent/adu_core_interface/tests/workflow_test_utils.h delete mode 100644 src/agent/adu_core_interface/tests/workflow_ut.cpp create mode 100644 src/agent/command_helper/CMakeLists.txt create mode 100644 src/agent/command_helper/inc/aduc/command_helper.h create mode 100644 src/agent/command_helper/src/command_helper.c create mode 100644 src/agent_orchestration/CMakeLists.txt rename src/{agent/adu_core_interface => agent_orchestration}/inc/aduc/agent_orchestration.h (87%) rename src/{agent/adu_core_interface => agent_orchestration}/src/agent_orchestration.c (71%) delete mode 100644 src/content_handlers/CMakeLists.txt delete mode 100644 src/content_handlers/inc/aduc/content_handler_factory.hpp delete mode 100644 src/content_handlers/src/content_handler_factory.cpp create mode 100644 src/deps/swupdate/.config create mode 100644 src/diagnostics_component/utils/config_utils/CMakeLists.txt create mode 100644 src/diagnostics_component/utils/config_utils/inc/diagnostics_config_utils.h create mode 100644 src/diagnostics_component/utils/config_utils/src/diagnostics_config_utils.c rename src/diagnostics_component/{diagnostics_workflow => utils/config_utils}/tests/CMakeLists.txt (61%) rename src/diagnostics_component/{diagnostics_workflow/tests/diagnostics_workflow_ut.cpp => utils/config_utils/tests/diagnostics_config_utils_ut.cpp} (68%) create mode 100644 src/diagnostics_component/utils/config_utils/tests/main.cpp delete mode 100644 src/extensions/component-enumerators/examples/contoso-component-enumerator/CMakeLists.txt delete mode 100644 src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/README.md delete mode 100644 src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-6.x.ps1 delete mode 100644 src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-8.x.ps1 create mode 100644 src/extensions/component_enumerators/examples/contoso_component_enumerator/CMakeLists.txt rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/README.md (97%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/assets/contoso-virtual-vacuum-components-diagram.svg (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/assets/contoso-virtual-vacuum-update-flow.svg (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator/contoso-component-enumerator.cpp => component_enumerators/examples/contoso_component_enumerator/contoso_component_enumerator.cpp} (52%) create mode 100644 src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/README.md rename src/extensions/{component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/contoso-components-config.json => component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/components-inventory-with-steamers.json} (88%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/demo-devices/contoso-devices/components-inventory.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/demo-devices/contoso-devices/vacuum-1/bootfs/diskimage.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/demo-devices/contoso-devices/vacuum-1/cameras/contoso-camera-serial-00000/firmware.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/demo-devices/contoso-devices/vacuum-1/cameras/contoso-camera-serial-00001/firmware.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/demo-devices/contoso-devices/vacuum-1/hostfw/firmware.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/demo-devices/contoso-devices/vacuum-1/motors/contoso-motor-serial-00000/firmware.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/demo-devices/contoso-devices/vacuum-1/motors/contoso-motor-serial-00001/firmware.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/demo-devices/contoso-devices/vacuum-1/motors/contoso-motor-serial-00002/firmware.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/demo-devices/contoso-devices/vacuum-1/rootfs/diskimage.json (100%) create mode 100644 src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/unplugged-streamers/contoso-steamer-serial-00000/firmware.json create mode 100644 src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/AduUpdate.psm1 rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/CreateSampleMSOEUpdate-1.x.ps1 (87%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/CreateSampleMSOEUpdate-2.x.ps1 (93%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/CreateSampleMSOEUpdate-3.x.ps1 (97%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/CreateSampleMSOEUpdate-4.x.ps1 (95%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/CreateSampleMSOEUpdate-5.x.ps1 (97%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-7.x.ps1 => component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-6.x.ps1} (80%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-10.x.ps1 => component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-7.x.ps1} (63%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/APT/apt-manifest-1.0.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/APT/apt-manifest-4.0.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/APT/apt-manifest-tree-1.0.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/APT/bad-apt-manifest-1.0.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/camera-firmware-1.1.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/camera-firmware-2.1.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/camera-firmware-2.2.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/camera-firmware-3.0.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/contoso-vacuum-apt-1.0.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/host-firmware-1.1.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/host-firmware-1.2.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/host-firmware-1.3.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/motor-firmware-1.1.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/motor-firmware-1.2.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/motor-firmware-1.3.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/motor-firmware-1.4.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/motor-firmware-1.5.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/motor-firmware-3.1.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/steamer-firmware-1.0.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/data-files/steamer-firmware-2.0.json (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/scripts/contoso-camera-installscript.sh (92%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/scripts/contoso-diskimage-installscript.sh (92%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/scripts/contoso-firmware-installscript.sh (92%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/scripts/contoso-motor-installscript.sh (92%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/scripts/contoso-steamer-installscript.sh (92%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/sample-updates/scripts/contoso-update-simulatorscript.sh (93%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/tools/add-packages-microsoft-com.sh (97%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/tools/reset-demo-components.sh (100%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/tutorial1.md (96%) rename src/extensions/{component-enumerators/examples/contoso-component-enumerator => component_enumerators/examples/contoso_component_enumerator}/demo/tutorial1.ps1 (96%) delete mode 100644 src/extensions/content-downloaders/CMakeLists.txt delete mode 100644 src/extensions/content-downloaders/curl-downloader/CMakeLists.txt create mode 100644 src/extensions/content_downloaders/CMakeLists.txt rename src/extensions/{content-downloaders => content_downloaders}/README.md (100%) create mode 100644 src/extensions/content_downloaders/curl_downloader/CMakeLists.txt create mode 100644 src/extensions/content_downloaders/curl_downloader/curl_content_downloader.EXPORTS.cpp rename src/extensions/{content-downloaders/curl-downloader/curl-content-downloader.cpp => content_downloaders/curl_downloader/curl_content_downloader.cpp} (90%) create mode 100644 src/extensions/content_downloaders/curl_downloader/curl_content_downloader.h rename src/extensions/{content-downloaders/deliveryoptimization-downloader => content_downloaders/deliveryoptimization_downloader}/CMakeLists.txt (58%) create mode 100644 src/extensions/content_downloaders/deliveryoptimization_downloader/deliveryoptimization_content_downloader.EXPORTS.cpp rename src/extensions/{content-downloaders/deliveryoptimization-downloader/deliveryoptimization-content-downloader.cpp => content_downloaders/deliveryoptimization_downloader/deliveryoptimization_content_downloader.cpp} (58%) create mode 100644 src/extensions/content_downloaders/deliveryoptimization_downloader/deliveryoptimization_content_downloader.h create mode 100644 src/extensions/download_handlers/CMakeLists.txt create mode 100644 src/extensions/download_handlers/README.md create mode 100644 src/extensions/download_handlers/download_handler_factory/CMakeLists.txt create mode 100644 src/extensions/download_handlers/download_handler_factory/inc/aduc/download_handler_factory.h create mode 100644 src/extensions/download_handlers/download_handler_factory/inc/aduc/download_handler_factory.hpp create mode 100644 src/extensions/download_handlers/download_handler_factory/src/download_handler_factory.cpp create mode 100644 src/extensions/download_handlers/download_handler_plugin/CMakeLists.txt create mode 100644 src/extensions/download_handlers/download_handler_plugin/inc/aduc/download_handler_plugin.h create mode 100644 src/extensions/download_handlers/download_handler_plugin/inc/aduc/download_handler_plugin.hpp create mode 100644 src/extensions/download_handlers/download_handler_plugin/src/download_handler_plugin.cpp create mode 100644 src/extensions/download_handlers/plugin_examples/CMakeLists.txt create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/CMakeLists.txt create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/CMakeLists.txt create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/lib/CMakeLists.txt create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/lib/inc/aduc/microsoft_delta_download_handler.h create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/lib/src/microsoft_delta_download_handler.c create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/plugin/CMakeLists.txt create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/plugin/src/microsoft_delta_download_handler_plugin.EXPORTS.c create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/CMakeLists.txt create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/inc/aduc/microsoft_delta_download_handler_utils.h create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/src/microsoft_delta_download_handler_utils.c create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/src/microsoft_delta_download_handler_utils.cpp create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/tests/CMakeLists.txt create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/tests/main.cpp create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/tests/microsoft_delta_download_handler_utils_ut.cpp create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/CMakeLists.txt create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/inc/aduc/source_update_cache.h create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/inc/aduc/source_update_cache_utils.h create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/src/source_update_cache.c create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/src/source_update_cache_utils.c create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/src/source_update_cache_utils.cpp create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/tests/CMakeLists.txt create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/tests/main.cpp create mode 100644 src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/tests/source_update_cache_utils_ut.cpp create mode 100644 src/extensions/extension_manager/inc/aduc/extension_manager_download_options.h create mode 100644 src/extensions/inc/aduc/exports/extension_common_export_symbols.h create mode 100644 src/extensions/inc/aduc/exports/extension_component_enumerator_export_symbols.h create mode 100644 src/extensions/inc/aduc/exports/extension_content_downloader_export_symbols.h create mode 100644 src/extensions/inc/aduc/exports/extension_content_handler_export_symbols.h create mode 100644 src/extensions/inc/aduc/exports/extension_download_handler_export_symbols.h create mode 100644 src/extensions/inc/aduc/exports/extension_export_symbols.h create mode 100644 src/extensions/shared_lib/CMakeLists.txt create mode 100644 src/extensions/shared_lib/inc/aduc/plugin_call_helper.hpp create mode 100644 src/extensions/shared_lib/inc/aduc/shared_lib.hpp create mode 100644 src/extensions/shared_lib/src/shared_lib.cpp create mode 100644 src/extensions/step_handlers/CMakeLists.txt rename src/{content_handlers => extensions/step_handlers}/apt_handler/CMakeLists.txt (73%) rename src/{content_handlers => extensions/step_handlers}/apt_handler/README.md (78%) rename src/{content_handlers => extensions/step_handlers}/apt_handler/inc/aduc/apt_handler.hpp (84%) rename src/{content_handlers => extensions/step_handlers}/apt_handler/inc/aduc/apt_parser.hpp (99%) rename src/{content_handlers => extensions/step_handlers}/apt_handler/src/apt_handler.cpp (67%) rename src/{content_handlers => extensions/step_handlers}/apt_handler/src/apt_parser.cpp (100%) rename src/{content_handlers => extensions/step_handlers}/apt_handler/tests/CMakeLists.txt (93%) rename src/{content_handlers => extensions/step_handlers}/apt_handler/tests/apt_parser_ut.cpp (100%) rename src/{content_handlers => extensions/step_handlers}/apt_handler/tests/main.cpp (100%) rename src/{content_handlers => extensions/step_handlers}/apt_handler/tests/sample_apt_manifest.json (100%) rename src/{content_handlers => extensions/step_handlers}/script_handler/CMakeLists.txt (51%) rename src/{content_handlers => extensions/step_handlers}/script_handler/README.md (93%) rename src/{content_handlers => extensions/step_handlers}/script_handler/examples/example-installscript.sh (95%) rename src/{content_handlers => extensions/step_handlers}/script_handler/images/script-handler-overview.svg (100%) rename src/{content_handlers => extensions/step_handlers}/script_handler/inc/aduc/script_handler.hpp (89%) rename src/{content_handlers => extensions/step_handlers}/script_handler/src/script_handler.cpp (85%) rename src/{content_handlers => extensions/step_handlers}/script_handler/tests/CMakeLists.txt (100%) rename src/{content_handlers => extensions/step_handlers}/script_handler/tests/main.cpp (100%) rename src/{content_handlers => extensions/step_handlers}/script_handler/tests/script_handler_ut.cpp (100%) rename src/{content_handlers => extensions/step_handlers}/simulator_handler/CMakeLists.txt (81%) rename src/{content_handlers => extensions/step_handlers}/simulator_handler/README.md (100%) rename src/{content_handlers => extensions/step_handlers}/simulator_handler/du-simulator-data-template.jsonc (82%) rename src/{content_handlers => extensions/step_handlers}/simulator_handler/inc/aduc/simulator_handler.hpp (92%) rename src/{content_handlers => extensions/step_handlers}/simulator_handler/src/simulator_handler.cpp (88%) rename src/{content_handlers => extensions/step_handlers}/simulator_handler/tests/CMakeLists.txt (89%) rename src/{content_handlers => extensions/step_handlers}/simulator_handler/tests/main.cpp (100%) rename src/{content_handlers => extensions/step_handlers}/simulator_handler/tests/simulator_handler_unit_tests.cpp (89%) rename src/{content_handlers => extensions/step_handlers}/swupdate_handler/CMakeLists.txt (83%) rename src/{content_handlers => extensions/step_handlers}/swupdate_handler/README.md (100%) rename src/{content_handlers => extensions/step_handlers}/swupdate_handler/inc/aduc/swupdate_handler.hpp (92%) rename src/{content_handlers => extensions/step_handlers}/swupdate_handler/src/swupdate_handler.cpp (74%) create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/CMakeLists.txt create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/README.md create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/images/highlevel-overview-swupdate-handler-workflow.svg create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/images/swupdate-script-options-and-arguments.svg create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/inc/aduc/swupdate_handler_v2.hpp create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/src/handler_create.cpp create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/src/swupdate_handler_v2.cpp create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/tests/CMakeLists.txt rename src/{diagnostics_component/diagnostics_workflow => extensions/step_handlers/swupdate_handler_v2}/tests/main.cpp (74%) create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/tests/swupdate_handler_v2_ut.cpp create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/README.md create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/create-yocto-ab-rootfs-update-import-manifest-1.0.ps1 create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/payloads/adu-version create mode 100755 src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/payloads/example-a-b-update.sh create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/payloads/my-update.swu create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/sample_swupdate_config.json create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/swupdate_filecopy/README.md create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/swupdate_filecopy/du-agent-swupdate-filecopy-test-1_1.0.swu create mode 100755 src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/swupdate_filecopy/example-du-swupdate-script.sh create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/swupdate_filecopy/installed-criteria.txt create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/swupdate_filecopy/mock-update-for-file-copy-test-1.txt create mode 100755 src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/swupdate_filecopy/preinst.sh create mode 100644 src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/swupdate_filecopy/sw-description create mode 100644 src/extensions/update_manifest_handlers/CMakeLists.txt rename src/{content_handlers => extensions/update_manifest_handlers}/steps_handler/CMakeLists.txt (80%) rename src/{content_handlers => extensions/update_manifest_handlers}/steps_handler/README.md (93%) rename src/{content_handlers => extensions/update_manifest_handlers}/steps_handler/assets/steps-handler-basic-sequence-overview.mmd (100%) rename src/{content_handlers => extensions/update_manifest_handlers}/steps_handler/assets/steps-handler-basic-sequence-overview.svg (100%) rename src/{content_handlers => extensions/update_manifest_handlers}/steps_handler/assets/steps-handler-child-update-sequence-overview.mmd (100%) rename src/{content_handlers => extensions/update_manifest_handlers}/steps_handler/assets/steps-handler-child-update-sequence-overview.svg (100%) rename src/{content_handlers => extensions/update_manifest_handlers}/steps_handler/inc/aduc/steps_handler.hpp (90%) create mode 100644 src/extensions/update_manifest_handlers/steps_handler/src/handler_create.cpp rename src/{content_handlers => extensions/update_manifest_handlers}/steps_handler/src/steps_handler.cpp (74%) create mode 100644 src/inc/aduc/aduc_inode.h create mode 100644 src/platform_layers/linux_platform_layer/src/os_release_info.hpp create mode 100644 src/utils/contract_utils/CMakeLists.txt create mode 100644 src/utils/contract_utils/inc/aduc/contract_utils.h create mode 100644 src/utils/contract_utils/src/contract_utils.c create mode 100644 src/utils/contract_utils/tests/CMakeLists.txt create mode 100644 src/utils/contract_utils/tests/contract_utils_ut.cpp create mode 100644 src/utils/contract_utils/tests/main.cpp create mode 100644 src/utils/d2c_messaging/CMakeLists.txt create mode 100644 src/utils/d2c_messaging/inc/aduc/d2c_messaging.h create mode 100644 src/utils/d2c_messaging/src/d2c_messaging.c create mode 100644 src/utils/d2c_messaging/tests/CMakeLists.txt create mode 100644 src/utils/d2c_messaging/tests/d2c_messaging_ut.cpp create mode 100644 src/utils/d2c_messaging/tests/main.cpp create mode 100644 src/utils/file_utils/CMakeLists.txt create mode 100644 src/utils/file_utils/inc/aduc/auto_opendir.hpp create mode 100644 src/utils/file_utils/inc/aduc/file_utils.hpp create mode 100644 src/utils/file_utils/src/auto_opendir.cpp create mode 100644 src/utils/file_utils/src/file_utils.cpp create mode 100644 src/utils/https_proxy_utils/CMakeLists.txt create mode 100644 src/utils/https_proxy_utils/inc/aduc/https_proxy_utils.h create mode 100644 src/utils/https_proxy_utils/src/https_proxy_utils.c create mode 100644 src/utils/https_proxy_utils/tests/CMakeLists.txt create mode 100644 src/utils/https_proxy_utils/tests/https_proxy_utils_ut.cpp create mode 100644 src/utils/https_proxy_utils/tests/main.cpp create mode 100644 src/utils/parser_utils/tests/CMakeLists.txt create mode 100644 src/utils/parser_utils/tests/main.cpp create mode 100644 src/utils/parser_utils/tests/parser_utils_ut.cpp create mode 100644 src/utils/path_utils/CMakeLists.txt create mode 100644 src/utils/path_utils/inc/aduc/path_utils.h create mode 100644 src/utils/path_utils/src/path_utils.c create mode 100644 src/utils/path_utils/tests/CMakeLists.txt create mode 100644 src/utils/path_utils/tests/main.cpp create mode 100644 src/utils/path_utils/tests/path_utils_ut.cpp create mode 100644 src/utils/test_utils/CMakeLists.txt create mode 100644 src/utils/test_utils/inc/aduc/auto_dir.hpp create mode 100644 src/utils/test_utils/inc/aduc/auto_workflowhandle.hpp create mode 100644 src/utils/test_utils/inc/aduc/file_test_utils.hpp create mode 100644 src/utils/test_utils/inc/aduc/free_deleter.hpp create mode 100644 src/utils/test_utils/src/auto_dir.cpp create mode 100644 src/utils/test_utils/src/file_test_utils.cpp create mode 100644 src/utils/workflow_utils/tests/testdata/workflow_get_update_file/desired_template.json create mode 100644 src/utils/workflow_utils/tests/testdata/workflow_get_update_file/updateManifest_downloadhandlerid_relatedfile.json create mode 100644 src/utils/workflow_utils/tests/workflow_get_update_file_ut.cpp create mode 100755 tools/AduCmdlets-py/CreateSampleComplexUpdate.py create mode 100755 tools/AduCmdlets-py/CreateSampleSimpleUpdate.py create mode 100644 tools/AduCmdlets-py/README.md create mode 100644 tools/AduCmdlets-py/scripts/aduupdate.py create mode 100644 tools/selfhost/README.md create mode 100755 tools/selfhost/bootstrap.py create mode 100755 tools/selfhost/bootstrap.sh create mode 100644 tools/selfhost/bootstrap_config_example.json create mode 100644 tools/selfhost/bootstrap_config_schema.json diff --git a/.gitattributes b/.gitattributes index 176a458f9..7de3372f1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,6 @@ * text=auto + +# require LF ending for shell scripts +*.sh text eol=lf + +*.png binary diff --git a/.gitignore b/.gitignore index 0d812b278..36b31074c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ TAGS .TAGS !TAGS/ tags +tags.lock +tags.temp .tags !tags/ gtags.files @@ -25,6 +27,9 @@ cscope.out cscope.in.out cscope.po.out +### Python Module Files +*__pycache__ +*__pycache__/* ### ADU build environment out/ build/ diff --git a/CMakeLists.txt b/CMakeLists.txt index a52998a4e..7eafac67c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,25 +35,25 @@ include (aduc_helpers) # https://semver.org/spec/v2.0.0.html set_cache_with_env_or_default ( ADUC_VERSION_MAJOR - "0" + "1" STRING "The major part of the semantic version") set_cache_with_env_or_default ( ADUC_VERSION_MINOR - "8" + "0" STRING "The minor part of the semantic version") set_cache_with_env_or_default ( ADUC_VERSION_PATCH - "2" + "0" STRING "The patch part of the semantic version") set_cache_with_env_or_default ( ADUC_VERSION_PRERELEASE - "public-preview" + "" STRING "The pre-release part of the semantic version") @@ -136,6 +136,11 @@ set ( "content_handler.json" CACHE STRING "Content handler registration file name.") +set ( + ADUC_DOWNLOAD_HANDLER_REG_FILENAME + "download_handler.json" + CACHE STRING "Download handler registration file name.") + set ( ADUC_EXTENSION_REG_FILENAME "extension.json" @@ -171,6 +176,16 @@ set ( "${ADUC_EXTENSIONS_FOLDER}/${ADUC_EXTENSIONS_SUBDIR_UPDATE_CONTENT_HANDLERS}" CACHE STRING "Sub folder for update content handler extensions.") +set ( + ADUC_EXTENSIONS_SUBDIR_DOWNLOAD_HANDLERS + "download_handlers" + CACHE STRING "Sub folder for download handler extensions.") + +set ( + ADUC_DOWNLOAD_HANDLER_EXTENSION_DIR + "${ADUC_EXTENSIONS_FOLDER}/${ADUC_EXTENSIONS_SUBDIR_DOWNLOAD_HANDLERS}" + CACHE STRING "Path for the download handler extensions") + set ( ADUC_DOWNLOADS_FOLDER "${ADUC_DATA_FOLDER}/downloads" @@ -181,6 +196,20 @@ set ( "microsoft/swupdate" CACHE STRING "The list of content handlers.") +set ( + ADUC_COMMANDS_FIFO_NAME + "${ADUC_DATA_FOLDER}/du-commands.fifo" + CACHE STRING "The named-pipe for commands IPC.") + +# +# Starting from version 0.8.1, Device Update Agent must support only one version of the update manifest +# based on the base dtmi model that the Agent announces when connecting to the IoT Hub. +# +# For this version, "dtmi:azure:iot:deviceUpdateModel;2" is using the manifest version verison 5. +# +set (SUPPORTED_UPDATE_MANIFEST_VERSION_MIN 4) +set (SUPPORTED_UPDATE_MANIFEST_VERSION_MAX 5) + # # DeviceInfo configuration # These values must be modified to describe your device. @@ -324,12 +353,116 @@ set ( "syslog" CACHE STRING "The syslog group.") +set ( + ADUC_IOT_HUB_PROTOCOL + "IotHub_Protocol_from_Config" + CACHE + STRING + "The protocol for Azure IotHub SDK communication. Options are MQTT, MQTT_over_WebSockets, and IotHub_Protocol_from_Config" +) + +# Device Update Repo Locations + +set ( + ADUC_SCRIPTS_FOLDER + "/scripts/" + CACHE STRING "The folder that holds all of the scripts") + +set ( + ADUC_SCRIPTS_FOLDER_PATH + "${PROJECT_SOURCE_DIR}/${ADUC_SCRIPTS_FOLDER}" + CACHE STRING "Path to the scripts folder within the Device Update project") + +# Error Code Generator Script Name +set ( + ADUC_ERROR_CODE_GENERATOR_SCRIPT_FILENAME + "generate_error_code_defs.sh" + CACHE STRING "The script file that generates the error code definitions") + +# Error Code Definition Folder and Filename +set ( + ADUC_ERROR_CODE_DEFS_FOLDER + "/src/inc/aduc/" + CACHE STRING "The folder within Device Update to generate the result.h file to") + +set ( + ADUC_GENERATED_ERROR_CODE_FOLDER_PATH + "${PROJECT_SOURCE_DIR}/${ADUC_ERROR_CODE_DEFS_FOLDER}" + CACHE STRING "Path to the folder to generate the error code file") + +set ( + ADUC_ERROR_CODE_DEFS_FILENAME + "result.h" + CACHE STRING "File name for the error code definitions") + +# Error Code Json Folder, Filename, and Path +set ( + ADUC_ERROR_CODE_GENERATOR_JSON_FOLDER + "error_code_generator_defs" + CACHE STRING "Name of the folder that contains the error codes JSON file") + +set ( + ADUC_ERROR_CODE_JSON_FILENAME + "result_codes.json" + CACHE STRING "Name of the json file that defines the error codes for Device Update") + +set ( + ADUC_ERROR_CODE_JSON_FOLDER_PATH + "${ADUC_SCRIPTS_FOLDER_PATH}/${ADUC_ERROR_CODE_GENERATOR_JSON_FOLDER}" + CACHE STRING "Path to the folder where the JSON error code definition file lives") + +# Paths for the Folders + +get_filename_component ( + ADUC_PATH_TO_ERROR_CODE_GENERATOR_SCRIPT + "${ADUC_SCRIPTS_FOLDER_PATH}/${ADUC_ERROR_CODE_GENERATOR_SCRIPT_FILENAME}" + ABSOLUTE + "/") + +get_filename_component ( + ADUC_PATH_TO_ERROR_CODE_JSON_FILE + "${ADUC_ERROR_CODE_JSON_FOLDER_PATH}/${ADUC_ERROR_CODE_JSON_FILENAME}" + ABSOLUTE + "/") + +get_filename_component ( + ADUC_GENERATED_ERROR_CODE_FILE_PATH + "${ADUC_GENERATED_ERROR_CODE_FOLDER_PATH}/${ADUC_ERROR_CODE_DEFS_FILENAME}" + ABSOLUTE + "/") + +####### +# BEGIN Delta Downloader Handler Source Update Cache Configurations + +# Source Update Cache Commit Strategy +# +# Value values are: +# "TWO_PHASE_COMMIT" - Moves the incoming files then deletes previous. +# "OVERWRITE" - Saves space by deleting existing source updates first. +set ( + ADUC_DELTA_DOWNLOAD_HANDLER_SOURCE_UPDATE_CACHE_COMMIT_STRATEGY + "TWO_PHASE_COMMIT" + CACHE STRING "The commit strategy for the source update cache.") + +# Source Update Cache Directory +# +# Directory where an update from download work folder is cached after successful workflow processing +set ( + ADUC_DELTA_DOWNLOAD_HANDLER_SOURCE_UPDATE_CACHE_DIR + "${ADUC_DATA_FOLDER}/sdc" + CACHE STRING + "The base directory to cache source update payloads that delta updates are based upon.") + +# END Delta Downloader Handler Source Update Cache Configurations +####### + option (ADUC_WARNINGS_AS_ERRORS "Treat warnings as errors (-Werror)" ON) option (ADUC_BUILD_UNIT_TESTS "Build unit tests and mock some functionality" OFF) option (ADUC_BUILD_DOCUMENTATION "Build documentation files" OFF) option (ADUC_BUILD_PACKAGES "Build the ADU Agent packages" OFF) option (ADUC_INSTALL_DAEMON "Install the ADU Agent as a daemon" ON) option (ADUC_REGISTER_DAEMON "Register the ADU Agent daemon with the system" ON) +option (ADUC_TRACE_TARGET_DEPS "Trace target dependencies" OFF) ### End CMake Options @@ -350,11 +483,31 @@ get_filename_component ( "${ADUC_CONF_FOLDER}/${DIAGNOSTICS_CONFIG_FILE}" ABSOLUTE "/") + set ( DIAGNOSTICS_CONFIG_FILE_PATH "${DIAGNOSTICS_CONFIG_FILE_PATH}" CACHE STRING "Path to the diagnostics configuration file.") +# Generate the error code definition file +execute_process ( + COMMAND + ${ADUC_PATH_TO_ERROR_CODE_GENERATOR_SCRIPT} --json-file-path + "${ADUC_PATH_TO_ERROR_CODE_JSON_FILE}" --result-file-path + "${ADUC_GENERATED_ERROR_CODE_FILE_PATH}" -s + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + RESULT_VARIABLE GENERATE_ERROR_CODE_DEFS_RESULT) + +if (NOT + GENERATE_ERROR_CODE_DEFS_RESULT + EQUAL + "0") + message ( + FATAL_ERROR + "Failed to generate error code definition from json file ${ADUC_PATH_TO_ERROR_CODE_JSON_FILE} to ${ADUC_GENERATED_ERROR_CODE_FILE_PATH} with error code ${GENERATE_ERROR_CODE_DEFS_RESULT}" + ) +endif () + if (ADUC_BUILD_UNIT_TESTS) # Need to be in the root directory to place CTestTestfile.cmake in root # of output folder. @@ -362,7 +515,6 @@ if (ADUC_BUILD_UNIT_TESTS) copy_test_data ("${CMAKE_SOURCE_DIR}" "${ADUC_TEST_DATA_PATH_SEGMENT}" "${ADUC_TEST_DATA_FOLDER}") - endif () if (ADUC_INSTALL_DAEMON) diff --git a/SUPPORT.md b/SUPPORT.md index 8893af710..83c230212 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,18 +1,18 @@ # Support -## How to file issues and get help +## How to file issues and get help -* Have a technical question? +* Have a technical question? Ask on Stack Overflow with tag "iot-hub-device-update". -* Found a bug, or want to request a new feature? +* Found a bug, or want to request a new feature? You can use [GitHub Issues](https://github.com/Azure/iot-hub-device-update/issues) to track bugs and feature requests. If you feel like taking a stab at fixing the bug/adding the feature, please see the [Contribution guidance here](/CONTRIBUTING.md). -* Need support? +* Need support? Please document your question on [GitHub Issues](https://github.com/Azure/iot-hub-device-update/issues) thoroughly, attach logs and give repro steps, so that we can help you unblock as soon as possible. Note that currently Device Update for Iot Hub is in public preview stage, so there is no guaranteed response time, but we will get back to you as soon as we can. * You can also contact Microsoft Support, see details [here](https://docs.microsoft.com/azure/iot-hub-device-update/troubleshoot-device-update#contact) -## Microsoft Support Policy +## Microsoft Support Policy Support for IoT Hub Device Update is limited to the resources listed above. diff --git a/azurepipelines/build/README.md b/azurepipelines/build/README.md new file mode 100644 index 000000000..27a67a76b --- /dev/null +++ b/azurepipelines/build/README.md @@ -0,0 +1,39 @@ +# Azure Build Pipelines ReadMe + +## Introduction + +This document is intended as a guideline to understand the structure of the current build pipeline and to add more pipelines in the future when needed. + +## Build Templates + +Underneath the `templates/` directory there are two `*.yml` files. +One is `adu-native-build-steps.yml`, because the default Azure VM is running on Ubuntu 18.04 amd64 environment, this is the current supported native build. The other one is `adu-docker-build.steps.yml`, which is a template of docker build. +For the templates, there are two parameters: `targetOs` and `targetArch`. These parameters will be passed from build pipeline yaml files. + +## Native Builds + +The native build calls `scripts/install-deps.sh` and then calls `scripts/build.sh`. + +## Docker Builds + +The docker build pulls a container with the dependencies pre-installed from Azure Container Registry. This way, the build time can be reduced because the dependencies don't need to be installed on each build. + +## Add a New Pipeline + +If you need to add support for a new platform, likely it does not have native Azure VM support (currently it only supports Ubuntu AMD64). In this case, you need to: + +1. Create a docker container image with the desirable OS and architecture, clone the ADU repo +2. run `scripts/install-deps.sh` to install dependencies on this container +3. Push the image to Azure Container Registry +4. If the combination of distro & architecture already exsits in the below matrix, you would find the yaml file, and then add a `job` on that yaml file. Example: Job `BuildAduAgent_ubuntu2004` and job `BuildAduAgent_ubuntu1804` on `docker/adu-ubuntu-arm64-build.yml`. + +Below is a matrix of currently supported pipeline builds +| Distro | Architecture| Yaml File | +|--------|--------------|-----------| +| Ubuntu 18.04 | amd64 | native/adu-ubuntu-amd64-build.yml | +| Ubuntu 20.04 | amd64 | docker/adu-ubuntu-amd64-build.yml | +| Ubuntu 18.04 | arm64 | docker/adu-ubuntu-arm64-build.yml | +| Ubuntu 20.04 | arm64 | docker/adu-ubuntu-arm64-build.yml | +| Debian 9 | arm32 | docker/adu-debian-arm32-build.yml | +| Debian 10 | arm64 | docker/adu-debian-arm64-build.yml | +| Debian 10 | amd64 | docker/adu-debian-amd64-build.yml | diff --git a/azurepipelines/build/docker/adu-debian-amd64-build.yml b/azurepipelines/build/docker/adu-debian-amd64-build.yml index 1d2597ff7..6ea985754 100644 --- a/azurepipelines/build/docker/adu-debian-amd64-build.yml +++ b/azurepipelines/build/docker/adu-debian-amd64-build.yml @@ -1,8 +1,8 @@ variables: - version.major: 0 - version.minor: 8 + version.major: 1 + version.minor: 0 version.patch: 0 - version.pre-release: "public-preview" + version.pre-release: "" version.build: $[format('{0:yyyyMMdd}-{0:HHmmss}', pipeline.startTime)] # Environment variables for all client builds: ADUC_VERSION_MAJOR: $(version.major) diff --git a/azurepipelines/build/docker/adu-debian-arm32-build.yml b/azurepipelines/build/docker/adu-debian-arm32-build.yml index d5e1203be..0682b3e25 100644 --- a/azurepipelines/build/docker/adu-debian-arm32-build.yml +++ b/azurepipelines/build/docker/adu-debian-arm32-build.yml @@ -1,8 +1,8 @@ variables: - version.major: 0 - version.minor: 8 - version.patch: 2 - version.pre-release: "public-preview" + version.major: 1 + version.minor: 0 + version.patch: 0 + version.pre-release: "" version.build: $[format('{0:yyyyMMdd}-{0:HHmmss}', pipeline.startTime)] # Environment variables for all client builds: @@ -44,7 +44,7 @@ pr: branches: include: - main - # - release/* + - release/* # - feature/* #- dev/* paths: @@ -60,13 +60,13 @@ pr: - licenses/* jobs: - - job: BuildAduAgent - displayName: "Build ADU Agent" + - job: BuildAduAgent_debian10 + displayName: "Build ADU Agent - Debian 10" timeoutInMinutes: 360 cancelTimeoutInMinutes: 360 pool: aduc_1es_client_pool steps: - template: ../templates/adu-docker-build-steps.yml parameters: - targetOs: 'debian9' + targetOs: 'debian10' targetArch: 'arm32' diff --git a/azurepipelines/build/docker/adu-debian-arm64-build.yml b/azurepipelines/build/docker/adu-debian-arm64-build.yml index a22e1281b..dc4a1a49b 100644 --- a/azurepipelines/build/docker/adu-debian-arm64-build.yml +++ b/azurepipelines/build/docker/adu-debian-arm64-build.yml @@ -1,8 +1,8 @@ variables: - version.major: 0 - version.minor: 8 + version.major: 1 + version.minor: 0 version.patch: 0 - version.pre-release: "public-preview" + version.pre-release: "" version.build: $[format('{0:yyyyMMdd}-{0:HHmmss}', pipeline.startTime)] # Environment variables for all client builds: ADUC_VERSION_MAJOR: $(version.major) diff --git a/azurepipelines/build/docker/adu-ubuntu-amd64-build.yml b/azurepipelines/build/docker/adu-ubuntu-amd64-build.yml deleted file mode 100644 index 4e7558e14..000000000 --- a/azurepipelines/build/docker/adu-ubuntu-amd64-build.yml +++ /dev/null @@ -1,71 +0,0 @@ -variables: - version.major: 0 - version.minor: 8 - version.patch: 2 - version.pre-release: "public-preview" - version.build: $[format('{0:yyyyMMdd}-{0:HHmmss}', pipeline.startTime)] - # Environment variables for all client builds: - ADUC_VERSION_MAJOR: $(version.major) - ADUC_VERSION_MINOR: $(version.minor) - ADUC_VERSION_PATCH: $(version.patch) - ADUC_VERSION_PRERELEASE: $(version.pre-release) - ADUC_VERSION_BUILD: $(version.build) - ADUC_DEBIAN_PACKAGE_ARCHITECTURE: "amd64" - - ENABLE_ADU_TELEMETRY_REPORTING: true - # ADUC_BUILDER_IDENTIFIER will be set to "DU" short for Device Update by default, for Device Update-sourced builder - ADUC_BUILDER_IDENTIFIER: DU - # DO requires gcc greater than 6 for c++17 support. - # gcc-8 matches what is built with poky warrior. - CC: gcc-8 - CXX: g++-8 - -name: $(version.major).$(version.minor).$(version.patch)-$(version.pre-release)+$(version.build) - -resources: - - repo: self - -trigger: - branches: - include: - - main - - release/* - paths: - exclude: - - docs/* - - README.md - - LICENSE.md - - .clang-format - - .cmake-format.json - - docker/* - -pr: - branches: - include: - - main - - release/* - # - feature/* - #- dev/* - paths: - exclude: - - docs/* - - README.md - - LICENSE.md - - .clang-format - - .cmake-format.json - - tools/* - - docker/* - - scripts/* - - licenses/* - -jobs: - - job: BuildAduAgent - displayName: "Build ADU Agent" - timeoutInMinutes: 360 - cancelTimeoutInMinutes: 360 - pool: aduc_1es_client_pool - steps: - - template: ../templates/adu-docker-build-steps.yml - parameters: - targetOs: 'ubuntu2004' - targetArch: 'amd64' diff --git a/azurepipelines/build/native/adu-ubuntu-amd64-build.yml b/azurepipelines/build/native/adu-ubuntu-amd64-build.yml index 6cff6e0e5..cab66f924 100644 --- a/azurepipelines/build/native/adu-ubuntu-amd64-build.yml +++ b/azurepipelines/build/native/adu-ubuntu-amd64-build.yml @@ -1,8 +1,8 @@ variables: - version.major: 0 - version.minor: 8 - version.patch: 2 - version.pre-release: "public-preview" + version.major: 1 + version.minor: 0 + version.patch: 0 + version.pre-release: "" version.build: $[format('{0:yyyyMMdd}-{0:HHmmss}', pipeline.startTime)] # Environment variables for all client builds: @@ -62,13 +62,24 @@ pr: - licenses/* jobs: - - job: BuildAduAgent - displayName: "Build ADU Agent" + - job: BuildAduAgent_ubuntu1804 + displayName: "Build ADU Agent - Ubuntu 18.04 (amd64)" timeoutInMinutes: 60 cancelTimeoutInMinutes: 60 pool: aduc_1es_client_pool steps: - - template: ../templates/adu-native-build-steps.yml + - template: ../templates/adu-docker-build-steps.yml parameters: targetOs: 'ubuntu1804' targetArch: 'amd64' + + - job: BuildAduAgent_ubuntu2004 + displayName: "Build ADU Agent - Ubuntu 20.04 (amd64)" + timeoutInMinutes: 60 + cancelTimeoutInMinutes: 60 + pool: 1es_hosted_pool_ubuntu2004 + steps: + - template: ../templates/adu-docker-build-steps.yml + parameters: + targetOs: 'ubuntu2004' + targetArch: 'amd64' diff --git a/azurepipelines/build/docker/adu-ubuntu-arm64-build.yml b/azurepipelines/build/native/adu-ubuntu-arm64-build.yml similarity index 76% rename from azurepipelines/build/docker/adu-ubuntu-arm64-build.yml rename to azurepipelines/build/native/adu-ubuntu-arm64-build.yml index 75bc4a491..a78792e3f 100644 --- a/azurepipelines/build/docker/adu-ubuntu-arm64-build.yml +++ b/azurepipelines/build/native/adu-ubuntu-arm64-build.yml @@ -1,8 +1,8 @@ variables: - version.major: 0 - version.minor: 8 - version.patch: 2 - version.pre-release: "public-preview" + version.major: 1 + version.minor: 0 + version.patch: 0 + version.pre-release: "" version.build: $[format('{0:yyyyMMdd}-{0:HHmmss}', pipeline.startTime)] # Environment variables for all client builds: ADUC_VERSION_MAJOR: $(version.major) @@ -60,24 +60,24 @@ pr: jobs: - job: BuildAduAgent_ubuntu2004 - displayName: "Build ADU Agent - Ubuntu 20.04" - timeoutInMinutes: 360 - cancelTimeoutInMinutes: 360 - pool: aduc_1es_client_pool + displayName: "Build ADU Agent - Ubuntu 20.04 (arm64)" + timeoutInMinutes: 60 + cancelTimeoutInMinutes: 60 + pool: ubuntu2004_arm_pool steps: - - template: ../templates/adu-docker-build-steps.yml + - template: ../templates/adu-native-build-steps.yml parameters: targetOs: 'ubuntu2004' targetArch: 'arm64' - job: BuildAduAgent_ubuntu1804 - displayName: "Build ADU Agent - Ubuntu 18.04" - timeoutInMinutes: 360 - cancelTimeoutInMinutes: 360 - pool: aduc_1es_client_pool + displayName: "Build ADU Agent - Ubuntu 18.04 (arm64)" + timeoutInMinutes: 60 + cancelTimeoutInMinutes: 60 + pool: ubuntu1804_arm_pool steps: - - template: ../templates/adu-docker-build-steps.yml + - template: ../templates/adu-native-build-steps.yml parameters: targetOs: 'ubuntu1804' targetArch: 'arm64' diff --git a/azurepipelines/build/templates/adu-docker-build-steps.yml b/azurepipelines/build/templates/adu-docker-build-steps.yml index d06585c82..a47806a64 100644 --- a/azurepipelines/build/templates/adu-docker-build-steps.yml +++ b/azurepipelines/build/templates/adu-docker-build-steps.yml @@ -65,7 +65,6 @@ steps: - task: PublishPipelineArtifact@0 displayName: "Publish Pipeline Artifacts" - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) inputs: artifactName: "adu-client-${{parameters.targetOs}}-${{parameters.targetArch}}" targetPath: "$(Build.ArtifactStagingDirectory)" diff --git a/azurepipelines/build/templates/adu-native-build-steps.yml b/azurepipelines/build/templates/adu-native-build-steps.yml index 785f45753..fc0500b3c 100644 --- a/azurepipelines/build/templates/adu-native-build-steps.yml +++ b/azurepipelines/build/templates/adu-native-build-steps.yml @@ -8,17 +8,18 @@ parameters: type: string steps: - - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 - displayName: "Component Detection" - inputs: - sourceScanPath: src + - ${{ if eq(parameters.targetArch, 'amd64') }}: + - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 + displayName: "Component Detection" + inputs: + sourceScanPath: src - task: Bash@3 displayName: "Install ADUC dependencies" inputs: targetType: "filePath" filePath: $(Build.SourcesDirectory)/scripts/install-deps.sh - arguments: --install-aduc-deps --install-packages + arguments: --install-all-deps - task: Bash@3 displayName: "Build Client, Unit Tests and Packages: linux-MinSizeRel" @@ -76,7 +77,6 @@ steps: - task: PublishPipelineArtifact@0 displayName: "Publish Pipeline Artifacts" - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) inputs: artifactName: "adu-client-${{parameters.targetOs}}-${{parameters.targetArch}}" targetPath: "$(Build.ArtifactStagingDirectory)" @@ -86,14 +86,15 @@ steps: # even if popd, pushd, or cd fails. # Ignore SC2209: Use var=$(command) to assign output (or quote to assign string). # We use 'ret=return' or 'ret=exit' to support dot sourcing. - - bash: | - sudo apt install -y shellcheck - shellcheck -s bash -e SC2164 -e SC2209 $(find -name "*.sh") - exit $? - displayName: "Run ShellCheck" + - ${{ if eq(parameters.targetArch, 'amd64') }}: + - bash: | + /tmp/deviceupdate-shellcheck --shell=bash $(find -name "*.sh" | grep -v -e '^\.\/out') + exit $? + displayName: "Run ShellCheck" - - task: ComponentGovernanceComponentDetection@0 - inputs: - scanType: "Register" - verbosity: "Verbose" - alertWarningLevel: "High" + - ${{ if eq(parameters.targetArch, 'amd64') }}: + - task: ComponentGovernanceComponentDetection@0 + inputs: + scanType: "Register" + verbosity: "Verbose" + alertWarningLevel: "High" diff --git a/azurepipelines/e2e_test/README.md b/azurepipelines/e2e_test/README.md new file mode 100644 index 000000000..b943fa1ce --- /dev/null +++ b/azurepipelines/e2e_test/README.md @@ -0,0 +1,228 @@ +# End-To-End (E2E) Test Automation Pipeline + +## DISCLAIMER + +This pipeline and test suite is published here for the convenience of our customers. We make ABSOLUTELY NO COMMITMENTS to maintaining, updating, or responding to questions about the pipeline. It is provided as is. + +## Introduction + +This section describes the structure of the scenarios in the pipeline, how Terraform is used within this project to deploy the virtual machines (VMs) to be used for testing, the structure of the `e2etest.yml` file, and how results are reported. Do not consider this an exhaustive list of capabilities and work done on the E2E test automation infrastructure just an introduction to the project. + +## Build Pipelines + +Underneath the `build/` directory there are `*.yml` files that implement the cloud build process for each of type of the Device Update Agent. Below is a matrix of currently supported pipeline builds + +| Distribution | Architecture| Yaml File | +|--------------|--------------|-----------| +| Ubuntu 18.04 | amd64 | adu-ubuntu-amd64-build.yml | +| Ubuntu 20.04 | arm64 | adu-ubuntu-arm64-build.yml | + +The `*.yml` files contained in this directory are templated so they can either be called by other jobs or by the build pipelines directly. + +## Directory Structure and Relevant Files + +- The `scenarios/` directory contains subdirectories for each of the distribution and architecture combinations that Device Update for IotHub currently implements for automated E2E testing as well as the `testingtoolkit`. +- Within each distribution and architecture directory there is always a directory named `vm-setup` which contains the `setup.sh` script for installing the Device Update for IotHub artifact under test on the VM as well as a `devicesetup.py` used by the pipeline to create the device or module for the test and generate the `du-config.json` configuration file. These two files are used to setup each of the artifacts under test for their parent distribution and architecture scenario. +- The rest of the files in the directory are the individual tests for the scenario. + +## Supported Scenarios and Scenario Development Plans + +The following is a list of the currently planned scenarios that will be run by the automated E2E test pipeline: + +1. APT Deployment +2. Diagnostics Log Collection +3. Multi-Component Update +4. Bundle Updates (Multi-Step Updates) +5. Agent Update + +The following tests are not expected to be supported by the E2E pipeline in it's current form: + +1. Nested Edge +2. SwUpdate Deployment + +All together there are 7 tests that will be run per distribution, however it is likely that the Nested Edge and SwUpdate Deployment's will need their own VMs/networks to be properly configured in order for these tests to be run. They are noted here for the sake of completeness and transparency. + +## Terraform Infrastructure + +Terraform is an automated infrastructure deployment tool that allows the user to define elements of their infrastructure in `*.yml` files. You can read more about the Terraform on Azure [here](https://docs.microsoft.com/en-us/azure/developer/terraform/). Our Terraform script deploys Azure VM hosts defined within `terraform/hosts/` to create the VMs used for testing. + +### Terraform File Structure and Use for Creating VMs + +| File | Description | +|:---------:|-------------| +| `terraform/host/DeviceUpdateHost.tf` | Defines the infrastructure needed to setup the VM for the E2E test | +| `terraform/host/variables.tf` | Defines the input variables for the `DeviceUpdateHost.tf` to tailor the VM creation process per test scenario | +| `terraform/resource_group/ResourceGroup.tf` | Defines the process for creating the resource group to be contain all the resources created per run of the pipeline | +| `terraform/resource_group/variables.tf` | Defines the input variables for creating the resource group | +| `terraform/resource_group/outputs.tf` | Exposes the variables and definitions created by `ResourceGroup.tf` | + +### Terraform Variables + +The following is a list of variables that the Device Update for IotHub Terraform script consumes to provision and then setup the VMs. + +| Variable Name | Description | Default Value| +|---------------|-------------|--------------| +| vm_name | name for the VM | test-device| +| image_publisher | publisher for the image to be used to create the VM | Canonical | +| image_offer | the name of the offering to be used from the `image_publisher` | UbuntuServer | +| image_sku | the sku for the `image_publisher` and `image_offer` to be used to create the VM | 18.04-LTS | +| image_version | the version for the `image_sku` to be used | latest | +| environment_tag | the tag to be used for creating the resources within the Azure Subscription | "du-e2etest" | +| test_setup_tarball | this variable should be the path to or actual tarball used for the setup of the device | "" | +| vm_du_tarball_script | script commands to be used for installing the device-update artifact under test as well as supporting information | "" | + +## E2E Automated Test Pipeline Automation YAML File + +The E2E Automated Test Pipeline is defined in `e2etest.yaml` file. The pipeline exercises all scenarios within the `scenarios` directory. The discussion here will focus on the three stages of the `e2etest.yaml` file that are most important: the Terraform VM provisioning stage, the test execution and results posting stage, and the Terraform teardown stage. + +### Pipeline Variables and Secrets + +Before discussing the stages of the pipeline we need to define and discuss the pipeline variables used throughout the `e2etest.yaml` file. These are defined in Azure Dev Ops instead of the yaml file but are referenced throughout the script. They usually within `$()`. These are used for holding secrets and values that the pipeline needs to access other resources but their values are not to be public. The pipeline variables can be separated into two types. The first is for the pipeline itself to complete the VM provisioning and run standard operations. The second set of variables is used by the `testingtoolkit` to enable and authenticate communication with the IotHub and ADU account for setting up and running the tests. + +#### Reference for Managing, Editing, and Viewing Pipeline Variables + +| Variable Name | Description | Where it's Used | Where it comes from | How to view it doc link | +|:-------------:|-------------|-----------------|---------------------|-------------------------| +| SUBSCRIPTION_ID | ID for the subscription for creating the VMs using Terraform | Pipeline | This value comes from the Azure Subscription being used for the testing infrastructure. | [Link](https://docs.microsoft.com/en-us/azure/azure-portal/get-subscription-tenant-id#find-your-azure-subscription)| +| SERVICE_CONNECTION_NAME |The name of the Service Connection that is used to authenticate calls to Azure for deleting the resource groups and authenticating the pipeline calls to larger systems.| Pipeline | The service connection name comes from the Azure DevOps project Service Connection and is used for connecting to other services which have given the service connection a role. | [Link](https://docs.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml#create-a-service-connection)| +| TERRAFORM_TENANT_ID |Tenant ID for the Azure Active Directory registrar that allows Terraform to access the Azure Key Vault, provision VMs, and authentication for various other services.|Pipeline| Comes from an Azure Active Directory registered application. | [Link](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app)| +| TERRAFORM_CLIENT_ID | Client ID for the Azure Active Directory registrar that allows Terraform to access the Azure Key Vault, provision VMs, and authentication for various other services.|Pipeline| Comes from an Azure Active Directory registered application. Follow the same instructions as for the tenant ID but look for the client ID instead. | [Link]()| +| TERRAFORM_CLIENT_SECRET | Client secret used in conjunction with the Tenant and Client IDs for the Azure Active Directory registrar that authenticates the user. |Pipeline| Comes from the secret blade within the same page you found the client and tenant ids on. | [Link]()| +| AZURE_KEY_VAULT_ID | The ID of the key vault where the ssh keys for the provisioned VMs are stored for remote debugging of failed scenarios.|Pipeline| Comes from the properties tab of any Azure Key Vault | [Link]()| +| ADU_ENDPOINT |The end point for the ADU Account that's being used for testing.|Testing Toolkit| This value is actually the HostName for the adu-account. You can find it on the Overview blade of any ADU Account | [Link](https://docs.microsoft.com/en-us/azure/iot-hub-device-update/device-update-resources)| +| ADU_INSTANCE_ID | The instance of ADU that's being used for testing.|Testing Toolkit| Comes from the instance name tied to the ADU account being used for testing | [Link](https://docs.microsoft.com/en-us/azure/iot-hub-device-update/create-device-update-account)| +| IOTHUB_CONNECTION_STRING |The connection string for the IotHub used to authenticate calls to the `IotHubRegistryManager`.|Testing Toolkit| Comes from the Shared access policies blade under Security Settings in your IotHub. You can also set | [Link](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-security)| +| IOTHUB_URL |Also called the HostName, this is the url for the IotHub being used for testing.|Testing Toolkit| Also called the hostname comes from the IotHub overview page. You can just copy and paste the string. | [Link](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-create-through-portal)| +| AAD_REGISTRAR_TENANT_ID |Tenant ID for the Azure Active Directory registrar that authenticates the `testingtoolkit` calls to the Azure Device Update Account.|Testing Toolkit| Comes from an Azure Active Directory registered application. | [Link](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app)| +| AAD_REGISTRAR_CLIENT_ID | Client ID for the Azure Active Directory registrar that authenticates the `testingtoolkit` calls to the Azure Device Update Account.|Testing Toolkit| Comes from an Azure Active Directory registered application. Follow the same instructions as for the tenant ID but look for the client ID instead. | [Link](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app)| +| ADU_CLIENT_SECRET | Client secret for the Azure Active Directory registrar that authenticates the `testingtoolkit` calls to the Azure Device Update Account.|Testing Toolkit| Comes from the secret blade within the same page you found the client and tenant ids on. | [Link](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app)| + +### Building Device Update for IotHub Artifacts for Testing + +The first stage in the pipeline invokes the build pipelines for all of the artifacts required for the tests. At the end of this stage all of the artifacts are published to the E2E test pipeline. At the end of a run the artifacts will always be available. In the case of a failure this allows the engineers to reproduce the error on their own VM if they like. + +### Terraform VM Provisioning Stage + +The Terraform VM Provisioning Section is a set of steps which are run according to the defined variables in the matrix. The steps for provisioning can be simplified to these steps: + +1. The first step for provisioning the VMs is to prepare the dependencies, artifact, and configuration for the Device Update for IotHub artifact being tested. + 1. First the artifact from the build template is downloaded and placed into the scenario's `testsetup` directory (created by the script underneath the scenario e.g.`scenarios/ubuntu-18.04-amd64/testsetup/`). + 2. Next `scenario/-/vm-setup/devicesetup.py` script is run. The `devicesetup.py` script uses the `testingtoolkit` to communicate with the IotHubRegistry in order to create a device or module for the artifact under test to use. The `devicesetup.py` script will always output the configuration required by the artifact in order to establish a connection. The outputs are up to the scenario writer to determine but by the end of execution the scenario should have everything it needs to setup a DeviceUpdate artifact for testing. + 3. Finally the `scenario/-/vm-setup/setup.sh` script is copied into the `testsetup/` directory. It manages installing the Device Update artifact's dependencies, installs the configuration information generated by `devicesetup.py`, and restarts the Device Update agent. This script is expected to be run on the device during the setup step for the VM. + 4. After running the `setup.sh` script the VM is expected to be able to connect to the IotHub. + 5. Once the `testsetup/` directory has been filled with the required documents it is archived and compressed into the `testsetup.tar.gz` tarball which is then passed to the Terraform VM to be used during by the Terraform scripts to setup the artifact on the Terraform provisioned VM. +2. Once the has been created the pipeline uses the `terraform apply` command to create the VM passing the tarball and the per-scenario matrix commands for running the setup.sh command on the VM is passed along with the required values to create the VMs under the pipeline run's resource group. +3. After creating each VM the Terraform state is then published to the pipeline artifacts so Terraform is able to clean up it's dependencies later on in the process. + +### Run Tests and Post Results Stage + +The next stage will target each of the previously provisioned devices running each `*.py` script within the respective scenario's directory. The output from each one of these tests is an xml file in JUnit XML format. All of the tests for each of the VMs is run before all of the results are posted to the DevTest hub from which the testers and developers can view the results. + +### Terraform Infrastructure Tear Down + +The tear down steps use `terraform destroy` command to destroy each of the matrixed scenarios described in the VMInitialization job. There's a final step run after that cleans up the resource group for all of the resources. At the end of each successful test all infrastructure created during the run of the pipeline will be destroyed. + +### Terraform VM Setup Process + +Terraform accepts multiple variable inputs for creating the VM machine each time `terraform apply` is called with any host file. These variables are what are used to configure the VM for the correct distribution and architecture as well as providing the appropriate artifact and supporting material for setting up the VM for the test. + +#### Terraform VM Initialization + +The Terraform Host template (`terraform/host/DeviceUpdateHost.tf`) to create the VMs. Variables are passed to the VM which are used by the setup script to create and configure the VMs for testing. You can examine the `e2etest.yaml` file has an example invocation including the variables. + +#### Device Update Artifact Setup On the Terraform VM + +The two most important variables for the E2E Test Automation pipeline within the Terraform scripts is the `test_setup_tarball` and `vm_du_tarball_script`. + +The `test_setup_tarball` is an archive containing the artifact to be tested, the configuration files for the artifact (e.g. a du-config.json, an IoTEdge config.toml, etc), and a `setup.sh` bash file that contains the steps to install the dependencies for the artifact, install the artifact itself, and then installing the appropriate configuration files for the Device Update for IotHub artifact. + +The `vm_du_tarball_script` is set with the commands to unpack the archive and run the `setup.sh` script. The common value for `vm_du_tarball_script` is + +```bash + tar -xvf /tmp/testsetup.tar.gz -C ./ && + chmod u=rwx,g=rwx,o=rx ./testsetup/setup.sh && + ./testsetup/setup.sh +``` + +These steps should be the same for all VMs being created by the VM but the author of the `e2etest.yaml` file can change this if it becomes necessary. + +## E2E Automated Test Pipeline Development Process + +The development process laid out here is for adding new scenarios as well as adding new tests. As a reminder a `scenario` is defined as a new distribution and architecture combination that has not been previously implemented. A test is apart of the coverage of a scenario. + +### Adding New Scenarios + +The process for adapting or adding a new scenario can be accomplished by following these steps: + +1. Create a new directory for the scenario under the directory `scenarios/`. The naming convention should follow `-` substituting the distribution name and the desired architecture to target. +2. Next under your new scenario directory create the `vm_setup` directory. + 1. Within this directory create the `setup.sh` script for installing the DeviceUpdate for IotHub agent on the VM as well as setup any and all needed dependencies (e.g. for an IotEdge device provisioned by the IoT Identity Service add the steps to install aziot-edge and configure it with the required information) + 2. The next step is create the `devicesetup.py` script under the same directory that creates the device in the IotHub and the corresponding `du-config.json` file. This can create whatever provisioning information or work that you'd like to have on the device but you'll have to modify the `TerraformVMInitialization` step to use that information provided your scenario needs more than the standard du-config.json file. + 3. For most cases the above two steps can be satisfied by just copying the `vm_setup` directory from any of the other scenarios and doing basic modifications. It should be easy to customize from there. +3. After creating the setup files for the VMs you need to add the scenario to the `TerraformVMInitialization` and `RunTestsAndPostResults` matrices. For both you need to modify the `matrix:` section to include the information for Terraform to create the VM an example of how Debian 10 might be added to the `TerraformVMInitialization` stage below. An explanation of what these are for and how they're used is provided in the sections above. + + ```yaml + debian-9-amd64: + distroName: debian-10 + image_publisher: CentOS + image_offer: Debian10 + image_sku: debian-10 + image_version: latest + packagePattern: "*debian-90-amd64*/*.deb" + package_pipeline: "Azure.adu-private-preview.e2e-test" + scenarioSetupDir: './scenarios/debian-10-amd64/' + du_tarball_script: >- + tar -xvf /tmp/testsetup.tar.gz -C ./ && + u=rwx,g=rwx,o=rx ./testsetup/setup.sh && + ./testsetup/setup.sh + ``` + + An example of the matrix for the `RunTestsAndPostResults` matrix is below. You can read more about what goes into this section above.: + + ```yaml + debian-10-amd64: + distroName: debian-10 + scenarioPath: scenarios/debian-10-amd64/ + ``` + +4. Once the scenario has been added to the `e2etest.yaml` file and the supporting scenario directory and it's constituent parts are set up it's time to start adding tests. All tests for the device should be added to it's scenario directory. There's expected patterns and names for these files. That is covered in the below section on adding tests. + +### Adding New Tests to a Scenario + +These steps assume that the scenario has already been setup under the `scenarios/` directory and that the `vm_setup` directory has already been created under that scenario directory. Each scenario is composed of a combination of tests and "meta" files for setting up and managing the device or module that's used by the scenario. + +#### Meta Files for the Automated Tests + +For all scenarios the test file names are the same, however there are some "meta" files that are used for setting the ADU Group, centralizing names for things that will be used by multiple tests (e.g. the device id and/or module id), and deleting the device that should also be added for every scenario. + +As of the last edit of this page the meta files are: + +| File Name | Description | +|---------------|-------------| +|`scenario_definitions.py` | Contains variables that are needed by multiple tests or are global to all tests to be used in the scenario (e.g. device-id/module-id, ADU Group Name, Deployment Group Name, etc) | +| `add_device_to_adu_group.py` | Takes the device id and/or module-id plus the ADU Group for the device/module defined in `scenario_definitions.py` and creates an ADU Group for that device/module with that ADU Group name | +| `delete_device.py` | Cancels or stops all deployments on the ADU Group , deletes the ADU Group, and then finally deletes the device or module for the scenario | + +#### Test File Names + +For each test there is a specific name that is hard coded into the yaml file for that test. The following is a list of the currently defined test files: + +| Test Type/ Goal | File Name | +|-----------------|-----------| +| APT Deployment | `apt_deployment.py` | +| Diagnostics Run| `diagnostics.py` | +| Multi-Component Update | `mcu_deployment.py` | +| Bundle Update | `bundle_update.py` | +| Agent Update | `agent_update.py` | + +Every scenario will have these defined in the repository. If there are no tests to be run or the test hasn't been implemented yet (say we're adding Debian 10 but we're not ready to add the full automated tests for bundle or multi-component updates) the files should be left with only the base test that allows the pipeline to run even if the file has no functionality. + +#### Structure of a Test File + +Before reading this section it's highly recommended that the reader peruse the ubuntu-18.04 test directory to see what the style looks like and what we're using to accomplish the actual testing. Additionally the reader should read the README.md for the Testing Toolkit that all of the Device Update for IotHub tests are built upon. The reader should not have to know how the toolkit specifically works but at least understand how to invoke and use the toolkit. + +The Device Update for IotHub E2E Automation Tests are written using the Python3.9 `unittest` module and the tests output is converted into JUnit format using the `xmlrunner` `xunit_plugin` module. It's a bit confusing but the actual output of the xmlrunner transform is NOT XUnit such as Azure Pipelines calls it (Azure Pipelines considers XUnit 2.0 .NET to be XUnit instead of just XUnit which is effectively the same format as JUnit). Instead the `xunit_plugin.transform` function allows us to transform from the Python `unittest.TestCase` object output to an Azure Pipelines consumable format (in this case JUnit or the old XUnit format). + +Each test file contains one Python class that inherits from `unittest.TestCase` and has one or more methods that implement some test related to it's parent caller. The APT Deployment test for Ubuntu 18.04 has been documented to help readers start writing their own tests [here](./scenarios/ubuntu-18.04-amd64/apt_deployment.py). + +For documentation on the tooling used you can read about the Python `unittest` module [here](https://docs.python.org/3/library/unittest.html), the XML Runner and XUnit plugin [here](https://github.com/xmlrunner/unittest-xml-reporting), and the Testing Toolkit itself [here](./scenarios/testingtoolkit/README.md). diff --git a/azurepipelines/e2e_test/e2etest.yaml b/azurepipelines/e2e_test/e2etest.yaml new file mode 100644 index 000000000..b4303df32 --- /dev/null +++ b/azurepipelines/e2e_test/e2etest.yaml @@ -0,0 +1,509 @@ +# E2E test pipeline - performs end-to-end tests on the following platforms +# - Ubuntu 18.04 + +resources: + containers: + - container: ubuntu + image: ubuntu:18.04 + +parameters: + - name: SKIP_TEARDOWN + displayName: "Leave test infrastructure in-place" + type: boolean + default: false + +variables: + SKIP_TEARDOWN: ${{ parameters.SKIP_TEARDOWN }} + E2E_WORKING_DIR: $(Build.SourcesDirectory)/azurepipelines/e2e_test + +name: "E2E Automated Test Run" + +stages: + - stage: PerformNativeBuilds + displayName: Builds all the native architecture builds + jobs: + - job: BuildUbuntu_1804_AMD64 + displayName: Building the Device Update Package for Ubuntu 18.04 AMD64 + continueOnError: False + pool: aduc_1es_client_pool + steps: + - template: ../build/templates/adu-docker-build-steps.yml + parameters: + targetOs: ubuntu1804 + targetArch: amd64 + - job: BuildUbuntu_1804_ARM64 + displayName: Building the Device Update Package for Ubuntu 18.04 ARM64 + continueOnError: False + pool: ubuntu1804_arm_pool + steps: + - template: ../build/templates/adu-native-build-steps.yml + parameters: + targetOs: ubuntu1804 + targetArch: arm64 + + - job: BuildUbuntu_2004_AMD64 + displayName: Building the Device Update Package for Ubuntu 20.04 AMD64 + continueOnError: False + pool: aduc_1es_client_pool + steps: + - template: ../build/templates/adu-docker-build-steps.yml + parameters: + targetOs: ubuntu2004 + targetArch: amd64 + + - job: BuildDebian_10_AMD64 + displayName: Building the Device Update Package for Debian 10 amd64 + continueOnError: False + pool: aduc_1es_client_pool + steps: + - template: ../build/templates/adu-docker-build-steps.yml + parameters: + targetOs: debian10 + targetArch: amd64 + + - stage: TerraformSetup + displayName: Initializes and installs Terraform into the VM + pool: + vmImage: "ubuntu-18.04" + jobs: + - job: TerraformInstall + displayName: "Install Terraform Into the VM and Create the Resource Group" + continueOnError: False + steps: + - script: | + sudo apt update && sudo apt install curl + curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - + sudo apt-add-repository "deb [arch=$(dpkg --print-architecture)] https://apt.releases.hashicorp.com $(lsb_release -cs) main" + sudo apt install terraform + displayName: "Installing Terraform" + - script: "terraform init" + displayName: terraform init (VM) + continueOnError: true + workingDirectory: $(E2E_WORKING_DIR)/terraform/resource_group/ + - script: 'terraform apply -var subscription_id="$(SUBSCRIPTION_ID)" -var tenant_id="$(TERRAFORM_TENANT_ID)" -var client_id="$(TERRAFORM_CLIENT_ID)" -var client_secret="$(TERRAFORM_CLIENT_SECRET)" -auto-approve' + displayName: Creating the resource group for the testing environment + continueOnError: true + workingDirectory: $(E2E_WORKING_DIR)/terraform/resource_group/ + - script: | + RESOURCE_GROUP_NAME=`terraform output du_resource_group_name` + echo "$RESOURCE_GROUP_NAME" + echo "##vso[task.setvariable variable=TERRAFORM_RESOURCE_GROUP_NAME;isOutput=true]$RESOURCE_GROUP_NAME" + name: resource_group_name_step + displayName: Retrieve Resource Group Name + continueOnError: true + workingDirectory: $(E2E_WORKING_DIR)/terraform/resource_group/ + + - stage: TerraformVMInitialization + displayName: Initializes the Terraform VMs for the Automated Tests + dependsOn: + - TerraformSetup + - PerformNativeBuilds + variables: + TERRAFORM_RESOURCE_GROUP_NAME: $[ stageDependencies.TerraformSetup.TerraformInstall.outputs['resource_group_name_step.TERRAFORM_RESOURCE_GROUP_NAME'] ] + pool: + vmImage: "ubuntu-18.04" + jobs: + - job: Terraform_VM_Setup + strategy: + matrix: + ubuntu-18.04-amd64: + distroName: ubuntu-18.04-amd64 + vm_size: Standard_DS1_v2 + image_publisher: Canonical + image_offer: UbuntuServer + image_sku: 18.04-LTS + image_version: latest + packagePattern: "*ubuntu1804-amd64*/*.deb" + package_pipeline: "Azure.adu-private-preview.e2e-test" + scenarioSetupDir: "./scenarios/ubuntu-18.04-amd64" + du_tarball_script: >- + tar -xvf /tmp/testsetup.tar.gz -C ./ && + chmod u=rwx,g=rwx,o=rx ./testsetup/setup.sh && + ./testsetup/setup.sh + + ubuntu-18.04-arm64: + distroName: ubuntu-18.04-arm64 + vm_size: Standard_D2plds_v5 + image_publisher: Canonical + image_offer: UbuntuServer + image_sku: 18_04-lts-arm64 + image_version: latest + packagePattern: "*ubuntu1804-arm64*/*.deb" + package_pipeline: "Azure.adu-private-preview.e2e-test" + scenarioSetupDir: "./scenarios/ubuntu-18.04-arm64" + du_tarball_script: >- + tar -xvf /tmp/testsetup.tar.gz -C ./ && + chmod u=rwx,g=rwx,o=rx ./testsetup/setup.sh && + ./testsetup/setup.sh + + ubuntu-20.04-amd64: + distroName: ubuntu-20.04-amd64 + vm_size: Standard_DS1_v2 + image_publisher: canonical + image_offer: 0001-com-ubuntu-server-focal + image_sku: 20_04-lts + image_version: latest + packagePattern: "*ubuntu2004-amd64*/*.deb" + package_pipeline: "Azure.adu-private-preview.e2e-test" + scenarioSetupDir: "./scenarios/ubuntu-20.04-amd64" + du_tarball_script: >- + tar -xvf /tmp/testsetup.tar.gz -C ./ && + chmod u=rwx,g=rwx,o=rx ./testsetup/setup.sh && + ./testsetup/setup.sh + + debian-10-amd64: + distroName: debian-10-amd64 + vm_size: Standard_DS1_v2 + image_publisher: debian + image_offer: debian-10 + image_sku: 10 + image_version: latest + packagePattern: "*debian10-amd64*/*.deb" + package_pipeline: "Azure.adu-private-preview.e2e-test" + scenarioSetupDir: "./scenarios/debian-10-amd64" + du_tarball_script: >- + tar -xvf /tmp/testsetup.tar.gz -C ./ && + chmod u=rwx,g=rwx,o=rx ./testsetup/setup.sh && + ./testsetup/setup.sh + + steps: + - task: DownloadPipelineArtifact@2 + displayName: "Download DeviceUpdate Package from pipeline" + inputs: + source: "current" + project: "adu-linux-client" + pipeline: $(package_pipeline) + itemPattern: "$(packagePattern)" + path: $(E2E_WORKING_DIR) + - script: | + mkdir $(scenarioSetupDir)/vm_setup/testsetup/ + displayName: "Create the test setup directory that will contain all of the values needed for the setup tarball" + continueOnError: false + workingDirectory: $(E2E_WORKING_DIR)/ + + - script: | + cp `find . -name '*.deb'` $(scenarioSetupDir)/vm_setup/testsetup/deviceupdate-package.deb + displayName: Stage DU Artifact for Preparing the VM Tarball + continueOnError: false + workingDirectory: $(E2E_WORKING_DIR)/ + + - task: UsePythonVersion@0 + displayName: Using Python version 3.10 + inputs: + versionSpec: 3.10 + + - script: | + python3 -m pip install -r scenarios/testingtoolkit/requirements.txt + workingDirectory: $(E2E_WORKING_DIR) + displayName: Installing the toolkits requirements + + - task: PythonScript@0 + displayName: "Run the script to create the device and output a configuration file" + inputs: + scriptSource: "filePath" + scriptPath: "$(E2E_WORKING_DIR)/$(scenarioSetupDir)/vm_setup/devicesetup.py" + workingDirectory: $(E2E_WORKING_DIR)/ + env: + IOTHUB_URL: $(IOTHUB_URL) + IOTHUB_CONNECTION_STRING: $(IOTHUB_CONNECTION_STRING) + ADU_ENDPOINT: $(ADU_ENDPOINT) + ADU_INSTANCE_ID: $(ADU_INSTANCE_ID) + AAD_REGISTRAR_CLIENT_ID: $(AAD_REGISTRAR_CLIENT_ID) + AAD_REGISTRAR_TENANT_ID: $(AAD_REGISTRAR_TENANT_ID) + AAD_CLIENT_SECRET: $(AAD_CLIENT_SECRET) + + - script: | + mv du-config.json $(scenarioSetupDir)/vm_setup/testsetup/ + displayName: Copy the configuration file to the same location as the artifact + continueOnError: False + workingDirectory: $(E2E_WORKING_DIR)/ + + - script: | + cp $(scenarioSetupDir)/vm_setup/setup.sh $(scenarioSetupDir)/vm_setup/testsetup/ + displayName: Copy the setup script to the same location as the artifact + continueOnError: False + workingDirectory: $(E2E_WORKING_DIR)/ + + - script: | + tar -czvf $(scenarioSetupDir)/vm_setup/testsetup/adu_srcs_repo.tar.gz -C $(Build.SourcesDirectory) ./src ./scripts ./tools + displayName: Taring the repo for access on the virtual machine + continueOnError: False + workingDirectory: $(E2E_WORKING_DIR) + + # + # At this point /testsetup/ + # du-agent.deb + # du-config.json + # setup.sh + # adu_srcs_repo.tar.gz + - script: | + tar -czvf $(scenarioSetupDir)/testsetup.tar.gz -C $(scenarioSetupDir)/vm_setup ./testsetup/ + displayName: Creating the test scenario tar ball with just the contents of ./scenarios//vm_setup/testsetup/ + continueOnError: False + workingDirectory: $(E2E_WORKING_DIR)/ + + - script: | + cp $(scenarioSetupDir)/testsetup.tar.gz terraform/host/ + workingDirectory: $(E2e_WORKING_DIR) + continueOnError: False + displayName: "Copying tarball over to the terraform host" + + - script: | + sudo apt update && sudo apt install curl + curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - + sudo apt-add-repository "deb [arch=$(dpkg --print-architecture)] https://apt.releases.hashicorp.com $(lsb_release -cs) main" + sudo apt install terraform + displayName: "Installing Terraform" + + - script: "terraform init" + displayName: terraform init (VM) + continueOnError: False + workingDirectory: $(E2E_WORKING_DIR)/terraform/host/ + + - script: terraform apply -var subscription_id="$(SUBSCRIPTION_ID)" -var tenant_id="$(TERRAFORM_TENANT_ID)" -var client_id="$(TERRAFORM_CLIENT_ID)" -var client_secret="$(TERRAFORM_CLIENT_SECRET)" -var key_vault_id="$(Azure_Key_Vault_ID)" -var resource_group_name=$(TERRAFORM_RESOURCE_GROUP_NAME) -var vm_name=$(distroName) -var vm_size=$(vm_size) -var image_offer=$(image_offer) -var image_publisher=$(image_publisher) -var image_sku=$(image_sku) -var image_version=$(image_version) -var test_setup_tarball="./testsetup.tar.gz" -var vm_du_tarball_script="$(du_tarball_script)" -auto-approve + displayName: Creating the Virtual Machines + continueOnError: False + workingDirectory: $(E2E_WORKING_DIR)/terraform/host/ + + - script: | + mkdir -p $(Build.ArtifactStagingDirectory)/terraform/host + cp -R ./terraform/host/* $(Build.ArtifactStagingDirectory)/terraform/host + displayName: Copy terraform state (vm) + continueOnError: False + workingDirectory: $(E2E_WORKING_DIR) + + - publish: "$(Build.ArtifactStagingDirectory)/terraform/host" + displayName: "Publish Terraform state for destroy stage (vm)" + continueOnError: False + artifact: terraformStateVM_$(distroName) + + - stage: RunTestsAndPostResults + displayName: Execute E2E Test + dependsOn: + - TerraformVMInitialization + jobs: + - job: E2ETestJob + strategy: + matrix: + ubuntu-18.04-amd64: + distroName: ubuntu-18.04-amd64 + scenarioPath: scenarios/ubuntu-18.04-amd64/ + ubuntu-18.04-arm64: + distroName: ubuntu-18.04-arm64 + scenarioPath: scenarios/ubuntu-18.04-arm64/ + ubuntu-20.04-amd64: + distroName: ubuntu-20.04-amd64 + scenarioPath: scenarios/ubuntu-20.04-amd64/ + debian-10-amd64: + distroName: debian-10-amd64 + scenarioPath: scenarios/debian-10-amd64/ + pool: + vmImage: "ubuntu-18.04" + steps: + - script: | + mkdir testresults + displayName: Creating the test results directory + continueOnError: False + workingDirectory: $(E2E_WORKING_DIR) + + - task: UsePythonVersion@0 + displayName: Set the python version to Python 3.10 + inputs: + versionSpec: 3.10 + + - script: | + python -m pip install -r scenarios/testingtoolkit/requirements.txt + workingDirectory: $(E2E_WORKING_DIR) + displayName: Installing the toolkit dependencies + continueOnError: False + + - task: PythonScript@0 + displayName: Run script to add the device to the ADU group + continueOnError: True + inputs: + scriptSource: "filePath" + scriptPath: "$(E2E_WORKING_DIR)/$(scenarioPath)/add_device_to_adu_group.py" + workingDirectory: $(E2E_WORKING_DIR) + env: + IOTHUB_URL: $(IOTHUB_URL) + IOTHUB_CONNECTION_STRING: $(IOTHUB_CONNECTION_STRING) + ADU_ENDPOINT: $(ADU_ENDPOINT) + ADU_INSTANCE_ID: $(ADU_INSTANCE_ID) + AAD_REGISTRAR_CLIENT_ID: $(AAD_REGISTRAR_CLIENT_ID) + AAD_REGISTRAR_TENANT_ID: $(AAD_REGISTRAR_TENANT_ID) + AAD_CLIENT_SECRET: $(AAD_CLIENT_SECRET) + + - task: PythonScript@0 + displayName: Run script to test apt-deployment + continueOnError: True + inputs: + scriptSource: "filePath" + scriptPath: "$(E2E_WORKING_DIR)/$(scenarioPath)/apt_deployment.py" + workingDirectory: $(E2E_WORKING_DIR) + env: + IOTHUB_URL: $(IOTHUB_URL) + IOTHUB_CONNECTION_STRING: $(IOTHUB_CONNECTION_STRING) + ADU_ENDPOINT: $(ADU_ENDPOINT) + ADU_INSTANCE_ID: $(ADU_INSTANCE_ID) + AAD_REGISTRAR_CLIENT_ID: $(AAD_REGISTRAR_CLIENT_ID) + AAD_REGISTRAR_TENANT_ID: $(AAD_REGISTRAR_TENANT_ID) + AAD_CLIENT_SECRET: $(AAD_CLIENT_SECRET) + + - task: PythonScript@0 + displayName: Run script to test diagnostics + continueOnError: True + inputs: + scriptSource: "filePath" + scriptPath: "$(E2E_WORKING_DIR)/$(scenarioPath)/diagnostics.py" + workingDirectory: $(E2E_WORKING_DIR) + env: + IOTHUB_URL: $(IOTHUB_URL) + IOTHUB_CONNECTION_STRING: $(IOTHUB_CONNECTION_STRING) + ADU_ENDPOINT: $(ADU_ENDPOINT) + ADU_INSTANCE_ID: $(ADU_INSTANCE_ID) + AAD_REGISTRAR_CLIENT_ID: $(AAD_REGISTRAR_CLIENT_ID) + AAD_REGISTRAR_TENANT_ID: $(AAD_REGISTRAR_TENANT_ID) + AAD_CLIENT_SECRET: $(AAD_CLIENT_SECRET) + + - task: PythonScript@0 + displayName: Run script to test mcu + continueOnError: True + inputs: + scriptSource: "filePath" + scriptPath: "$(E2E_WORKING_DIR)/$(scenarioPath)/Multi-Component-Update.py" + workingDirectory: $(E2E_WORKING_DIR) + env: + IOTHUB_URL: $(IOTHUB_URL) + IOTHUB_CONNECTION_STRING: $(IOTHUB_CONNECTION_STRING) + ADU_ENDPOINT: $(ADU_ENDPOINT) + ADU_INSTANCE_ID: $(ADU_INSTANCE_ID) + AAD_REGISTRAR_CLIENT_ID: $(AAD_REGISTRAR_CLIENT_ID) + AAD_REGISTRAR_TENANT_ID: $(AAD_REGISTRAR_TENANT_ID) + AAD_CLIENT_SECRET: $(AAD_CLIENT_SECRET) + + - task: PythonScript@0 + displayName: Run script to test bundle-update + continueOnError: True + inputs: + scriptSource: "filePath" + scriptPath: "$(E2E_WORKING_DIR)/$(scenarioPath)/Bundle-update.py" + workingDirectory: $(E2E_WORKING_DIR) + env: + IOTHUB_URL: $(IOTHUB_URL) + IOTHUB_CONNECTION_STRING: $(IOTHUB_CONNECTION_STRING) + ADU_ENDPOINT: $(ADU_ENDPOINT) + ADU_INSTANCE_ID: $(ADU_INSTANCE_ID) + AAD_REGISTRAR_CLIENT_ID: $(AAD_REGISTRAR_CLIENT_ID) + AAD_REGISTRAR_TENANT_ID: $(AAD_REGISTRAR_TENANT_ID) + AAD_CLIENT_SECRET: $(AAD_CLIENT_SECRET) + + - task: PythonScript@0 + displayName: Run script to clean up the device + continueOnError: True + inputs: + scriptSource: "filePath" + scriptPath: "$(E2E_WORKING_DIR)/$(scenarioPath)/delete_device.py" + workingDirectory: $(E2E_WORKING_DIR) + env: + IOTHUB_URL: $(IOTHUB_URL) + IOTHUB_CONNECTION_STRING: $(IOTHUB_CONNECTION_STRING) + ADU_ENDPOINT: $(ADU_ENDPOINT) + ADU_INSTANCE_ID: $(ADU_INSTANCE_ID) + AAD_REGISTRAR_CLIENT_ID: $(AAD_REGISTRAR_CLIENT_ID) + AAD_REGISTRAR_TENANT_ID: $(AAD_REGISTRAR_TENANT_ID) + AAD_CLIENT_SECRET: $(AAD_CLIENT_SECRET) + + - script: | + mkdir -p $(Build.ArtifactStagingDirectory)/testresults/ + cp -R ./testresults/* $(Build.ArtifactStagingDirectory)/testresults/ + displayName: Copy terraform state (vm) + workingDirectory: $(E2E_WORKING_DIR) + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: "$(Build.ArtifactStagingDirectory)/testresults/" + artifact: "TestResults_$(distroName)" + publishLocation: "pipeline" + + - task: PublishTestResults@2 + inputs: + testRunner: JUnit + testResultsFiles: "$(E2E_WORKING_DIR)/testresults/*.xml" + failTaskOnFailedTests: false + testRunTitle: $(distroName) + + - stage: TerraformDestroyVm + displayName: Tear down cloud resources (VMs) and destroy the resource group + dependsOn: + - TerraformSetup + - TerraformVMInitialization + - RunTestsAndPostResults + pool: + vmImage: "ubuntu-18.04" + variables: + TERRAFORM_RESOURCE_GROUP_NAME: $[ stageDependencies.TerraformSetup.TerraformInstall.outputs['resource_group_name_step.TERRAFORM_RESOURCE_GROUP_NAME'] ] + jobs: + - job: TerraformDestroy + strategy: + matrix: + ubuntu-18.04-amd64: + distroName: ubuntu-18.04-amd64 + ubuntu-18.04-arm64: + distroName: ubuntu-18.04-arm64 + ubuntu-20.04: + distroName: ubuntu-20.04-amd64 + debian-10: + distroName: debian-10-amd64 + steps: + - script: echo "$(TERRAFORM_RESOURCE_GROUP_NAME)" + displayName: Checking terraform resource group name + - script: | + sudo apt update && sudo apt install curl + curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - + sudo apt-add-repository "deb [arch=$(dpkg --print-architecture)] https://apt.releases.hashicorp.com $(lsb_release -cs) main" + sudo apt install terraform + displayName: Installing Terraform + - script: | + mkdir $(Pipeline.Workspace)/terraformStateVM_$(distroName) + displayName: Create the artifact directory + workingDirectory: $(Pipeline.Workspace) + + - task: DownloadPipelineArtifact@2 + inputs: + buildType: "current" + artifactName: "terraformStateVM_$(distroName)" + targetPath: "$(Pipeline.Workspace)/terraformStateVM_$(distroName)" + displayName: Downloading the terraform artifacts + continueOnError: false + + - script: | + terraform init + terraform destroy -var client_id="$(TERRAFORM_CLIENT_ID)" -var client_secret="$(TERRAFORM_CLIENT_SECRET)" -var subscription_id="$(SUBSCRIPTION_ID)" -var tenant_id="$(TERRAFORM_TENANT_ID)" -var key_vault_id="$(Azure_Key_Vault_ID)" -var resource_group_name=$(TERRAFORM_RESOURCE_GROUP_NAME) -auto-approve || true + workingDirectory: $(Pipeline.Workspace)/terraformStateVM_$(distroName) + displayName: Destroy cloud resources (VM) + continueOnError: true + + - stage: RG_Destroy + displayName: Destroys the Resource Group used for the End-To-End Test + dependsOn: + - TerraformSetup + - TerraformVMInitialization + - RunTestsAndPostResults + - TerraformDestroyVm + pool: + vmImage: "ubuntu-18.04" + variables: + TERRAFORM_RESOURCE_GROUP_NAME: $[ stageDependencies.TerraformSetup.TerraformInstall.outputs['resource_group_name_step.TERRAFORM_RESOURCE_GROUP_NAME'] ] + jobs: + - job: ResourceGroupDestroy + steps: + - task: AzureCLI@2 + displayName: Cleanup All Test Infrastructure + condition: eq(variables['SKIP_TEARDOWN'], 'false') + inputs: + azureSubscription: $(SERVICE_CONNECTION_NAME) + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + echo "Starting to delete the resource group" + az group delete -n $(TERRAFORM_RESOURCE_GROUP_NAME) --yes --no-wait diff --git a/azurepipelines/e2e_test/scenarios/debian-10-amd64/Bundle-update.py b/azurepipelines/e2e_test/scenarios/debian-10-amd64/Bundle-update.py new file mode 100644 index 000000000..4cab5c966 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/debian-10-amd64/Bundle-update.py @@ -0,0 +1,160 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner + +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/debian-10-amd64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix,test_bundle_update_deployment_id, test_connection_timeout_tries , retry_wait_time_in_seconds + +# +# Global Test Variables +# + +bundle_update_status_retries = 15 + +class BundleUpdateTest(unittest.TestCase): + + # + # Every test within a subclass of a unittest.TestCase to be run by unittest must have the prefix test_ + # + def test_BundleUpdate(self): + + # + # The first step to any test is to create the test helper that allows us to make calls to both the DU account and the + # IotHub. In the pipeline the parameters to create the test helper are passed to the environment from which we can read them + # here. + # + # For testing it might be convenient to simply define these above and directly instantiate the object or if you're not planning + # on running the tests as apart of a unittest.TestCase you can simply pass them via the CLI to the python program + # and create the test helper using the helper method + # DuAutomatedTestConfigurationManager.FromCliArgs() + # + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + + # + # We retrieve the apt deployment id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.deploymentId = test_bundle_update_deployment_id + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0,test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus,"Connected") + # + # For every deployment we need to create an Update Id. For all tests running in the DeviceUpdate for IotHub Test Automation + # pipeline we use Fabrikaam as a provider and Vaccuum as the name for the update. You'll see the same values supplied as the + # manufacturer and model in the devicesetup.py script. + # + self.deploymentUpdateId = UpdateId(provider="Contoso1", name="Virtual", version="3.0") + + # + # The assumption is that we've already imported the update targeting the manufacturer and + # model for the device created in the devicesetup.py. Now we have to create the deployment for + # the device using the update-id for the update + # + # Note: ALL UPDATES SHOULD BE UPLOADED TO THE TEST AUTOMATION HUB NOTHING SHOULD BE IMPORTED AT TEST TIME + # + status_code = self.duTestHelper.StartDeploymentForGroup(deploymentId=self.deploymentId,groupName=test_adu_group,updateId=self.deploymentUpdateId) + + self.assertEqual(status_code,200) + + # + # Once we've started the deployment for the group we can then query for the status of the deployment + # to see if/when the deployment finishes. We expect a timeout of the configured amount of time + # + deploymentStatus = None + + + for i in range(0,bundle_update_status_retries): + deploymentStatus = self.duTestHelper.GetDeploymentStatusForGroup(self.deploymentId,test_adu_group) + + # + # If we see all the devices have completed the deployment then we can exit early + # + if (len(deploymentStatus.subgroupStatuses) != 0): + if (deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount == 1): + break + time.sleep(retry_wait_time_in_seconds) + + # + # Should only be one device group in the deployment + # + self.assertEqual(len(deploymentStatus.subgroupStatuses),1) + + # + # Devices in the group should have succeeded + # + self.assertEqual(deploymentStatus.subgroupStatuses[0].totalDevices,deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount) + + # Sleep to give time for changes to propagate and for DU to switch it's state back to idle + time.sleep(retry_wait_time_in_seconds) + + # + # Once we've checked that we've reached the proper outcome for the + # deployment we need to check that the device itself has reported it + # is back in the idle state. + # + twin = self.duTestHelper.GetDeviceTwinForDevice(test_device_id) + + self.assertEqual(twin.properties.reported["deviceUpdate"]["agent"]["state"],0) + + # + # In case of a succeeded deployment we need to clean up the resources we created. + # mainly the deletion of the deployment + # + time.sleep(retry_wait_time_in_seconds) + + # Once stopped we can delete the deployment + self.assertEqual(self.duTestHelper.DeleteDeployment(self.deploymentId,test_adu_group),204) + + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + # + # Create the IO pipe + # + out = io.BytesIO() + + # + # Exercise the TestCase and all the tests within it. + # + unittest.main(testRunner = xmlrunner.XMLTestRunner(output=out),failfast=False,buffer=False,catchbreak=False,exit=False) + + # + # Finally transform the output unto the X/JUnit XML file format + # + with open('./testresults/' + test_result_file_prefix + '-bundle-update-deployment-test.xml','wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/debian-10-amd64/Multi-Component-Update.py b/azurepipelines/e2e_test/scenarios/debian-10-amd64/Multi-Component-Update.py new file mode 100644 index 000000000..dcd868fdb --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/debian-10-amd64/Multi-Component-Update.py @@ -0,0 +1,160 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner + +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/debian-10-amd64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, test_mcu_deployment_id, test_connection_timeout_tries, retry_wait_time_in_seconds + +# +# Global Test Variables +# + +mcu_deployment_status_retries = 15 + +class MCUDeploymentTest(unittest.TestCase): + + # + # Every test within a subclass of a unittest.TestCase to be run by unittest must have the prefix test_ + # + def test_MCUDeployment(self): + + # + # The first step to any test is to create the test helper that allows us to make calls to both the DU account and the + # IotHub. In the pipeline the parameters to create the test helper are passed to the environment from which we can read them + # here. + # + # For testing it might be convenient to simply define these above and directly instantiate the object or if you're not planning + # on running the tests as apart of a unittest.TestCase you can simply pass them via the CLI to the python program + # and create the test helper using the helper method + # DuAutomatedTestConfigurationManager.FromCliArgs() + # + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + + # + # We retrieve the apt deployment id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.deploymentId = test_mcu_deployment_id + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0,test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus,"Connected") + # + # For every deployment we need to create an Update Id. For all tests running in the DeviceUpdate for IotHub Test Automation + # pipeline we use Fabrikaam as a provider and Vaccuum as the name for the update. You'll see the same values supplied as the + # manufacturer and model in the devicesetup.py script. + # + self.deploymentUpdateId = UpdateId(provider="Contoso1", name="Virtual", version="6.0") + + # + # The assumption is that we've already imported the update targeting the manufacturer and + # model for the device created in the devicesetup.py. Now we have to create the deployment for + # the device using the update-id for the update + # + # Note: ALL UPDATES SHOULD BE UPLOADED TO THE TEST AUTOMATION HUB NOTHING SHOULD BE IMPORTED AT TEST TIME + # + status_code = self.duTestHelper.StartDeploymentForGroup(deploymentId=self.deploymentId,groupName=test_adu_group,updateId=self.deploymentUpdateId) + + self.assertEqual(status_code,200) + + # + # Once we've started the deployment for the group we can then query for the status of the deployment + # to see if/when the deployment finishes. We expect a timeout of the configured amount of time + # + deploymentStatus = None + + + for i in range(0,mcu_deployment_status_retries): + deploymentStatus = self.duTestHelper.GetDeploymentStatusForGroup(self.deploymentId,test_adu_group) + + # + # If we see all the devices have completed the deployment then we can exit early + # + if (len(deploymentStatus.subgroupStatuses) != 0): + if (deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount == 1): + break + time.sleep(retry_wait_time_in_seconds) + + + # + # Should only be one device group in the deployment + # + self.assertEqual(len(deploymentStatus.subgroupStatuses),1) + + # + # Devices in the group should have succeeded + # + self.assertEqual(deploymentStatus.subgroupStatuses[0].totalDevices,deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount) + + # Sleep to give time for changes to propagate and for DU to switch it's state back to idle + time.sleep(retry_wait_time_in_seconds) + + # + # Once we've checked that we've reached the proper outcome for the + # deployment we need to check that the device itself has reported it + # is back in the idle state. + # + twin = self.duTestHelper.GetDeviceTwinForDevice(test_device_id) + + self.assertEqual(twin.properties.reported["deviceUpdate"]["agent"]["state"],0) + + # + # In case of a succeeded deployment we need to clean up the resources we created. + # mainly the deletion of the deployment + # + time.sleep(retry_wait_time_in_seconds) + + # Once stopped we can delete the deployment + self.assertEqual(self.duTestHelper.DeleteDeployment(self.deploymentId,test_adu_group),204) + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + # + # Create the IO pipe + # + out = io.BytesIO() + + # + # Exercise the TestCase and all the tests within it. + # + unittest.main(testRunner = xmlrunner.XMLTestRunner(output=out),failfast=False,buffer=False,catchbreak=False,exit=False) + + # + # Finally transform the output unto the X/JUnit XML file format + # + with open('./testresults/' + test_result_file_prefix + '-mcu-deployment-test.xml','wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/debian-10-amd64/add_device_to_adu_group.py b/azurepipelines/e2e_test/scenarios/debian-10-amd64/add_device_to_adu_group.py new file mode 100644 index 000000000..a9a482659 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/debian-10-amd64/add_device_to_adu_group.py @@ -0,0 +1,60 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner +from xmlrunner.extra.xunit_plugin import transform +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/debian-10-amd64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, test_connection_timeout_tries, retry_wait_time_in_seconds + +class AddDeviceToGroupTest(unittest.TestCase): + + def test_AddDeviceToGroup(self): + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0,test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus, "Connected") + + # + # Before we can run the deployment we need to add the ADUGroup test_adu_group tag to the + # the device before we make the ADUGroup which we can then use to target the deployment + # to the device. + # + success = self.duTestHelper.AddDeviceToGroup(test_device_id, test_adu_group) + + self.assertTrue(success) + time.sleep(retry_wait_time_in_seconds) + + +if (__name__ == "__main__"): + out = io.BytesIO() + + unittest.main(testRunner = xmlrunner.XMLTestRunner(output=out), failfast=False, buffer=False, catchbreak=False, exit=False) + + with open('./testresults/' + test_result_file_prefix + 'add-device-to-adu-group-test.xml', 'wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/debian-10-amd64/apt_deployment.py b/azurepipelines/e2e_test/scenarios/debian-10-amd64/apt_deployment.py new file mode 100644 index 000000000..f28b1fcaa --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/debian-10-amd64/apt_deployment.py @@ -0,0 +1,158 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner + +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/debian-10-amd64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, test_apt_deployment_id, test_connection_timeout_tries, retry_wait_time_in_seconds + +# +# Global Test Variables +# + +apt_deployment_status_retries = 15 + +class AptDeploymentTest(unittest.TestCase): + + # + # Every test within a subclass of a unittest.TestCase to be run by unittest must have the prefix test_ + # + def test_AptDeployment(self): + + # + # The first step to any test is to create the test helper that allows us to make calls to both the DU account and the + # IotHub. In the pipeline the parameters to create the test helper are passed to the environment from which we can read them + # here. + # + # For testing it might be convenient to simply define these above and directly instantiate the object or if you're not planning + # on running the tests as apart of a unittest.TestCase you can simply pass them via the CLI to the python program + # and create the test helper using the helper method + # DuAutomatedTestConfigurationManager.FromCliArgs() + # + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + + # + # We retrieve the apt deployment id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.deploymentId = test_apt_deployment_id + + self.deploymentUpdateId = UpdateId(provider="Contoso1", name="Virtual", version="1.0.2") + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0,test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus, "Connected") + + # + # The assumption is that we've already imported the update targeting the manufacturer and + # model for the device created in the devicesetup.py. Now we have to create the deployment for + # the device using the update-id for the update + # + # Note: ALL UPDATES SHOULD BE UPLOADED TO THE TEST AUTOMATION HUB NOTHING SHOULD BE IMPORTED AT TEST TIME + # + status_code = self.duTestHelper.StartDeploymentForGroup(deploymentId=self.deploymentId,groupName=test_adu_group,updateId=self.deploymentUpdateId) + + self.assertEqual(status_code, 200) + + # + # Once we've started the deployment for the group we can then query for the status of the deployment + # to see if/when the deployment finishes. We expect a timeout of the configured amount of time + # + deploymentStatus = None + + + for i in range(0,apt_deployment_status_retries): + deploymentStatus = self.duTestHelper.GetDeploymentStatusForGroup(self.deploymentId,test_adu_group) + + # + # If we see all the devices have completed the deployment then we can exit early + # + if (len(deploymentStatus.subgroupStatuses) != 0): + if (deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount == 1): + break + time.sleep(retry_wait_time_in_seconds) + + # + # Should only be one device group in the deployment + # + self.assertEqual(len(deploymentStatus.subgroupStatuses),1) + + # + # Devices in the group should have succeeded + # + self.assertEqual(deploymentStatus.subgroupStatuses[0].totalDevices,deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount) + + # Sleep to give time for changes to propagate and for DU to switch it's state back to idle + time.sleep(retry_wait_time_in_seconds) + + # + # Once we've checked that we've reached the proper outcome for the + # deployment we need to check that the device itself has reported it + # is back in the idle state. + # + twin = self.duTestHelper.GetDeviceTwinForDevice(test_device_id) + + self.assertEqual(twin.properties.reported["deviceUpdate"]["agent"]["state"],0) + + # + # In case of a succeeded deployment we need to clean up the resources we created. + # mainly the deletion of the deployment + # + time.sleep(retry_wait_time_in_seconds) + + deviceClassId = deploymentStatus.subgroupStatuses[0].deviceClassId + + # Once stopped we can delete the deployment + self.assertEqual(self.duTestHelper.DeleteDeployment(self.deploymentId, test_adu_group), 204) + + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + # + # Create the IO pipe + # + out = io.BytesIO() + + # + # Exercise the TestCase and all the tests within it. + # + unittest.main(testRunner = xmlrunner.XMLTestRunner(output=out),failfast=False,buffer=False,catchbreak=False,exit=False) + + # + # Finally transform the output unto the X/JUnit XML file format + # + with open('./testresults/' + test_result_file_prefix + '-apt-deployment-test.xml','wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/debian-10-amd64/delete_device.py b/azurepipelines/e2e_test/scenarios/debian-10-amd64/delete_device.py new file mode 100644 index 000000000..0738a1f5d --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/debian-10-amd64/delete_device.py @@ -0,0 +1,45 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/debian-10-amd64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, retry_wait_time_in_seconds + +class DeleteDeviceAndGroup(unittest.TestCase): + + def test_DeleteTestDeviceAndGroup(self): + configManager = DuAutomatedTestConfigurationManager.FromOSEnvironment() + + duTestHelper = configManager.CreateDeviceUpdateTestHelper() + + self.assertTrue(duTestHelper.DeleteDevice(test_device_id)) + + time.sleep(retry_wait_time_in_seconds*5) + + self.assertEqual(duTestHelper.DeleteADUGroup(test_adu_group), 204) + + + +if (__name__ == "__main__"): + out = io.BytesIO() + + unittest.main(testRunner=xmlrunner.XMLTestRunner(output=out), + failfast=False, buffer=False, catchbreak=False, exit=False) + + with open('./testresults/' + test_result_file_prefix + '-delete-device-test.xml', 'wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/debian-10-amd64/diagnostics.py b/azurepipelines/e2e_test/scenarios/debian-10-amd64/diagnostics.py new file mode 100644 index 000000000..4a5d75fa4 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/debian-10-amd64/diagnostics.py @@ -0,0 +1,95 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DiagnosticLogCollectionStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/debian-10-amd64/') +from scenario_definitions import test_device_id, test_result_file_prefix, test_operation_id + +diagnostics_operation_status_retries = 15 +class DiagnosticsTest(unittest.TestCase): + def test_diagnostics(self): + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + # + # We retrieve the test operation id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.operationId = test_operation_id + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0, 5): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(30) + + self.assertEqual(connectionStatus, "Connected") + + # + # The assumption is that we've already configured diagnostics in test account and already run a apt deployment + # Start a diagnostics log collection operation + # 201 indicate the device diagnostics log collection operation is created + # + diagnosticResponseStatus_code = self.duTestHelper.RunDiagnosticsOnDeviceOrModule( + test_device_id, operationId=self.operationId, description="") + + self.assertEqual(diagnosticResponseStatus_code, 201) + + # + # Get device diagnostics log collection operatio status and device status + # device status represent Log upload status, operatin status represent background operation status + # + diagnosticJsonStatus = DiagnosticLogCollectionStatusResponse() + for i in range(0, diagnostics_operation_status_retries): + time.sleep(5) + diagnosticJsonStatus = self.duTestHelper.GetDiagnosticsLogCollectionStatus(self.operationId) + + if (diagnosticJsonStatus.status == "Succeeded"): + break + + self.assertEqual(diagnosticJsonStatus.status, "Succeeded") + + self.assertEqual(len(diagnosticJsonStatus.deviceStatus),1) + self.assertEqual(diagnosticJsonStatus.deviceStatus[0].status, "Succeeded") + self.assertEqual(diagnosticJsonStatus.deviceStatus[0].resultCode, "200") + + + + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + out = io.BytesIO() + + unittest.main(testRunner=xmlrunner.XMLTestRunner(output=out), + failfast=False, buffer=False, catchbreak=False, exit=False) + + with open('./testresults/' + test_result_file_prefix + '-diagnostic-test.xml', 'wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/debian-10-amd64/scenario_definitions.py b/azurepipelines/e2e_test/scenarios/debian-10-amd64/scenario_definitions.py new file mode 100644 index 000000000..92768a07c --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/debian-10-amd64/scenario_definitions.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +# +# These are the definitions used by the scenario to complete it's operations. It is used by all the TestCases held within +# the project. +# +import sys +import uuid + +sys.path.append('./') +from testingtoolkit import UpdateId + +# +# Base Device Definitions +# +test_device_id = "debian10-amd64-deployment-test-device" + +test_adu_group = "Debian10AMD64TestGroup" + +test_apt_deployment_id = str(uuid.uuid4()) + +test_operation_id = str(uuid.uuid4()).replace('-', '') + +test_mcu_deployment_id = str(uuid.uuid4()) + +test_bundle_update_deployment_id = str(uuid.uuid4()) + +test_result_file_prefix = 'debian10-amd64' + +test_connection_timeout_tries = 10 + +retry_wait_time_in_seconds = 60 # For all retries this is the total amount of time we wait for all operations + +# +# Other variables that should be available to all tests in this scenario may be added here +# diff --git a/azurepipelines/e2e_test/scenarios/debian-10-amd64/vm_setup/devicesetup.py b/azurepipelines/e2e_test/scenarios/debian-10-amd64/vm_setup/devicesetup.py new file mode 100644 index 000000000..ae1dbbcc8 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/debian-10-amd64/vm_setup/devicesetup.py @@ -0,0 +1,78 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import json +import sys + +# Note: the intention is that this script is called like: +# python .//vm-setup/devicesetup.py +sys.path.append('./scenarios/') +sys.path.append('./scenarios/debian-10-amd64/') +from scenario_definitions import test_device_id +from testingtoolkit import DeviceUpdateTestHelper, DuAutomatedTestConfigurationManager + +def main(): + duTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + # + # Instantiate the Wrapper Class from the config manager + # + duTestWrapper = duTestConfig.CreateDeviceUpdateTestHelper() + + # + # Delete the device if it exists, we don't care if we fail here. + # + duTestWrapper.DeleteDevice(test_device_id) + # + # Create the Device + # + connectionString = duTestWrapper.CreateDevice(test_device_id,isIotEdge=True) + + if (len(connectionString) == 0): + print_error("Failed to create the device in the IotHub") + exit(1) + + # + # Create the du-config.json JSON Object + # + duConfigJson = { + "schemaVersion": "1.1", + "aduShellTrustedUsers": [ + "adu", + "do" + ], + "iotHubProtocol": "mqtt", + "manufacturer": "contoso", + "model": "virtual-vacuum-v2", + "agents": [ + { + "name": "main", + "runas": "adu", + "connectionSource": { + "connectionType": "string", + "connectionData": connectionString + }, + "manufacturer": "contoso", + "model": "virtual-vacuum-v2" + } + ] + } + # + # Write the configuration out to disk so we can install it as a part of the test + # + + with open('du-config.json','w') as jsonFile: + configJson = json.dumps(duConfigJson) + jsonFile.write(configJson) + +# +# Output is now a newly created device and a du-config.json file that will work with it +# + + +if __name__ == '__main__': + main() + +def print_error(msg): + print(msg, file=sys.stderr) diff --git a/azurepipelines/e2e_test/scenarios/debian-10-amd64/vm_setup/setup.sh b/azurepipelines/e2e_test/scenarios/debian-10-amd64/vm_setup/setup.sh new file mode 100644 index 000000000..5d13e30ae --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/debian-10-amd64/vm_setup/setup.sh @@ -0,0 +1,112 @@ +#!/bin/bash +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +# Setup Script for Test: debian-10-amd64-APT-deployment +# +# Should be run on the Virtual Machine being provisioned for the work. + +# This file should be included in an archive that is created at provisioning time +# with the following structure: +# setup.sh (this file) +# deviceupdate-package.deb (debian package under test) +# +# where the archive is named: +# testsetup.tar.gz +# The archive is manually mounted to the file system at: +# ~/testsetup.tar.gz +# Once the script has been extracted we'll be running from +# ~ while the script itself is at +# ~/testsetup/setup.sh +# So we need to localize the path to that. +# +# +# Install the Microsoft APT repository +# + +wget https://packages.microsoft.com/config/debian/10/packages-microsoft-prod.deb -O packages-microsoft-prod.deb +sudo dpkg -i packages-microsoft-prod.deb +rm packages-microsoft-prod.deb + +# +# Update the apt repositories +# +sudo apt-get update + + +sudo apt-cache policy deliveryoptimization-agent + + +sudo apt-cache policy deliveryoptimization-plugin-apt + + +sudo apt-cache policy libdeliveryoptimization + +# +# Install Device Update Dependencies from Release Repo +# +# Note: If there are other dependencies tht need to be installed via APT or other means they should +# be added here. You might be installing iotedge, another package to setup for a deployment test, or +# anything else. + +# Handle installing DO from latest build instead of packages.microsoft.com +wget https://github.com/microsoft/do-client/releases/download/v1.0.0/debian10_x64-packages.tar -O debian10_x64-packages.tar +tar -xvf debian10_x64-packages.tar +sudo apt-get install -y ./deliveryoptimization-agent_1.0.0_amd64.deb ./deliveryoptimization-plugin-apt_0.5.1_amd64.deb ./libdeliveryoptimization_1.0.0_amd64.deb + +# +# Install the Device Update Artifact Under Test +# +sudo apt-get install -y ./testsetup/deviceupdate-package.deb + +echo 'Checking package versions' + + +sudo apt-cache policy deliveryoptimization-agent + + +sudo apt-cache policy deliveryoptimization-plugin-apt + + +sudo apt-cache policy libdeliveryoptimization + +# +# Install the du-config.json so the device can be provisioned +# +# Note: In other setup scripts there may be more info that needs to +# go here. For instance you might have the config.toml for IotEdge, +# another kind of diagnostics file, or other kinds of data +# this is the area where such things can be added +sudo cp ./testsetup/du-config.json /etc/adu/du-config.json + + +mkdir ~/adu_srcs/ + +tar -xvf ./testsetup/adu_srcs_repo.tar.gz -C ~/adu_srcs/ + +sudo mkdir -p ~/demo/demo-devices/contoso-devices + +cd ~/adu_srcs/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo || exit + +chmod 755 ./tools/reset-demo-components.sh + +#copy components-inventory.json and adds it to the /usr/local/contoso-devices folder +sudo cp -a ./demo-devices/contoso-devices/. ~/demo/demo-devices/contoso-devices/ + +sh ./tools/reset-demo-components.sh + +#registers the extension +sudo /usr/bin/AducIotAgent -l 2 --extension-type componentEnumerator --register-extension /var/lib/adu/extensions/sources/libcontoso_component_enumerator.so + +sh ./tools/reset-demo-components.sh + + +# +# Restart the deviceupdate-agent.service +# +# Note: We expect that everything should be setup for the deviceupdate agent at this point. Once +# we restart the agent we expect it to be able to boot back up and connect to the IotHub. Otherwise +# this test will be considered a failure. +sudo systemctl restart deviceupdate-agent.service diff --git a/azurepipelines/e2e_test/scenarios/testingtoolkit/README.md b/azurepipelines/e2e_test/scenarios/testingtoolkit/README.md new file mode 100644 index 000000000..b6aa73356 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/testingtoolkit/README.md @@ -0,0 +1,122 @@ +# ADU Testing Toolkit For Python + +## Introduction + +The goal of this project is to write testing helpers that ease the creation of tests for the DU Client. + +These are for testing. Not for running high level operations. If you're looking for easy to use Python extensions for Device Update for IotHub please look [here](https://github.com/Azure/azure-sdk-for-python). The Testing Toolkit was written in Python 3.9. It is expected that any tests making use of the testing toolkit also uses that version. + +## Dependencies + +### Language Support + +This toolkit was explicitly written for Python 3.9. Other versions have not been tested. + +### Azure SDK For Python azure-iot-deviceupdate + +This toolkit is expected to be used with the DeviceUpdate package for Python from the Azure SDK for Python. While the sdk does not directly implement the configurations held here it does provide the user with a solid basis for running requests against a DeviceUpdate Account. After setting up the DeviceUpdateClient, the toolkit then builds HTTPRequests for each of the supported operations. + +You can see an example of the toolkit being used [here](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/deviceupdate/azure-iot-deviceupdate/samples/sample_hello_world.py) + +You can install just the azure-iot-deviceupdate module with + +```shell + pip install azure-iot-deviceupdate +``` + +You can also clone the azure-sdk-for-python repository and look at the module source files if you have questions. The repository is extremely verbose. It's a very useful reference. + +### Azure IoT SDK for Python azure.iot.hub + +The toolkit also makes use of the azure.iot.hub Python module for running requests against the IotHub. This package comes from the Azure IoT SDK Python found [here](https://github.com/Azure/azure-iot-sdk-python). If you're examining the contents, wondering about return types, or objects it's highly recommended you clone the repository and look at the underlying layers. + +For the Python Module you can install it with + +```shell + pip install azure.iot.hub +``` + +### Azure Identity + +In order to make a connection with the IotHub or Adu Account you need to have an associated identity. The toolkit is built to run using an (Azure Active Directory Application Registrar)[https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app]. Using the AAD Registrar and the azure.identity package we can create a connection to the Adu Account for sending messages/data/commands to the DU REST API. + +The module can be installed using: + +```shell + pip install azure-identity +``` + +Then in your script if you need to load the dependency you can use something like the following snippet to load the AAD Registrar's data into a credential that can be used for authenticating calls: + +```python +from azure.identity import ClientSecretCredential + +credential=ClientSecretCredential(tenant_id=_aadRegistrarTenantId, client_id=_aadRegistrarClientId, client_secret=_aadRegistrarClientSecret) +``` + +### How to Install Dependencies + +The dependencies can be installed by running the following: + +```shell + pip install azure-iot-deviceupdate azure.iot.hub azure-identity +``` + +or using the requirements.txt file: + +```shell + python3 -m pip install -r +``` + +## Requirements for instantiating a Device Update client in Python + +Note this is a developers note and NOT something required to be completed by the user. All the user needs to know is how to get those values and pump them through to the testing toolkit. + +1. A Token Credential: retrieved from the Azure Identity using the code above +2. The DU endpoint: the endoint for the DeviceUpdate account +3. The DU Instance Id: the instance identifier for the du instance you're trying to connect to. + +## Requirements for instantiating an IotHubRegistryManager client in Python + +1. An IotHub URL: The URL for the iothub you're trying to connect to +2. A Token Credential: retrieved from Azure IDentity using the code above. + +## Supported Operations + +1. Create Devices or Modules using a subscription or account-id +2. Retrieve a Module or Device twin +3. Delete devices or modules +4. Add an IotHub group to a device or module +5. Create an ADU Group from an IotHub Group +6. Delete an ADU Group +7. Query for the connection status of a device +8. Create a deployment +9. Start a deployment +10. Query for the status and metadata surrounding a deployment +11. Delete a deployment +12. Run a diagnostics workflow + +## What is generally needed to send an HTTP Request? + +There are three values that will always be required : + +1. The Account endpoint (Account refers to DU Account we should be able to get from above... maybe.) +2. The Instance-Id (the accounts instance identifier) + +## What is needed to Authenticate The IotHubRegistryManager? + +This section is an expansion of the one above describing how and what we need to load so we can get the IotHubRegistryManager online. + +The IotHub Registry manager is able to connect to an IotHub's device and module registration process for creating, managing, and retrieving portions of the device and module twin. To authenticate the connection to the IotHubRegistry manager we need to use the [Azure Identity Python Package](https://pypi.org/project/azure-identity/). The package can be installed using: + +```bash + pip install azure-identity +``` + +For the actual setup we expect all of these scripts to be running in the Pipelines within the context of an Azure CLI. The IotHubs we're using SHOULD (not tested still working on that) just require us to use the DefaultCredential values to get access to the DU account. + +You can see an [example](https://github.com/Azure/azure-iot-sdk-python/blob/main/azure-iot-hub/samples/iothub_registry_manager_token_credential_sample.py) for the type of authentication we're looking at. + +## Using the Toolkit in Tests + +For using the toolkit it's recommended to use the `DuAutomatedTestConfigurationManager` class to create the `DeviceUpdateTestHelper` class. It manages all of the variables that need to be passed to it in order to make a connection. You can either instantiate the class using command line arguments (implemented for testing) or use the OS Environment variables like the pipelines do. The choice is up to you but any test that is added to the pipeline will be required to use the OS environment variables. diff --git a/azurepipelines/e2e_test/scenarios/testingtoolkit/__init__.py b/azurepipelines/e2e_test/scenarios/testingtoolkit/__init__.py new file mode 100644 index 000000000..99ea39752 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/testingtoolkit/__init__.py @@ -0,0 +1,21 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. +# -------------------------------------------------------------------------- + +from ._adu_test_toolkit import DeviceUpdateTestHelper,DeploymentStatusResponse,UpdateId, DuAutomatedTestConfigurationManager,DiagnosticLogCollectionStatusResponse,DiagnosticsDeviceStatus,DeploymentSubGroupStatus +from ._version import VERSION + +__version__ = VERSION + + + +# I don't know what this is but we're keeping it for posterity in case I need it later +# try: +# from ._patch import patch_sdk # type: ignore +# patch_sdk() +# except ImportError: +# pass diff --git a/azurepipelines/e2e_test/scenarios/testingtoolkit/_adu_test_toolkit.py b/azurepipelines/e2e_test/scenarios/testingtoolkit/_adu_test_toolkit.py new file mode 100644 index 000000000..300e81e10 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/testingtoolkit/_adu_test_toolkit.py @@ -0,0 +1,571 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from azure.identity import ClientSecretCredential +from azure.iot.deviceupdate import DeviceUpdateClient +from azure.iot.hub import IoTHubRegistryManager +from azure.core.rest import HttpRequest, HttpResponse +from azure.iot.hub.protocol.models import Device +from azure.iot.hub.protocol.models import Module +from azure.iot.hub.protocol.models import Twin +from urllib.parse import urljoin +import base64 +import datetime +import getopt +import json +from msrest.exceptions import HttpOperationError +import os +import sys +import uuid + + +class DuAutomatedTestConfigurationManager(): + def __init__(self, aduEndpoint="", aduInstanceId="", iotHubUrl="", iotHubConnectionString="", aadRegistrarClientId="", aadRegistrarTenantId="", pathToCertificate="", passwordForCertificate="") -> None: + """ + Convenience wrapper for the configuration details required for DU automated tests to run. + """ + self._aduEndpoint = "" + self._aduInstanceId = "" + self._iotHubUrl = "" + self._iotHubConnectionString = "" + self._aadRegistrarClientId = "" + self._aadRegistrarTenantId = "" + self._pathToCertificate = "" + self._passwordForCertificate = "" + self._configured = False + + @classmethod + def FromCliArgs(cls): + + newCls = cls() + newCls.ParseFromCLI() + return newCls + + @classmethod + def FromOSEnvironment(cls): + newCls = cls() + newCls.ParseFromOsEnviron() + return newCls + + def ParseFromOsEnviron(self): + self._aduEndpoint = os.environ['ADU_ENDPOINT'] + self._aduInstanceId = os.environ['ADU_INSTANCE_ID'] + self._iotHubUrl = os.environ['IOTHUB_URL'] + self._iotHubConnectionString = os.environ['IOTHUB_CONNECTION_STRING'] + self._aadRegistrarClientId = os.environ['AAD_REGISTRAR_CLIENT_ID'] + self._aadRegistrarTenantId = os.environ['AAD_REGISTRAR_TENANT_ID'] + self._aadRegistrarClientSecret = os.environ['AAD_CLIENT_SECRET'] + + self._configured = True + + def ParseFromCLI(self): + # + # Get opt will use the following for arguments. + # (-a)--adu-endpoint + # (-i)--adu-instance-id + # (-u)--iothub-url + # (-c)--iothub-connection-string + # (-r)--aad-registrar-client-id + # (-t)--aad-registrar-tenant-id + # (-p)--aad-registrar-client-secret + shortopts = "a:i:u:c:r:t:p:" + longopts = ['adu-endpoint=', 'adu-instance-id=', 'iothub-url=', 'iothub-connection-string=', + 'aad-registrar-client-id=', 'aad-registrar-tenant-id=', 'aad-registrar-client-secret=', ] + optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) + + # + # Require that all parameters have been passed + # + + if (len(optlist) < 7): + return None + + for opt, val in optlist: + if (opt == "-a" or opt == "--" + longopts[0]): + self._aduEndpoint = val + elif (opt == "-i" or opt == "--" + longopts[1]): + self._aduInstanceId = val + elif (opt == "-u" or opt == "--" + longopts[2]): + self._iotHubUrl = val + elif (opt == "-c" or opt == "--" + longopts[3]): + self._iotHubConnectionString = val + elif (opt == "-r" or opt == "--" + longopts[4]): + self._aadRegistrarClientId = val + elif (opt == "-t" or opt == "--" + longopts[5]): + self._aadRegistrarTenantId = val + elif (opt == "-p" or opt == "--" + longopts[6]): + self._aadRegistrarClientSecret = val + + self._configured = True + + def CreateClientSecretCredential(self): + + if (not self._configured): + print("Calling CreateClientSecretCredential without configuring first") + return None + + return ClientSecretCredential(tenant_id=self._aadRegistrarTenantId, client_id=self._aadRegistrarClientId, client_secret=self._aadRegistrarClientSecret) + + def CreateDeviceUpdateTestHelper(self, credential=None): + + if (not self._configured): + print("Calling CreateDeviceUpdateTestHelper without configuring first") + return None + self.credential = credential + if (self.credential == None): + self.credential = self.CreateClientSecretCredential() + + return DeviceUpdateTestHelper(self._aduInstanceId, self._iotHubUrl, self._iotHubConnectionString, self.credential, self._aduEndpoint) + + +class DeploymentSubGroupStatus(): + def __init__(self, subGroupJson) -> None: + self.groupId = subGroupJson["groupId"] + self.deviceClassId = subGroupJson["deviceClassId"] + self.deploymentState = subGroupJson["deploymentState"] + self.totalDevices = subGroupJson["totalDevices"] + self.devicesInProgressCount = subGroupJson["devicesInProgressCount"] + self.devicesCompletedFailedCount = subGroupJson["devicesCompletedFailedCount"] + self.devicesCompletedSucceededCount = subGroupJson["devicesCompletedSucceededCount"] + self.devicesCanceledCount = subGroupJson["devicesCanceledCount"] + + if ("error" in subGroupJson): + self.error = subGroupJson["error"] + + +class DeploymentStatusResponse(): + def __init__(self, deploymentStatusJson) -> None: + """ + Convenience wrapper object for the deployment status response. Converts the JSON returned by + the service request into a Python object that makes it easier to access. + + You can see the types and potential values of each of the variables here: https://docs.microsoft.com/en-us/rest/api/deviceupdate/2021-06-01-preview/device-management/get-deployment-status + """ + self.deploymentState = "" + self.ParseResponseJson(deploymentStatusJson) + + def ParseResponseJson(self, deploymentStatusJson): + self.deploymentState = deploymentStatusJson["deploymentState"] + self.groupId = deploymentStatusJson["groupId"] + + subgroupStatus = deploymentStatusJson["subgroupStatus"] + + self.subgroupStatuses = [] + for status in subgroupStatus: + self.subgroupStatuses.append(DeploymentSubGroupStatus(status)) + + +class DiagnosticsDeviceStatus: + def __init__(self,deviceStatusJson): + self.deviceId = deviceStatusJson["deviceId"] + self.moduleId = "" + + if ("moduleId" in deviceStatusJson): + self.moduleId = deviceStatusJson["moduleId"] + + self.status = deviceStatusJson["status"] + + if ("resultCode" in deviceStatusJson): + self.resultCode = deviceStatusJson["resultCode"] + else: + print ("No result code") + if ("extendedResultCode" in deviceStatusJson): + self.extendedResultCode = deviceStatusJson["extendedResultCode"] + else: + print("No extended result code") + self.logLocation = deviceStatusJson["logLocation"] + +class DiagnosticLogCollectionStatusResponse(): + def __init__(self): + """ + Convenience wrapper object for the diagnostic log collection status response. + Converts the JSON returned by the service request into a Python object that makes it easier to access. + You can see the types and potential values of each of the variables here: https://docs.microsoft.com/en-us/rest/api/deviceupdate/2021-06-01-preview/device-management/get-deployment-status + """ + super().__init__() + self.operationId = "" + self.status = "" + self.createdDateTime = "" + self.lastActionDateTime = "" + self.deviceStatus = [] + + def ParseResponseJson(self, logCollectionStatusResponseJson): + self.operationId = logCollectionStatusResponseJson["operationId"] + self.createdDateTime = logCollectionStatusResponseJson["createdDateTime"] + self.lastActionDateTime = logCollectionStatusResponseJson["lastActionDateTime"] + self.status = logCollectionStatusResponseJson["status"] + + deviceStatuses = logCollectionStatusResponseJson["deviceStatus"] + + for status in deviceStatuses: + deviceStatus = DiagnosticsDeviceStatus(status) + self.deviceStatus.append(deviceStatus) + + return self + +class UpdateId(): + def __init__(self, provider, name, version) -> None: + """ + Convenience wrapper for update-ids being used for deployments following the standard methods. + """ + self.provider = provider + self.name = name + self.version = version + + def __str__(self) -> str: + return '{ "provider":"' + str(self.provider) + '", "name": "' + str(self.name) + '", "version": "' + str(self.version) + '"}' + +class DeviceUpdateTestHelper: + def __init__(self, aduInstanceId, iothubUrl, iothub_connection_string, adu_credential, endpoint="") -> None: + """ + Test Helper for the Device Update Test Automation work. The object wraps different parts of the required functions + and works with the azure.iot.deviceupdate.DeviceUpdateClient and azure.iot.hub.IotHubRegistryManager objects to + complete all of these operations + + :param str aduInstanceId: The InstanceId for the DeviceUpdate Account to be connected to + :param str credential: The Azure token credential object retrieved from azure-identity + :param str endpoint: The endpoint for the Device Update instance + :ivar str _aduInstanceId: The InstanceId for the DeviceUpdate Account to be connected to + :ivar str _aduEndpoint: The endpoint for the Device Update instance + :ivar AzureCredential _credential: The Azure token credential object retrieved from azure-identity + :ivar str _iotHubUrl: The url for the iothub to be used for testing + :ivar DeviceUpdateClient _aduAcnt: The Device Update Client account object instantiated using the parameters used for DeviceUpdate operations + :ivar IotHubRegistryManager _hubRegistryManager: The IotHubRegistryManager object instantiated using the parameters used for IotHub operations + """ + + # Internal Values for managing the connection to DU and the IotHub + self._aduInstanceId = aduInstanceId + self._aduEndpoint = endpoint + self._iothub_connection_string = iothub_connection_string + self._aduCredential = adu_credential + self._iotHubUrl = iothubUrl + self._aduAcnt = DeviceUpdateClient(endpoint, aduInstanceId, adu_credential) + + self._hubRegistryManager = IoTHubRegistryManager.from_connection_string(iothub_connection_string) + + self._base_url = f'https://{self._aduEndpoint}/deviceUpdate/{self._aduInstanceId}/' + self._iotHubApiVersion = "?api-version=2021-06-01-preview" + self._aduApiVersion= "?api-version=2022-07-01-preview" + + def CreateDevice(self, deviceId, isIotEdge=False): + """ + Use the IotHubRegistryManager to create the device using the passed deviceId and isIotEdge parameters using the IotHubRegistryManager + + :param str deviceId: The device-id to create the device with + :param bool isIotEdge: Flag that determines whether the device should be created as an IotEdge device or an IotDevice + :returns: A connection string on success; An empty string on failure + """ + primary_key = base64.b64encode(str(uuid.uuid4()).encode()).decode() + secondary_key = base64.b64encode(str(uuid.uuid4()).encode()).decode() + device_status = "enabled" + + self._hubRegistryManager.create_device_with_sas(deviceId, primary_key, secondary_key, device_status, isIotEdge) + + # Should always be of type Device + device = self._hubRegistryManager.get_device(deviceId) + + if (type(device) != Device): + print( + "Return type for retrieving the device state is not Device. Requested Raw response?") + + # Can't guarantee that the device will be connected but the generation-id should work + if (device.generation_id == ""): + return "" + + # Once we can confirm that the device has been created we can make the connection string + connectionString = "HostName=" + self._iotHubUrl + ";DeviceId=" + deviceId + ";SharedAccessKey=" + primary_key + + return connectionString + + def DeleteDevice(self, deviceId): + """ + Deletes the device specified by deviceId + + :param str deviceId: the identifier of the device to be deleted + :returns: True on successful deletion, false otherwise + """ + try: + + self._hubRegistryManager.delete_device(deviceId) + + except HttpOperationError: + return False + + return True + + def CreateModuleForExistingDevice(self, deviceId, moduleId): + """ + Use the IotHubRegistryManager to create the module on the device using the passed deviceId and moduleId parameters using the IotHubRegistryManager + + :param str deviceId: The device on which to create the module + :param str moduleId: The module name to be used when creating the module + :returns: A connection string on success; An empty string on failure + """ + primary_key = base64.b64encode(str(uuid.uuid4()).encode()).decode() + secondary_key = base64.b64encode(str(uuid.uuid4()).encode()).decode() + device_status = "enabled" + + self._hubRegistryManager.create_module_with_sas(deviceId, moduleId, managed_by="", primary_key=primary_key, secondary_key=secondary_key) + + module = self._hubRegistryManager.get_module(deviceId, moduleId) + + if (module.generation_id == ""): + return "" + + # Once we can confirm that the module has been created we can make the connection string + connectionString = "HostName=" + self._iotHubUrl + ";DeviceId=" + deviceId + ";ModuleId=" + moduleId + ";SharedAccessKey=" + primary_key + + return connectionString + + def DeleteModuleOnDevice(self, deviceId, moduleId): + """ + Deletes the module specified by moduleId on the device + + :param str deviceId: the device for the module + :param str moduleId: the module to be deleted + :returns: True on successful deletion, False otherwise + """ + + try: + + self._hubRegistryManager.delete_module(deviceId, moduleId) + + except HttpOperationError: + return False + + return True + + def AddDeviceToGroup(self, deviceId, groupName): + """ + Patches the device twin "tags" value on the device twin to include the key ADUGroup and value of the parameter groupName + + :param str deviceId: the device-id of the device to add to the group + :param str groupName: Name of the group to add the device to + :returns: True on success; False on failure + """ + + newTagForTwin = Twin(tags={"ADUGroup": groupName}) + + updatedTwin = self._hubRegistryManager.update_twin(deviceId, newTagForTwin) + + if (updatedTwin.tags["ADUGroup"] != groupName): + return False + + return True + + def AddModuleToGroup(self, deviceId, moduleId, groupName): + """ + Patches the module twin "tags" value on the device twin to include the key ADUGroup and value of the parameter groupName + + :param str deviceId: the device-id of the device for the module + :param str moduleId: the module-id for the module to which to add the group + :param str groupName: Name of the group to add the module to + :returns: True on success; False on failure + """ + + newTagForTwin = Twin(tags={"ADUGroup": groupName}) + twin = self.GetDeviceTwinForDevice(deviceId) + + updatedTwin = self._hubRegistryManager.update_module_twin(deviceId, moduleId, newTagForTwin) + + if (updatedTwin.tags["ADUGroup"] != groupName): + return False + + return True + + def GetDeviceTwinForDevice(self, deviceId): + """ + Returns the twin of the device + + :param str deviceId: Identifier for the device to retreive the Twin for + :returns: An object of type azure.iot.hub.protocols.models.Twin which encapsulates the twin + """ + return self._hubRegistryManager.get_twin(deviceId) + + def GetModuleTwinForModule(self, deviceId, moduleId): + """ + Returns the twin of the module + + :param str deviceId: Identifier for the device for the module + :param str moduleId: Identifier for the module for which to retrieve the twin + :returns: An object of type azure.iot.hub.protocols.models.Twin which encapsulates the twin + """ + return self._hubRegistryManager.get_module_twin(deviceId, moduleId) + + def GetConnectionStatusForDevice(self, deviceId): + """ + Returns the connection state of the device + + :param str deviceId: the deviceId for the device + :returns: the string representation of the connection_state + """ + return self._hubRegistryManager.get_device(deviceId).connection_state + + def GetConnectionStatusForModule(self, deviceId, moduleId): + """ + Returns the connection state of the module + + :param str deviceId: the deviceId for the device + :param str moduleId: the module for which to retrieve the connection_state + :returns: the string representation of the connection_state + """ + return self._hubRegistryManager.get_module(deviceId, moduleId).connection_state + + def StartDeploymentForGroup(self, deploymentId, groupName, updateId): + """ + Starts the deployment for the specified groupname and updateId + + :param str deploymentId: the id for the deployment + :param str groupName: the id for the group to which to deploy the update + :param str updateId: the update-id for the deployment + :param str rollbackPolicy: the string version of the rollback policy + :param str failure: the string body of the failure definition + :returns: the status code for the deployment request, 200 for success, all other values are failures + """ + requestString = "/deviceUpdate/" + self._aduInstanceId + "/management/groups/" + groupName + "/deployments/" + deploymentId + self._aduApiVersion + + if (type(updateId) != UpdateId): + print("Unusable type for updateId, use the UpdateId class") + + jsonBodyString = '{"deploymentId": "' + deploymentId + '","groupId": "' + groupName + '","startDateTime": "' + str(datetime.datetime.now()) + '","update":{ "updateId": ' + str(updateId) + ' } }' + + deploymentStartRequest = HttpRequest("PUT", requestString, json=json.loads(jsonBodyString)) + + deploymentStartResponse = self._aduAcnt.send_request(deploymentStartRequest) + + return deploymentStartResponse.status_code + + def StopDeployment(self, deploymentId, groupName, deviceClassId): + """ + Stops the deployment for the specified groupname + + :param str deploymentId: the id for the deployment + :param str groupName: the id for the group to which to cancel the deployment + :returns: the status code for the deployment cancel request, 200 for success, all other values are failures + """ + # /deviceUpdate/{instanceId}/management/groups/{groupId}/deviceClassSubgroups/{deviceClassId}/deployments/{deploymentId} + requestString = "/deviceUpdate/" + self._aduInstanceId + "/management/groups/" + groupName + "/deployments/" + deploymentId + self._aduApiVersion + + deploymentCancelRequest = HttpRequest("POST", requestString) + + deploymentCancelResponse = self._aduAcnt.send_request(deploymentCancelRequest) + + return deploymentCancelResponse.status_code + + def GetDeploymentStatusForGroup(self, deploymentId, groupName): + """ + Returns the deployment state for the given deploymentId and groupName + + :param str deploymentId: the id for the deployment + :param str groupName: the id for the group to which the deployment was made + :returns: An object of type DeploymentStatusResponse, this will be empty on failure + """ + deploymentStatusRequest = HttpRequest("GET", "/deviceUpdate/" + self._aduInstanceId + "/management/groups/" + groupName + "/deployments/" + deploymentId + "/status" + self._aduApiVersion) + + deploymentStatusResponse = self._aduAcnt.send_request(deploymentStatusRequest) + + if (deploymentStatusResponse.status_code != 200): + return None + + deploymentStateResponseJson = DeploymentStatusResponse(deploymentStatusResponse.json()) + + return deploymentStateResponseJson + + def DeleteDeployment(self, deploymentId, groupId): + """ + Deletes the deployment specified by deploymentId for group specified by groupId + + :param str deploymentId: the identifier for the deployment to be deleted + :param str groupId: the group-id for group on which the deployment was operating + :returns: the status code of the response to delete the deployment + """ + requestString = '/deviceUpdate/' + self._aduInstanceId + '/management/groups/' + groupId + '/deployments/' + deploymentId + self._aduApiVersion + + deleteDeploymentRequest = HttpRequest("DELETE", requestString) + + deleteDeploymentResponse = self._aduAcnt.send_request(deleteDeploymentRequest) + + return deleteDeploymentResponse.status_code + + def RunDiagnosticsOnDeviceOrModule(self,deviceId,operationId,description,moduleId=""): + """ + Initiates a diagnostics log collection flow for the specified device or module using the operationId and description passed + + :param str deviceId: the device-id to target + :param str operationId: the operation-id for the diagnostics workflow + :param str description: the description for the diagnostics workflow + :param str moduleId: Optional parameter only to be specified when targeting a module + :returns: the status code of the request to start the diagnostics operation + """ + jsonBody = None + if (len(moduleId) == 0): + jsonBody = { + "deviceList":[ + { + "deviceId": deviceId + }, + ], + "description": description, + "operationId":operationId + } + else: + jsonBody = { + "deviceList": [ + { + "deviceId": deviceId, + "moduleId": moduleId + } + ], + "description": description, + "operationId":operationId + } + + collectLog_url = f'management/deviceDiagnostics/logCollections/{operationId}{self._iotHubApiVersion}' + + requestString = urljoin(self._base_url, collectLog_url) + + diagnosticsRequest = HttpRequest("PUT", requestString, json=jsonBody) + + diagnosticsResponse = self._aduAcnt.send_request(diagnosticsRequest) + + return diagnosticsResponse.status_code + + def GetDiagnosticsLogCollectionStatus(self, operationId): + """ + Returns the log collection state for the given operationId + :param str operationId: Log collection operation identifier + :returns: An object of type DiagnosticLogCollectionStatusResponse, this will be empty on failure + """ + getLogCollectDetail_url = f'management/deviceDiagnostics/logCollections/{operationId}/detailedstatus{self._aduApiVersion}' + + requestString = urljoin(self._base_url, getLogCollectDetail_url) + + logCollectionStatusRequest = HttpRequest("GET", requestString) + + logCollectionStatusResponse = self._aduAcnt.send_request(logCollectionStatusRequest) + + if (logCollectionStatusResponse.status_code != 200): + return DiagnosticLogCollectionStatusResponse() + + logCollectionStatusResponseJson = DiagnosticLogCollectionStatusResponse().ParseResponseJson(logCollectionStatusResponse.json()) + + return logCollectionStatusResponseJson + + def DeleteADUGroup(self, aduGroupId): + """ + Deletes the ADUGroup declared by aduGroupId + + :param str aduGroupId: the ADU Group to delete + :returns: the status code of the response + """ + requestString = '/deviceUpdate/' + self._aduInstanceId + '/management/groups/' + aduGroupId + self._aduApiVersion + + deleteAduGroupRequest = HttpRequest("DELETE", requestString) + + deleteAduGroupResponse = self._aduAcnt.send_request(deleteAduGroupRequest) + + return deleteAduGroupResponse.status_code + + diff --git a/azurepipelines/e2e_test/scenarios/testingtoolkit/_version.py b/azurepipelines/e2e_test/scenarios/testingtoolkit/_version.py new file mode 100644 index 000000000..2dd0f5af6 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/testingtoolkit/_version.py @@ -0,0 +1,7 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- + +VERSION = "0.1.0" diff --git a/azurepipelines/e2e_test/scenarios/testingtoolkit/requirements.txt b/azurepipelines/e2e_test/scenarios/testingtoolkit/requirements.txt new file mode 100644 index 000000000..5f3037a35 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/testingtoolkit/requirements.txt @@ -0,0 +1,6 @@ +azure-core==1.23.0 +azure-identity==1.8.0 +azure-iot-deviceupdate==1.0.0b2 +azure-iot-hub==2.6.0 +msrest==0.6.21 +unittest-xml-reporting==3.1.0 diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/Bundle-update.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/Bundle-update.py new file mode 100644 index 000000000..679cd90f7 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/Bundle-update.py @@ -0,0 +1,163 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner + +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/ubuntu-18.04-amd64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix,test_bundle_update_deployment_id, test_connection_timeout_tries , retry_wait_time_in_seconds + +# +# Global Test Variables +# + +bundle_update_status_retries = 15 + +class AptDeploymentTest(unittest.TestCase): + + # + # Every test within a subclass of a unittest.TestCase to be run by unittest must have the prefix test_ + # + def test_AptDeployment(self): + + # + # The first step to any test is to create the test helper that allows us to make calls to both the DU account and the + # IotHub. In the pipeline the parameters to create the test helper are passed to the environment from which we can read them + # here. + # + # For testing it might be convenient to simply define these above and directly instantiate the object or if you're not planning + # on running the tests as apart of a unittest.TestCase you can simply pass them via the CLI to the python program + # and create the test helper using the helper method + # DuAutomatedTestConfigurationManager.FromCliArgs() + # + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + + # + # We retrieve the apt deployment id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.deploymentId = test_bundle_update_deployment_id + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0,test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus,"Connected") + # + # For every deployment we need to create an Update Id. For all tests running in the DeviceUpdate for IotHub Test Automation + # pipeline we use Fabrikaam as a provider and Vaccuum as the name for the update. You'll see the same values supplied as the + # manufacturer and model in the devicesetup.py script. + # + self.deploymentUpdateId = UpdateId(provider="Contoso1", name="Virtual", version="3.0") + + # + # The assumption is that we've already imported the update targeting the manufacturer and + # model for the device created in the devicesetup.py. Now we have to create the deployment for + # the device using the update-id for the update + # + # Note: ALL UPDATES SHOULD BE UPLOADED TO THE TEST AUTOMATION HUB NOTHING SHOULD BE IMPORTED AT TEST TIME + # + status_code = self.duTestHelper.StartDeploymentForGroup(deploymentId=self.deploymentId,groupName=test_adu_group,updateId=self.deploymentUpdateId) + + self.assertEqual(status_code,200) + + # + # Once we've started the deployment for the group we can then query for the status of the deployment + # to see if/when the deployment finishes. We expect a timeout of the configured amount of time + # + deploymentStatus = None + + + for i in range(0,bundle_update_status_retries): + deploymentStatus = self.duTestHelper.GetDeploymentStatusForGroup(self.deploymentId,test_adu_group) + + # + # If we see all the devices have completed the deployment then we can exit early + # + if (len(deploymentStatus.subgroupStatuses) != 0): + if (deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount == 1): + break + time.sleep(retry_wait_time_in_seconds) + + # + # Should only be one device group in the deployment + # + self.assertEqual(len(deploymentStatus.subgroupStatuses),1) + + # + # Devices in the group should have succeeded + # + self.assertEqual(deploymentStatus.subgroupStatuses[0].totalDevices,deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount) + + # Sleep to give time for changes to propagate and for DU to switch it's state back to idle + time.sleep(retry_wait_time_in_seconds) + + # + # Once we've checked that we've reached the proper outcome for the + # deployment we need to check that the device itself has reported it + # is back in the idle state. + # + twin = self.duTestHelper.GetDeviceTwinForDevice(test_device_id) + + self.assertEqual(twin.properties.reported["deviceUpdate"]["agent"]["state"],0) + + # + # In case of a succeeded deployment we need to clean up the resources we created. + # mainly the deletion of the deployment + # + time.sleep(retry_wait_time_in_seconds) + + # self.assertEqual(self.duTestHelper.StopDeployment(self.deploymentId,test_adu_group),200) + # time.sleep(retry_wait_time_in_seconds) + + # Once stopped we can delete the deployment + self.assertEqual(self.duTestHelper.DeleteDeployment(self.deploymentId,test_adu_group),204) + + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + # + # Create the IO pipe + # + out = io.BytesIO() + + # + # Exercise the TestCase and all the tests within it. + # + unittest.main(testRunner = xmlrunner.XMLTestRunner(output=out),failfast=False,buffer=False,catchbreak=False,exit=False) + + # + # Finally transform the output unto the X/JUnit XML file format + # + with open('./testresults/' + test_result_file_prefix + '-bundle-update-deployment-test.xml','wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/Multi-Component-Update.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/Multi-Component-Update.py new file mode 100644 index 000000000..e17e3d4c7 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/Multi-Component-Update.py @@ -0,0 +1,164 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner + +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/ubuntu-18.04-amd64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, test_mcu_deployment_id, test_connection_timeout_tries, retry_wait_time_in_seconds + +# +# Global Test Variables +# + +mcu_deployment_status_retries = 15 + +class AptDeploymentTest(unittest.TestCase): + + # + # Every test within a subclass of a unittest.TestCase to be run by unittest must have the prefix test_ + # + def test_AptDeployment(self): + + # + # The first step to any test is to create the test helper that allows us to make calls to both the DU account and the + # IotHub. In the pipeline the parameters to create the test helper are passed to the environment from which we can read them + # here. + # + # For testing it might be convenient to simply define these above and directly instantiate the object or if you're not planning + # on running the tests as apart of a unittest.TestCase you can simply pass them via the CLI to the python program + # and create the test helper using the helper method + # DuAutomatedTestConfigurationManager.FromCliArgs() + # + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + + # + # We retrieve the apt deployment id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.deploymentId = test_mcu_deployment_id + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0,test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus,"Connected") + # + # For every deployment we need to create an Update Id. For all tests running in the DeviceUpdate for IotHub Test Automation + # pipeline we use Fabrikaam as a provider and Vaccuum as the name for the update. You'll see the same values supplied as the + # manufacturer and model in the devicesetup.py script. + # + self.deploymentUpdateId = UpdateId(provider="Contoso1", name="Virtual", version="6.0") + + # + # The assumption is that we've already imported the update targeting the manufacturer and + # model for the device created in the devicesetup.py. Now we have to create the deployment for + # the device using the update-id for the update + # + # Note: ALL UPDATES SHOULD BE UPLOADED TO THE TEST AUTOMATION HUB NOTHING SHOULD BE IMPORTED AT TEST TIME + # + status_code = self.duTestHelper.StartDeploymentForGroup(deploymentId=self.deploymentId,groupName=test_adu_group,updateId=self.deploymentUpdateId) + + self.assertEqual(status_code,200) + + # + # Once we've started the deployment for the group we can then query for the status of the deployment + # to see if/when the deployment finishes. We expect a timeout of the configured amount of time + # + deploymentStatus = None + + + for i in range(0,mcu_deployment_status_retries): + deploymentStatus = self.duTestHelper.GetDeploymentStatusForGroup(self.deploymentId,test_adu_group) + + # + # If we see all the devices have completed the deployment then we can exit early + # + if (len(deploymentStatus.subgroupStatuses) != 0): + if (deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount == 1): + break + time.sleep(retry_wait_time_in_seconds) + + + # + # Should only be one device group in the deployment + # + self.assertEqual(len(deploymentStatus.subgroupStatuses),1) + + # + # Devices in the group should have succeeded + # + self.assertEqual(deploymentStatus.subgroupStatuses[0].totalDevices,deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount) + + # Sleep to give time for changes to propagate and for DU to switch it's state back to idle + time.sleep(retry_wait_time_in_seconds) + + # + # Once we've checked that we've reached the proper outcome for the + # deployment we need to check that the device itself has reported it + # is back in the idle state. + # + twin = self.duTestHelper.GetDeviceTwinForDevice(test_device_id) + + self.assertEqual(twin.properties.reported["deviceUpdate"]["agent"]["state"],0) + + # + # In case of a succeeded deployment we need to clean up the resources we created. + # mainly the deletion of the deployment + # + time.sleep(retry_wait_time_in_seconds) + + # self.assertEqual(self.duTestHelper.StopDeployment(self.deploymentId,test_adu_group),200) + # time.sleep(retry_wait_time_in_seconds) + + # Once stopped we can delete the deployment + self.assertEqual(self.duTestHelper.DeleteDeployment(self.deploymentId,test_adu_group),204) + + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + # + # Create the IO pipe + # + out = io.BytesIO() + + # + # Exercise the TestCase and all the tests within it. + # + unittest.main(testRunner = xmlrunner.XMLTestRunner(output=out),failfast=False,buffer=False,catchbreak=False,exit=False) + + # + # Finally transform the output unto the X/JUnit XML file format + # + with open('./testresults/' + test_result_file_prefix + '-mcu-deployment-test.xml','wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/add_device_to_adu_group.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/add_device_to_adu_group.py new file mode 100644 index 000000000..96811164e --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/add_device_to_adu_group.py @@ -0,0 +1,60 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner +from xmlrunner.extra.xunit_plugin import transform +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/ubuntu-18.04-amd64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, test_connection_timeout_tries, retry_wait_time_in_seconds + +class AddDeviceToGroupTest(unittest.TestCase): + + def test_AddDeviceToGroup(self): + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0,test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus, "Connected") + + # + # Before we can run the deployment we need to add the ADUGroup test_adu_group tag to the + # the device before we make the ADUGroup which we can then use to target the deployment + # to the device. + # + success = self.duTestHelper.AddDeviceToGroup(test_device_id, test_adu_group) + + self.assertTrue(success) + time.sleep(retry_wait_time_in_seconds) + + +if (__name__ == "__main__"): + out = io.BytesIO() + + unittest.main(testRunner = xmlrunner.XMLTestRunner(output=out), failfast=False, buffer=False, catchbreak=False, exit=False) + + with open('./testresults/' + test_result_file_prefix + 'add-device-to-adu-group-test.xml', 'wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/apt_deployment.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/apt_deployment.py new file mode 100644 index 000000000..adca164a2 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/apt_deployment.py @@ -0,0 +1,161 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner + +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/ubuntu-18.04-amd64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, test_apt_deployment_id, test_connection_timeout_tries, retry_wait_time_in_seconds + +# +# Global Test Variables +# + +apt_deployment_status_retries = 15 + +class AptDeploymentTest(unittest.TestCase): + + # + # Every test within a subclass of a unittest.TestCase to be run by unittest must have the prefix test_ + # + def test_AptDeployment(self): + + # + # The first step to any test is to create the test helper that allows us to make calls to both the DU account and the + # IotHub. In the pipeline the parameters to create the test helper are passed to the environment from which we can read them + # here. + # + # For testing it might be convenient to simply define these above and directly instantiate the object or if you're not planning + # on running the tests as apart of a unittest.TestCase you can simply pass them via the CLI to the python program + # and create the test helper using the helper method + # DuAutomatedTestConfigurationManager.FromCliArgs() + # + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + + # + # We retrieve the apt deployment id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.deploymentId = test_apt_deployment_id + + self.deploymentUpdateId = UpdateId(provider="Contoso1", name="Virtual", version="1.0.2") + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0,test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus, "Connected") + + # + # The assumption is that we've already imported the update targeting the manufacturer and + # model for the device created in the devicesetup.py. Now we have to create the deployment for + # the device using the update-id for the update + # + # Note: ALL UPDATES SHOULD BE UPLOADED TO THE TEST AUTOMATION HUB NOTHING SHOULD BE IMPORTED AT TEST TIME + # + status_code = self.duTestHelper.StartDeploymentForGroup(deploymentId=self.deploymentId,groupName=test_adu_group,updateId=self.deploymentUpdateId) + + self.assertEqual(status_code, 200) + + # + # Once we've started the deployment for the group we can then query for the status of the deployment + # to see if/when the deployment finishes. We expect a timeout of the configured amount of time + # + deploymentStatus = None + + + for i in range(0,apt_deployment_status_retries): + deploymentStatus = self.duTestHelper.GetDeploymentStatusForGroup(self.deploymentId,test_adu_group) + + # + # If we see all the devices have completed the deployment then we can exit early + # + if (len(deploymentStatus.subgroupStatuses) != 0): + if (deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount == 1): + break + time.sleep(retry_wait_time_in_seconds) + + # + # Should only be one device group in the deployment + # + self.assertEqual(len(deploymentStatus.subgroupStatuses),1) + + # + # Devices in the group should have succeeded + # + self.assertEqual(deploymentStatus.subgroupStatuses[0].totalDevices,deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount) + + # Sleep to give time for changes to propagate and for DU to switch it's state back to idle + time.sleep(retry_wait_time_in_seconds) + + # + # Once we've checked that we've reached the proper outcome for the + # deployment we need to check that the device itself has reported it + # is back in the idle state. + # + twin = self.duTestHelper.GetDeviceTwinForDevice(test_device_id) + + self.assertEqual(twin.properties.reported["deviceUpdate"]["agent"]["state"],0) + + # + # In case of a succeeded deployment we need to clean up the resources we created. + # mainly the deletion of the deployment + # + time.sleep(retry_wait_time_in_seconds) + + deviceClassId = deploymentStatus.subgroupStatuses[0].deviceClassId + + # self.assertEqual(self.duTestHelper.StopDeployment(self.deploymentId,test_adu_group,deviceClassId),200) + # time.sleep(retry_wait_time_in_seconds) + + # Once stopped we can delete the deployment + self.assertEqual(self.duTestHelper.DeleteDeployment(self.deploymentId, test_adu_group), 204) + + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + # + # Create the IO pipe + # + out = io.BytesIO() + + # + # Exercise the TestCase and all the tests within it. + # + unittest.main(testRunner = xmlrunner.XMLTestRunner(output=out),failfast=False,buffer=False,catchbreak=False,exit=False) + + # + # Finally transform the output unto the X/JUnit XML file format + # + with open('./testresults/' + test_result_file_prefix + '-apt-deployment-test.xml','wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/delete_device.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/delete_device.py new file mode 100644 index 000000000..a36dfaca5 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/delete_device.py @@ -0,0 +1,43 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +sys.path.append('./scenarios/ubuntu-18.04-amd64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, retry_wait_time_in_seconds + +class DeleteDeviceAndGroup(unittest.TestCase): + + def test_DeleteTestDeviceAndGroup(self): + configManager = DuAutomatedTestConfigurationManager.FromOSEnvironment() + + duTestHelper = configManager.CreateDeviceUpdateTestHelper() + + self.assertTrue(duTestHelper.DeleteDevice(test_device_id)) + + time.sleep(retry_wait_time_in_seconds*5) + + self.assertEqual(duTestHelper.DeleteADUGroup(test_adu_group), 204) + + + +if (__name__ == "__main__"): + out = io.BytesIO() + + unittest.main(testRunner=xmlrunner.XMLTestRunner(output=out), + failfast=False, buffer=False, catchbreak=False, exit=False) + + with open('./testresults/' + test_result_file_prefix + '-delete-device-test.xml', 'wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/diagnostics.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/diagnostics.py new file mode 100644 index 000000000..42f92df92 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/diagnostics.py @@ -0,0 +1,99 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner + +# Import testingtoolkit module from parent directory +# sys.path.append('../') +# from testingtoolkit import DeviceUpdateTestHelper +# from testingtoolkit import UpdateId +# from azure.identity import DefaultAzureCredential +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DiagnosticLogCollectionStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/ubuntu-18.04-amd64/') +from scenario_definitions import test_device_id, test_result_file_prefix, test_operation_id + +diagnostics_operation_status_retries = 15 +class DiagnosticsTest(unittest.TestCase): + def test_diagnostics(self): + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + # + # We retrieve the test operation id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.operationId = test_operation_id + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0, 5): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(30) + + self.assertEqual(connectionStatus, "Connected") + + # + # The assumption is that we've already configured diagnostics in test account and already run a apt deployment + # Start a diagnostics log collection operation + # 201 indicate the device diagnostics log collection operation is created + # + diagnosticResponseStatus_code = self.duTestHelper.RunDiagnosticsOnDeviceOrModule( + test_device_id, operationId=self.operationId, description="") + + self.assertEqual(diagnosticResponseStatus_code, 201) + + # + # Get device diagnostics log collection operatio status and device status + # device status represent Log upload status, operatin status represent background operation status + # + diagnosticJsonStatus = DiagnosticLogCollectionStatusResponse() + for i in range(0, diagnostics_operation_status_retries): + time.sleep(5) + diagnosticJsonStatus = self.duTestHelper.GetDiagnosticsLogCollectionStatus(self.operationId) + + if (diagnosticJsonStatus.status == "Succeeded"): + break + + self.assertEqual(diagnosticJsonStatus.status, "Succeeded") + + self.assertEqual(len(diagnosticJsonStatus.deviceStatus),1) + self.assertEqual(diagnosticJsonStatus.deviceStatus[0].status, "Succeeded") + self.assertEqual(diagnosticJsonStatus.deviceStatus[0].resultCode, "200") + + + + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + out = io.BytesIO() + + unittest.main(testRunner=xmlrunner.XMLTestRunner(output=out), + failfast=False, buffer=False, catchbreak=False, exit=False) + + with open('./testresults/' + test_result_file_prefix + '-diagnostic-test.xml', 'wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/scenario_definitions.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/scenario_definitions.py new file mode 100644 index 000000000..d65d2d8b7 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/scenario_definitions.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +# +# These are the definitions used by the scenario to complete it's operations. It is used by all the TestCases held within +# the project. +# +import sys +import uuid + +sys.path.append('./') + +# +# Base Device Definitions +# +test_device_id = "ubuntu18.04-amd64-deployment-test-device" + +test_adu_group = "Ubuntu1804AMD64TestGroup" + +test_apt_deployment_id = str(uuid.uuid4()) + +test_operation_id = str(uuid.uuid4()).replace('-', '') + +test_mcu_deployment_id = str(uuid.uuid4()) + +test_bundle_update_deployment_id = str(uuid.uuid4()) + +test_result_file_prefix = 'ubuntu18.04-amd64' + +test_connection_timeout_tries = 10 + +retry_wait_time_in_seconds = 60 # For all retries this is the total amount of time we wait for all operations + + +# +# Other variables that should be available to all tests in this scenario may be added here +# diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/vm_setup/devicesetup.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/vm_setup/devicesetup.py new file mode 100644 index 000000000..a3914d298 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/vm_setup/devicesetup.py @@ -0,0 +1,82 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import json +import sys + +# Note: the intention is that this script is called like: +# python .//vm-setup/devicesetup.py +sys.path.append('./scenarios/') + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/ubuntu-18.04-amd64/') +from scenario_definitions import test_device_id +from testingtoolkit import DeviceUpdateTestHelper, DuAutomatedTestConfigurationManager + +def main(): + duTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + # + # Instantiate the Wrapper Class from the config manager + # + duTestWrapper = duTestConfig.CreateDeviceUpdateTestHelper() + + # + # Delete the device if it exists, we don't care if we fail here. + # + duTestWrapper.DeleteDevice(test_device_id) + # + # Create the Device + # + connectionString = duTestWrapper.CreateDevice(test_device_id,isIotEdge=True) + + if (len(connectionString) == 0): + print_error("Failed to create the device in the IotHub") + exit(1) + + # + # Create the du-config.json JSON Object + # + duConfigJson = { + "schemaVersion": "1.1", + "aduShellTrustedUsers": [ + "adu", + "do" + ], + "iotHubProtocol": "mqtt", + "manufacturer": "contoso", + "model": "virtual-vacuum-v2", + "agents": [ + { + "name": "main", + "runas": "adu", + "connectionSource": { + "connectionType": "string", + "connectionData": connectionString + }, + "manufacturer": "contoso", + "model": "virtual-vacuum-v2" + } + ] + } + + # + # Write the configuration out to disk so we can install it as a part of the test + # + + with open('du-config.json','w') as jsonFile: + configJson = json.dumps(duConfigJson) + jsonFile.write(configJson) + +# +# Output is now a newly created device and a du-config.json file that will work with it +# + + +if __name__ == '__main__': + main() + +def print_error(msg): + print(msg, file=sys.stderr) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/vm_setup/setup.sh b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/vm_setup/setup.sh new file mode 100644 index 000000000..d66017661 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-amd64/vm_setup/setup.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +# Setup Script for Test: Ubuntu-18.04-amd64-APT-deployment +# +# Should be run on the Virtual Machine being provisioned for the work. + +# This file should be included in an archive that is created at provisioning time +# with the following structure: +# setup.sh (this file) +# deviceupdate-package.deb (debian package under test) +# +# where the archive is named: +# testsetup.tar.gz +# The archive is manually mounted to the file system at: +# ~/testsetup.tar.gz +# Once the script has been extracted we'll be running from +# ~ while the script itself is at +# ~/testsetup/setup.sh +# So we need to localize the path to that. +# +# +# Install the Microsoft APT repository +# + +wget https://packages.microsoft.com/config/ubuntu/18.04/multiarch/packages-microsoft-prod.deb -O packages-microsoft-prod.deb +sudo dpkg -i packages-microsoft-prod.deb +rm packages-microsoft-prod.deb + +# +# Update the apt repositories +# +sudo apt-get update + +# +# Install Device Update Dependencies from APT +# +# Note: If there are other dependencies tht need to be installed via APT or other means they should +# be added here. You might be installing iotedge, another package to setup for a deployment test, or +# anything else. + +# Handle installing DO from latest build instead of packages.microsoft.com +wget https://github.com/microsoft/do-client/releases/download/v1.0.0/ubuntu1804_x64-packages.tar -O ubuntu18_x64-packages.tar +tar -xvf ubuntu18_x64-packages.tar +sudo apt-get install -y ./deliveryoptimization-agent_1.0.0_amd64.deb ./deliveryoptimization-plugin-apt_0.5.1_amd64.deb ./libdeliveryoptimization_1.0.0_amd64.deb + + +# +# Install the Device Update Artifact Under Test +# +sudo apt-get install -y ./testsetup/deviceupdate-package.deb + +# +# Install the du-config.json so the device can be provisioned +# +# Note: In other setup scripts there may be more info that needs to +# go here. For instance you might have the config.toml for IotEdge, +# another kind of diagnostics file, or other kinds of data +# this is the area where such things can be added +sudo cp ./testsetup/du-config.json /etc/adu/du-config.json + + +mkdir ~/adu_srcs/ + +tar -xvf ./testsetup/adu_srcs_repo.tar.gz -C ~/adu_srcs/ + +sudo mkdir -p ~/demo/demo-devices/contoso-devices + +cd ~/adu_srcs/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo || exit + +chmod 755 ./tools/reset-demo-components.sh + +#copy components-inventory.json and adds it to the /usr/local/contoso-devices folder +sudo cp -a ./demo-devices/contoso-devices/. ~/demo/demo-devices/contoso-devices/ + +sh ./tools/reset-demo-components.sh + +#registers the extension +sudo /usr/bin/AducIotAgent -l 2 --extension-type componentEnumerator --register-extension /var/lib/adu/extensions/sources/libcontoso_component_enumerator.so + +sh ./tools/reset-demo-components.sh + + +# +# Restart the deviceupdate-agent.service +# +# Note: We expect that everything should be setup for the deviceupdate agent at this point. Once +# we restart the agent we expect it to be able to boot back up and connect to the IotHub. Otherwise +# this test will be considered a failure. +sudo systemctl restart deviceupdate-agent.service diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/Bundle-update.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/Bundle-update.py new file mode 100644 index 000000000..cf763b755 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/Bundle-update.py @@ -0,0 +1,163 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner + +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/ubuntu-18.04-arm64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix,test_bundle_update_deployment_id, test_connection_timeout_tries , retry_wait_time_in_seconds + +# +# Global Test Variables +# + +bundle_update_status_retries = 15 + +class AptDeploymentTest(unittest.TestCase): + + # + # Every test within a subclass of a unittest.TestCase to be run by unittest must have the prefix test_ + # + def test_AptDeployment(self): + + # + # The first step to any test is to create the test helper that allows us to make calls to both the DU account and the + # IotHub. In the pipeline the parameters to create the test helper are passed to the environment from which we can read them + # here. + # + # For testing it might be convenient to simply define these above and directly instantiate the object or if you're not planning + # on running the tests as apart of a unittest.TestCase you can simply pass them via the CLI to the python program + # and create the test helper using the helper method + # DuAutomatedTestConfigurationManager.FromCliArgs() + # + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + + # + # We retrieve the apt deployment id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.deploymentId = test_bundle_update_deployment_id + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0,test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus,"Connected") + # + # For every deployment we need to create an Update Id. For all tests running in the DeviceUpdate for IotHub Test Automation + # pipeline we use Fabrikaam as a provider and Vaccuum as the name for the update. You'll see the same values supplied as the + # manufacturer and model in the devicesetup.py script. + # + self.deploymentUpdateId = UpdateId(provider="Contoso1", name="Virtual", version="3.0") + + # + # The assumption is that we've already imported the update targeting the manufacturer and + # model for the device created in the devicesetup.py. Now we have to create the deployment for + # the device using the update-id for the update + # + # Note: ALL UPDATES SHOULD BE UPLOADED TO THE TEST AUTOMATION HUB NOTHING SHOULD BE IMPORTED AT TEST TIME + # + status_code = self.duTestHelper.StartDeploymentForGroup(deploymentId=self.deploymentId,groupName=test_adu_group,updateId=self.deploymentUpdateId) + + self.assertEqual(status_code,200) + + # + # Once we've started the deployment for the group we can then query for the status of the deployment + # to see if/when the deployment finishes. We expect a timeout of the configured amount of time + # + deploymentStatus = None + + + for i in range(0,bundle_update_status_retries): + deploymentStatus = self.duTestHelper.GetDeploymentStatusForGroup(self.deploymentId,test_adu_group) + + # + # If we see all the devices have completed the deployment then we can exit early + # + if (len(deploymentStatus.subgroupStatuses) != 0): + if (deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount == 1): + break + time.sleep(retry_wait_time_in_seconds) + + # + # Should only be one device group in the deployment + # + self.assertEqual(len(deploymentStatus.subgroupStatuses),1) + + # + # Devices in the group should have succeeded + # + self.assertEqual(deploymentStatus.subgroupStatuses[0].totalDevices,deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount) + + # Sleep to give time for changes to propagate and for DU to switch it's state back to idle + time.sleep(retry_wait_time_in_seconds) + + # + # Once we've checked that we've reached the proper outcome for the + # deployment we need to check that the device itself has reported it + # is back in the idle state. + # + twin = self.duTestHelper.GetDeviceTwinForDevice(test_device_id) + + self.assertEqual(twin.properties.reported["deviceUpdate"]["agent"]["state"],0) + + # + # In case of a succeeded deployment we need to clean up the resources we created. + # mainly the deletion of the deployment + # + time.sleep(retry_wait_time_in_seconds) + + # self.assertEqual(self.duTestHelper.StopDeployment(self.deploymentId,test_adu_group),200) + # time.sleep(retry_wait_time_in_seconds) + + # Once stopped we can delete the deployment + self.assertEqual(self.duTestHelper.DeleteDeployment(self.deploymentId,test_adu_group),204) + + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + # + # Create the IO pipe + # + out = io.BytesIO() + + # + # Exercise the TestCase and all the tests within it. + # + unittest.main(testRunner = xmlrunner.XMLTestRunner(output=out),failfast=False,buffer=False,catchbreak=False,exit=False) + + # + # Finally transform the output unto the X/JUnit XML file format + # + with open('./testresults/' + test_result_file_prefix + '-bundle-update-deployment-test.xml','wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/Multi-Component-Update.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/Multi-Component-Update.py new file mode 100644 index 000000000..9ae786136 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/Multi-Component-Update.py @@ -0,0 +1,164 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner + +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/ubuntu-18.04-arm64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, test_mcu_deployment_id, test_connection_timeout_tries, retry_wait_time_in_seconds + +# +# Global Test Variables +# + +mcu_deployment_status_retries = 15 + +class AptDeploymentTest(unittest.TestCase): + + # + # Every test within a subclass of a unittest.TestCase to be run by unittest must have the prefix test_ + # + def test_AptDeployment(self): + + # + # The first step to any test is to create the test helper that allows us to make calls to both the DU account and the + # IotHub. In the pipeline the parameters to create the test helper are passed to the environment from which we can read them + # here. + # + # For testing it might be convenient to simply define these above and directly instantiate the object or if you're not planning + # on running the tests as apart of a unittest.TestCase you can simply pass them via the CLI to the python program + # and create the test helper using the helper method + # DuAutomatedTestConfigurationManager.FromCliArgs() + # + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + + # + # We retrieve the apt deployment id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.deploymentId = test_mcu_deployment_id + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0,test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus,"Connected") + # + # For every deployment we need to create an Update Id. For all tests running in the DeviceUpdate for IotHub Test Automation + # pipeline we use Fabrikaam as a provider and Vaccuum as the name for the update. You'll see the same values supplied as the + # manufacturer and model in the devicesetup.py script. + # + self.deploymentUpdateId = UpdateId(provider="Contoso1", name="Virtual", version="6.0") + + # + # The assumption is that we've already imported the update targeting the manufacturer and + # model for the device created in the devicesetup.py. Now we have to create the deployment for + # the device using the update-id for the update + # + # Note: ALL UPDATES SHOULD BE UPLOADED TO THE TEST AUTOMATION HUB NOTHING SHOULD BE IMPORTED AT TEST TIME + # + status_code = self.duTestHelper.StartDeploymentForGroup(deploymentId=self.deploymentId,groupName=test_adu_group,updateId=self.deploymentUpdateId) + + self.assertEqual(status_code,200) + + # + # Once we've started the deployment for the group we can then query for the status of the deployment + # to see if/when the deployment finishes. We expect a timeout of the configured amount of time + # + deploymentStatus = None + + + for i in range(0,mcu_deployment_status_retries): + deploymentStatus = self.duTestHelper.GetDeploymentStatusForGroup(self.deploymentId,test_adu_group) + + # + # If we see all the devices have completed the deployment then we can exit early + # + if (len(deploymentStatus.subgroupStatuses) != 0): + if (deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount == 1): + break + time.sleep(retry_wait_time_in_seconds) + + + # + # Should only be one device group in the deployment + # + self.assertEqual(len(deploymentStatus.subgroupStatuses),1) + + # + # Devices in the group should have succeeded + # + self.assertEqual(deploymentStatus.subgroupStatuses[0].totalDevices,deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount) + + # Sleep to give time for changes to propagate and for DU to switch it's state back to idle + time.sleep(retry_wait_time_in_seconds) + + # + # Once we've checked that we've reached the proper outcome for the + # deployment we need to check that the device itself has reported it + # is back in the idle state. + # + twin = self.duTestHelper.GetDeviceTwinForDevice(test_device_id) + + self.assertEqual(twin.properties.reported["deviceUpdate"]["agent"]["state"],0) + + # + # In case of a succeeded deployment we need to clean up the resources we created. + # mainly the deletion of the deployment + # + time.sleep(retry_wait_time_in_seconds) + + # self.assertEqual(self.duTestHelper.StopDeployment(self.deploymentId,test_adu_group),200) + # time.sleep(retry_wait_time_in_seconds) + + # Once stopped we can delete the deployment + self.assertEqual(self.duTestHelper.DeleteDeployment(self.deploymentId,test_adu_group),204) + + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + # + # Create the IO pipe + # + out = io.BytesIO() + + # + # Exercise the TestCase and all the tests within it. + # + unittest.main(testRunner = xmlrunner.XMLTestRunner(output=out),failfast=False,buffer=False,catchbreak=False,exit=False) + + # + # Finally transform the output unto the X/JUnit XML file format + # + with open('./testresults/' + test_result_file_prefix + '-mcu-deployment-test.xml','wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/add_device_to_adu_group.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/add_device_to_adu_group.py new file mode 100644 index 000000000..ac88e1de9 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/add_device_to_adu_group.py @@ -0,0 +1,60 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner +from xmlrunner.extra.xunit_plugin import transform +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/ubuntu-18.04-arm64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, test_connection_timeout_tries, retry_wait_time_in_seconds + +class AddDeviceToGroupTest(unittest.TestCase): + + def test_AddDeviceToGroup(self): + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0,test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus, "Connected") + + # + # Before we can run the deployment we need to add the ADUGroup test_adu_group tag to the + # the device before we make the ADUGroup which we can then use to target the deployment + # to the device. + # + success = self.duTestHelper.AddDeviceToGroup(test_device_id, test_adu_group) + + self.assertTrue(success) + time.sleep(retry_wait_time_in_seconds) + + +if (__name__ == "__main__"): + out = io.BytesIO() + + unittest.main(testRunner = xmlrunner.XMLTestRunner(output=out), failfast=False, buffer=False, catchbreak=False, exit=False) + + with open('./testresults/' + test_result_file_prefix + 'add-device-to-adu-group-test.xml', 'wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/apt_deployment.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/apt_deployment.py new file mode 100644 index 000000000..64d550a8a --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/apt_deployment.py @@ -0,0 +1,161 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner + +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/ubuntu-18.04-arm64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, test_apt_deployment_id, test_connection_timeout_tries, retry_wait_time_in_seconds + +# +# Global Test Variables +# + +apt_deployment_status_retries = 15 + +class AptDeploymentTest(unittest.TestCase): + + # + # Every test within a subclass of a unittest.TestCase to be run by unittest must have the prefix test_ + # + def test_AptDeployment(self): + + # + # The first step to any test is to create the test helper that allows us to make calls to both the DU account and the + # IotHub. In the pipeline the parameters to create the test helper are passed to the environment from which we can read them + # here. + # + # For testing it might be convenient to simply define these above and directly instantiate the object or if you're not planning + # on running the tests as apart of a unittest.TestCase you can simply pass them via the CLI to the python program + # and create the test helper using the helper method + # DuAutomatedTestConfigurationManager.FromCliArgs() + # + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + + # + # We retrieve the apt deployment id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.deploymentId = test_apt_deployment_id + + self.deploymentUpdateId = UpdateId(provider="Contoso1", name="Virtual", version="1.0.2") + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0,test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus, "Connected") + + # + # The assumption is that we've already imported the update targeting the manufacturer and + # model for the device created in the devicesetup.py. Now we have to create the deployment for + # the device using the update-id for the update + # + # Note: ALL UPDATES SHOULD BE UPLOADED TO THE TEST AUTOMATION HUB NOTHING SHOULD BE IMPORTED AT TEST TIME + # + status_code = self.duTestHelper.StartDeploymentForGroup(deploymentId=self.deploymentId,groupName=test_adu_group,updateId=self.deploymentUpdateId) + + self.assertEqual(status_code, 200) + + # + # Once we've started the deployment for the group we can then query for the status of the deployment + # to see if/when the deployment finishes. We expect a timeout of the configured amount of time + # + deploymentStatus = None + + + for i in range(0,apt_deployment_status_retries): + deploymentStatus = self.duTestHelper.GetDeploymentStatusForGroup(self.deploymentId,test_adu_group) + + # + # If we see all the devices have completed the deployment then we can exit early + # + if (len(deploymentStatus.subgroupStatuses) != 0): + if (deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount == 1): + break + time.sleep(retry_wait_time_in_seconds) + + # + # Should only be one device group in the deployment + # + self.assertEqual(len(deploymentStatus.subgroupStatuses),1) + + # + # Devices in the group should have succeeded + # + self.assertEqual(deploymentStatus.subgroupStatuses[0].totalDevices,deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount) + + # Sleep to give time for changes to propagate and for DU to switch it's state back to idle + time.sleep(retry_wait_time_in_seconds) + + # + # Once we've checked that we've reached the proper outcome for the + # deployment we need to check that the device itself has reported it + # is back in the idle state. + # + twin = self.duTestHelper.GetDeviceTwinForDevice(test_device_id) + + self.assertEqual(twin.properties.reported["deviceUpdate"]["agent"]["state"],0) + + # + # In case of a succeeded deployment we need to clean up the resources we created. + # mainly the deletion of the deployment + # + time.sleep(retry_wait_time_in_seconds) + + deviceClassId = deploymentStatus.subgroupStatuses[0].deviceClassId + + # self.assertEqual(self.duTestHelper.StopDeployment(self.deploymentId,test_adu_group,deviceClassId),200) + # time.sleep(retry_wait_time_in_seconds) + + # Once stopped we can delete the deployment + self.assertEqual(self.duTestHelper.DeleteDeployment(self.deploymentId, test_adu_group), 204) + + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + # + # Create the IO pipe + # + out = io.BytesIO() + + # + # Exercise the TestCase and all the tests within it. + # + unittest.main(testRunner = xmlrunner.XMLTestRunner(output=out),failfast=False,buffer=False,catchbreak=False,exit=False) + + # + # Finally transform the output unto the X/JUnit XML file format + # + with open('./testresults/' + test_result_file_prefix + '-apt-deployment-test.xml','wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/delete_device.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/delete_device.py new file mode 100644 index 000000000..804e70d61 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/delete_device.py @@ -0,0 +1,43 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +sys.path.append('./scenarios/ubuntu-18.04-arm64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, retry_wait_time_in_seconds + +class DeleteDeviceAndGroup(unittest.TestCase): + + def test_DeleteTestDeviceAndGroup(self): + configManager = DuAutomatedTestConfigurationManager.FromOSEnvironment() + + duTestHelper = configManager.CreateDeviceUpdateTestHelper() + + self.assertTrue(duTestHelper.DeleteDevice(test_device_id)) + + time.sleep(retry_wait_time_in_seconds*5) + + self.assertEqual(duTestHelper.DeleteADUGroup(test_adu_group), 204) + + + +if (__name__ == "__main__"): + out = io.BytesIO() + + unittest.main(testRunner=xmlrunner.XMLTestRunner(output=out), + failfast=False, buffer=False, catchbreak=False, exit=False) + + with open('./testresults/' + test_result_file_prefix + '-delete-device-test.xml', 'wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/diagnostics.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/diagnostics.py new file mode 100644 index 000000000..ca9342f10 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/diagnostics.py @@ -0,0 +1,96 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner + +# Import testingtoolkit module from parent directory +# sys.path.append('../') +# from testingtoolkit import DeviceUpdateTestHelper +# from testingtoolkit import UpdateId +# from azure.identity import DefaultAzureCredential +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DiagnosticLogCollectionStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/ubuntu-18.04-arm64/') +from scenario_definitions import test_device_id, test_result_file_prefix, test_operation_id + +diagnostics_operation_status_retries = 15 +class DiagnosticsTest(unittest.TestCase): + def test_diagnostics(self): + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + # + # We retrieve the test operation id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.operationId = test_operation_id + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0, 5): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(30) + + self.assertEqual(connectionStatus, "Connected") + + # + # The assumption is that we've already configured diagnostics in test account and already run a apt deployment + # Start a diagnostics log collection operation + # 201 indicate the device diagnostics log collection operation is created + # + diagnosticResponseStatus_code = self.duTestHelper.RunDiagnosticsOnDeviceOrModule( + test_device_id, operationId=self.operationId, description="") + + self.assertEqual(diagnosticResponseStatus_code, 201) + + # + # Get device diagnostics log collection operatio status and device status + # device status represent Log upload status, operatin status represent background operation status + # + diagnosticJsonStatus = DiagnosticLogCollectionStatusResponse() + for i in range(0, diagnostics_operation_status_retries): + time.sleep(5) + diagnosticJsonStatus = self.duTestHelper.GetDiagnosticsLogCollectionStatus(self.operationId) + + if (diagnosticJsonStatus.status == "Succeeded"): + break + + self.assertEqual(diagnosticJsonStatus.status, "Succeeded") + + self.assertEqual(len(diagnosticJsonStatus.deviceStatus),1) + self.assertEqual(diagnosticJsonStatus.deviceStatus[0].status, "Succeeded") + self.assertEqual(diagnosticJsonStatus.deviceStatus[0].resultCode, "200") + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + out = io.BytesIO() + + unittest.main(testRunner=xmlrunner.XMLTestRunner(output=out), + failfast=False, buffer=False, catchbreak=False, exit=False) + + with open('./testresults/' + test_result_file_prefix + '-diagnostic-test.xml', 'wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/scenario_definitions.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/scenario_definitions.py new file mode 100644 index 000000000..081af96b2 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/scenario_definitions.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +# +# These are the definitions used by the scenario to complete it's operations. It is used by all the TestCases held within +# the project. +# +import sys +import uuid + +sys.path.append('./') + +# +# Base Device Definitions +# +test_device_id = "ubuntu18.04-arm64-deployment-test-device" + +test_adu_group = "Ubuntu1804ARM64TestGroup" + +test_apt_deployment_id = str(uuid.uuid4()) + +test_operation_id = str(uuid.uuid4()).replace('-', '') + +test_mcu_deployment_id = str(uuid.uuid4()) + +test_bundle_update_deployment_id = str(uuid.uuid4()) + +test_result_file_prefix = 'ubuntu18.04-arm64' + +test_connection_timeout_tries = 10 + +retry_wait_time_in_seconds = 60 # For all retries this is the total amount of time we wait for all operations + + +# +# Other variables that should be available to all tests in this scenario may be added here +# diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/vm_setup/devicesetup.py b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/vm_setup/devicesetup.py new file mode 100644 index 000000000..4f1591caf --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/vm_setup/devicesetup.py @@ -0,0 +1,82 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import json +import sys + +# Note: the intention is that this script is called like: +# python .//vm-setup/devicesetup.py +sys.path.append('./scenarios/') + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/ubuntu-18.04-arm64/') +from scenario_definitions import test_device_id +from testingtoolkit import DeviceUpdateTestHelper, DuAutomatedTestConfigurationManager + +def main(): + duTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + # + # Instantiate the Wrapper Class from the config manager + # + duTestWrapper = duTestConfig.CreateDeviceUpdateTestHelper() + + # + # Delete the device if it exists, we don't care if we fail here. + # + duTestWrapper.DeleteDevice(test_device_id) + # + # Create the Device + # + connectionString = duTestWrapper.CreateDevice(test_device_id,isIotEdge=True) + + if (len(connectionString) == 0): + print_error("Failed to create the device in the IotHub") + exit(1) + + # + # Create the du-config.json JSON Object + # + duConfigJson = { + "schemaVersion": "1.1", + "aduShellTrustedUsers": [ + "adu", + "do" + ], + "iotHubProtocol": "mqtt", + "manufacturer": "contoso", + "model": "virtual-vacuum-v2", + "agents": [ + { + "name": "main", + "runas": "adu", + "connectionSource": { + "connectionType": "string", + "connectionData": connectionString + }, + "manufacturer": "contoso", + "model": "virtual-vacuum-v2" + } + ] + } + + # + # Write the configuration out to disk so we can install it as a part of the test + # + + with open('du-config.json','w') as jsonFile: + configJson = json.dumps(duConfigJson) + jsonFile.write(configJson) + +# +# Output is now a newly created device and a du-config.json file that will work with it +# + + +if __name__ == '__main__': + main() + +def print_error(msg): + print(msg, file=sys.stderr) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/vm_setup/setup.sh b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/vm_setup/setup.sh new file mode 100644 index 000000000..476583072 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-18.04-arm64/vm_setup/setup.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +# Setup Script for Test: Ubuntu-18.04-arm64-APT-deployment +# +# Should be run on the Virtual Machine being provisioned for the work. + +# This file should be included in an archive that is created at provisioning time +# with the following structure: +# setup.sh (this file) +# deviceupdate-package.deb (debian package under test) +# +# where the archive is named: +# testsetup.tar.gz +# The archive is manually mounted to the file system at: +# ~/testsetup.tar.gz +# Once the script has been extracted we'll be running from +# ~ while the script itself is at +# ~/testsetup/setup.sh +# So we need to localize the path to that. +# +# +# Install the Microsoft APT repository +# + +wget https://packages.microsoft.com/config/ubuntu/18.04/multiarch/packages-microsoft-prod.deb -O packages-microsoft-prod.deb +sudo dpkg -i packages-microsoft-prod.deb +rm packages-microsoft-prod.deb + +# +# Update the apt repositories +# +sudo apt-get update + +# +# Install Device Update Dependencies from APT +# +# Note: If there are other dependencies tht need to be installed via APT or other means they should +# be added here. You might be installing iotedge, another package to setup for a deployment test, or +# anything else. + +# Handle installing DO from latest build instead of packages.microsoft.com +wget https://github.com/microsoft/do-client/releases/download/v1.0.0/ubuntu1804_arm64-packages.tar -O ubuntu1804_arm64-packages.tar +tar -xvf ubuntu1804_arm64-packages.tar +sudo apt-get install -y ./deliveryoptimization-agent_1.0.0_arm64.deb ./deliveryoptimization-plugin-apt_0.5.1_arm64.deb ./libdeliveryoptimization_1.0.0_arm64.deb + + +# +# Install the Device Update Artifact Under Test +# +sudo apt-get install -y ./testsetup/deviceupdate-package.deb + +# +# Install the du-config.json so the device can be provisioned +# +# Note: In other setup scripts there may be more info that needs to +# go here. For instance you might have the config.toml for IotEdge, +# another kind of diagnostics file, or other kinds of data +# this is the area where such things can be added +sudo cp ./testsetup/du-config.json /etc/adu/du-config.json + + +mkdir ~/adu_srcs/ + +tar -xvf ./testsetup/adu_srcs_repo.tar.gz -C ~/adu_srcs/ + +sudo mkdir -p ~/demo/demo-devices/contoso-devices + +cd ~/adu_srcs/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo || exit + +chmod 755 ./tools/reset-demo-components.sh + +#copy components-inventory.json and adds it to the /usr/local/contoso-devices folder +sudo cp -a ./demo-devices/contoso-devices/. ~/demo/demo-devices/contoso-devices/ + +sh ./tools/reset-demo-components.sh + +#registers the extension +sudo /usr/bin/AducIotAgent -l 2 --extension-type componentEnumerator --register-extension /var/lib/adu/extensions/sources/libcontoso_component_enumerator.so + +sh ./tools/reset-demo-components.sh + + +# +# Restart the deviceupdate-agent.service +# +# Note: We expect that everything should be setup for the deviceupdate agent at this point. Once +# we restart the agent we expect it to be able to boot back up and connect to the IotHub. Otherwise +# this test will be considered a failure. +sudo systemctl restart deviceupdate-agent.service diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/Bundle-update.py b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/Bundle-update.py new file mode 100644 index 000000000..2ca50162f --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/Bundle-update.py @@ -0,0 +1,164 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner + +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/ubuntu-20.04-amd64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix,test_bundle_update_deployment_id, test_connection_timeout_tries , retry_wait_time_in_seconds + +# +# Global Test Variables +# + +bundle_update_deployment_retries = 15 + +class AptDeploymentTest(unittest.TestCase): + + # + # Every test within a subclass of a unittest.TestCase to be run by unittest must have the prefix test_ + # + def test_AptDeployment(self): + + # + # The first step to any test is to create the test helper that allows us to make calls to both the DU account and the + # IotHub. In the pipeline the parameters to create the test helper are passed to the environment from which we can read them + # here. + # + # For testing it might be convenient to simply define these above and directly instantiate the object or if you're not planning + # on running the tests as apart of a unittest.TestCase you can simply pass them via the CLI to the python program + # and create the test helper using the helper method + # DuAutomatedTestConfigurationManager.FromCliArgs() + # + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + + # + # We retrieve the apt deployment id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.deploymentId = test_bundle_update_deployment_id + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0,test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus,"Connected") + # + # For every deployment we need to create an Update Id. For all tests running in the DeviceUpdate for IotHub Test Automation + # pipeline we use Fabrikaam as a provider and Vaccuum as the name for the update. You'll see the same values supplied as the + # manufacturer and model in the devicesetup.py script. + # + self.deploymentUpdateId = UpdateId(provider="Contoso1", name="Virtual", version="3.0") + + # + # The assumption is that we've already imported the update targeting the manufacturer and + # model for the device created in the devicesetup.py. Now we have to create the deployment for + # the device using the update-id for the update + # + # Note: ALL UPDATES SHOULD BE UPLOADED TO THE TEST AUTOMATION HUB NOTHING SHOULD BE IMPORTED AT TEST TIME + # + status_code = self.duTestHelper.StartDeploymentForGroup(deploymentId=self.deploymentId,groupName=test_adu_group,updateId=self.deploymentUpdateId) + + self.assertEqual(status_code,200) + + # + # Once we've started the deployment for the group we can then query for the status of the deployment + # to see if/when the deployment finishes. We expect a timeout of the configured amount of time + # + deploymentStatus = None + + + for i in range(0,bundle_update_deployment_retries): + deploymentStatus = self.duTestHelper.GetDeploymentStatusForGroup(self.deploymentId,test_adu_group) + + # + # If we see the device has completed the deployment then we can exit early + # + if (len(deploymentStatus.subgroupStatuses) != 0): + if (deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount == 1): + break + time.sleep(retry_wait_time_in_seconds) + + + # + # Do the check to make sure the device in the subgroup have succeeded + # + self.assertEqual(len(deploymentStatus.subgroupStatuses),1) + + # + # Devices in the group should have succeeded + # + self.assertEqual(deploymentStatus.subgroupStatuses[0].totalDevices,deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount) + + # Sleep to give time for changes to propagate and for DU to switch it's state back to idle + time.sleep(retry_wait_time_in_seconds) + + # + # Once we've checked that we've reached the proper outcome for the + # deployment we need to check that the device itself has reported it + # is back in the idle state. + # + twin = self.duTestHelper.GetDeviceTwinForDevice(test_device_id) + + self.assertEqual(twin.properties.reported["deviceUpdate"]["agent"]["state"],0) + + # + # In case of a succeeded deployment we need to clean up the resources we created. + # mainly the deletion of the deployment + # + time.sleep(retry_wait_time_in_seconds) + + # self.assertEqual(self.duTestHelper.StopDeployment(self.deploymentId,test_adu_group),200) + # time.sleep(retry_wait_time_in_seconds) + + # Once stopped we can delete the deployment + self.assertEqual(self.duTestHelper.DeleteDeployment(self.deploymentId,test_adu_group),204) + + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + # + # Create the IO pipe + # + out = io.BytesIO() + + # + # Exercise the TestCase and all the tests within it. + # + unittest.main(testRunner = xmlrunner.XMLTestRunner(output=out),failfast=False,buffer=False,catchbreak=False,exit=False) + + # + # Finally transform the output unto the X/JUnit XML file format + # + with open('./testresults/' + test_result_file_prefix + '-bundle-update-deployment-test.xml','wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/Multi-Component-Update.py b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/Multi-Component-Update.py new file mode 100644 index 000000000..227acab88 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/Multi-Component-Update.py @@ -0,0 +1,165 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner + +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/ubuntu-20.04-amd64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, test_mcu_deployment_id, test_connection_timeout_tries, retry_wait_time_in_seconds + +# +# Global Test Variables +# + +mcu_deployment_status_retries = 15 + +class AptDeploymentTest(unittest.TestCase): + + # + # Every test within a subclass of a unittest.TestCase to be run by unittest must have the prefix test_ + # + def test_AptDeployment(self): + + # + # The first step to any test is to create the test helper that allows us to make calls to both the DU account and the + # IotHub. In the pipeline the parameters to create the test helper are passed to the environment from which we can read them + # here. + # + # For testing it might be convenient to simply define these above and directly instantiate the object or if you're not planning + # on running the tests as apart of a unittest.TestCase you can simply pass them via the CLI to the python program + # and create the test helper using the helper method + # DuAutomatedTestConfigurationManager.FromCliArgs() + # + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + + # + # We retrieve the apt deployment id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.deploymentId = test_mcu_deployment_id + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0,test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus,"Connected") + # + # For every deployment we need to create an Update Id. For all tests running in the DeviceUpdate for IotHub Test Automation + # pipeline we use Fabrikaam as a provider and Vaccuum as the name for the update. You'll see the same values supplied as the + # manufacturer and model in the devicesetup.py script. + # + self.deploymentUpdateId = UpdateId(provider="Contoso1", name="Virtual", version="6.0") + + # + # The assumption is that we've already imported the update targeting the manufacturer and + # model for the device created in the devicesetup.py. Now we have to create the deployment for + # the device using the update-id for the update + # + # Note: ALL UPDATES SHOULD BE UPLOADED TO THE TEST AUTOMATION HUB NOTHING SHOULD BE IMPORTED AT TEST TIME + # + status_code = self.duTestHelper.StartDeploymentForGroup(deploymentId=self.deploymentId,groupName=test_adu_group,updateId=self.deploymentUpdateId) + + self.assertEqual(status_code,200) + + # + # Once we've started the deployment for the group we can then query for the status of the deployment + # to see if/when the deployment finishes. We expect a timeout of the configured amount of time + # + deploymentStatus = None + + + for i in range(0,mcu_deployment_status_retries): + deploymentStatus = self.duTestHelper.GetDeploymentStatusForGroup(self.deploymentId,test_adu_group) + + # + # If we see all the devices have completed the deployment then we can exit early + # + if (len(deploymentStatus.subgroupStatuses) != 0): + if (deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount == 1): + break + time.sleep(retry_wait_time_in_seconds) + + + + # + # Should only be one device group in the deployment + # + self.assertEqual(len(deploymentStatus.subgroupStatuses),1) + + # + # Devices in the group should have succeeded + # + self.assertEqual(deploymentStatus.subgroupStatuses[0].totalDevices,deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount) + + # Sleep to give time for changes to propagate and for DU to switch it's state back to idle + time.sleep(retry_wait_time_in_seconds) + + # + # Once we've checked that we've reached the proper outcome for the + # deployment we need to check that the device itself has reported it + # is back in the idle state. + # + twin = self.duTestHelper.GetDeviceTwinForDevice(test_device_id) + + self.assertEqual(twin.properties.reported["deviceUpdate"]["agent"]["state"],0) + + # + # In case of a succeeded deployment we need to clean up the resources we created. + # mainly the deletion of the deployment + # + time.sleep(retry_wait_time_in_seconds) + + # self.assertEqual(self.duTestHelper.StopDeployment(self.deploymentId,test_adu_group),200) + # time.sleep(retry_wait_time_in_seconds) + + # Once stopped we can delete the deployment + self.assertEqual(self.duTestHelper.DeleteDeployment(self.deploymentId,test_adu_group),204) + + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + # + # Create the IO pipe + # + out = io.BytesIO() + + # + # Exercise the TestCase and all the tests within it. + # + unittest.main(testRunner = xmlrunner.XMLTestRunner(output=out),failfast=False,buffer=False,catchbreak=False,exit=False) + + # + # Finally transform the output unto the X/JUnit XML file format + # + with open('./testresults/' + test_result_file_prefix + '-mcu-deployment-test.xml','wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/add_device_to_adu_group.py b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/add_device_to_adu_group.py new file mode 100644 index 000000000..d6d18ce50 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/add_device_to_adu_group.py @@ -0,0 +1,60 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner +from xmlrunner.extra.xunit_plugin import transform +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager + +sys.path.append('./scenarios/ubuntu-20.04-amd64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, test_connection_timeout_tries, retry_wait_time_in_seconds + +class AddDeviceToGroupTest(unittest.TestCase): + + def test_AddDeviceToGroup(self): + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0, test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus, "Connected") + + # + # Before we can run the deployment we need to add the ADUGroup test_adu_group tag to the + # the device before we make the ADUGroup which we can then use to target the deployment + # to the device. + # + success = self.duTestHelper.AddDeviceToGroup(test_device_id, test_adu_group) + + self.assertTrue(success) + time.sleep(retry_wait_time_in_seconds) + + +if (__name__ == "__main__"): + + out = io.BytesIO() + + unittest.main(testRunner=xmlrunner.XMLTestRunner(output=out), + failfast=False, buffer=False, catchbreak=False, exit=False) + + with open('./testresults/' + test_result_file_prefix + '-add-device-to-adu-group-test.xml', 'wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/apt_deployment.py b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/apt_deployment.py new file mode 100644 index 000000000..f623983ff --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/apt_deployment.py @@ -0,0 +1,156 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner + +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import UpdateId +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/ubuntu-20.04-amd64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, test_apt_deployment_id, test_connection_timeout_tries, retry_wait_time_in_seconds + +# +# Global Test Variables +# +apt_deployment_status_retries = 15 + +class AptDeploymentTest(unittest.TestCase): + + # + # Every test within a subclass of a unittest.TestCase to be run by unittest must have the prefix test_ + # + def test_AptDeployment(self): + + # + # The first step to any test is to create the test helper that allows us to make calls to both the DU account and the + # IotHub. In the pipeline the parameters to create the test helper are passed to the environment from which we can read them + # here. + # + # For testing it might be convenient to simply define these above and directly instantiate the object or if you're not planning + # on running the tests as apart of a unittest.TestCase you can simply pass them via the CLI to the python program + # and create the test helper using the helper method + # DuAutomatedTestConfigurationManager.FromCliArgs() + # + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + # + # We retrieve the apt deployment id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.deploymentId = test_apt_deployment_id + self.deploymentUpdateId = UpdateId(provider="Contoso1", name="Virtual", version="1.0.2") + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0, test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus, "Connected") + + # + # The assumption is that we've already imported the update targeting the manufacturer and + # model for the device created in the devicesetup.py. Now we have to create the deployment for + # the device using the update-id for the update + # + status_code = self.duTestHelper.StartDeploymentForGroup( + deploymentId=self.deploymentId, groupName=test_adu_group, updateId=self.deploymentUpdateId) + + self.assertEqual(status_code, 200) + + # + # Once we've started the deployment for the group we can then query for the status of the deployment + # to see if/when the deployment finishes. We expect a timeout of the configured amount of time + # + deploymentStatus = None + + for i in range(0, apt_deployment_status_retries): + deploymentStatus = self.duTestHelper.GetDeploymentStatusForGroup( + self.deploymentId, test_adu_group) + + # + # If we see all the devices have completed the deployment then we can exit early + # + if (len(deploymentStatus.subgroupStatuses) != 0): + if (deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount == 1): + break + time.sleep(retry_wait_time_in_seconds) + + # + # Should only be one device group in the deployment + # + self.assertEqual(len(deploymentStatus.subgroupStatuses),1) + + # + # Devices in the group should have succeeded + # + self.assertEqual(deploymentStatus.subgroupStatuses[0].totalDevices,deploymentStatus.subgroupStatuses[0].devicesCompletedSucceededCount) + + # Sleep to give time for changes to propagate and for DU to switch it's state back to idle + time.sleep(retry_wait_time_in_seconds) + + # + # Once we've checked that we've reached the proper outcome for the + # deployment we need to check that the device itself has reported it + # is back in the idle state. + # + twin = self.duTestHelper.GetDeviceTwinForDevice(test_device_id) + + self.assertEqual(twin.properties.reported["deviceUpdate"]["agent"]["state"],0) + + # + # In case of a succeeded deployment we need to clean up the resources we created. + # mainly the deletion of the deployment + # + time.sleep(retry_wait_time_in_seconds) + + # self.assertEqual(self.duTestHelper.StopDeployment(self.deploymentId, test_adu_group), 200) + # time.sleep(retry_wait_time_in_seconds) + + # Once stopped we can delete the deployment + self.assertEqual(self.duTestHelper.DeleteDeployment(self.deploymentId, test_adu_group), 204) + + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + # + # Create the IO pipe + # + out = io.BytesIO() + + # + # Exercise the TestCase and all the tests within it. + # + unittest.main(testRunner=xmlrunner.XMLTestRunner(output=out), + failfast=False, buffer=False, catchbreak=False, exit=False) + + # + # Finally transform the output unto the X/JUnit XML file format + # + with open('./testresults/' + test_result_file_prefix + '-apt-deployment-test.xml', 'wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/delete_device.py b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/delete_device.py new file mode 100644 index 000000000..30b0176f0 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/delete_device.py @@ -0,0 +1,43 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper +from testingtoolkit import DuAutomatedTestConfigurationManager +from xmlrunner.extra.xunit_plugin import transform + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/ubuntu-20.04-amd64/') +from scenario_definitions import test_device_id, test_adu_group, test_result_file_prefix, retry_wait_time_in_seconds + +class DeleteDeviceAndGroup(unittest.TestCase): + + def test_DeleteTestDeviceAndGroup(self): + configManager = DuAutomatedTestConfigurationManager.FromOSEnvironment() + + duTestHelper = configManager.CreateDeviceUpdateTestHelper() + + self.assertTrue(duTestHelper.DeleteDevice(test_device_id)) + + time.sleep(retry_wait_time_in_seconds*5) + + self.assertEqual(duTestHelper.DeleteADUGroup(test_adu_group), 204) + +if (__name__ == "__main__"): + out = io.BytesIO() + + unittest.main(testRunner=xmlrunner.XMLTestRunner(output=out), + failfast=False, buffer=False, catchbreak=False, exit=False) + + with open('./testresults/' + test_result_file_prefix + '-delete-device-test.xml', 'wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/diagnostics.py b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/diagnostics.py new file mode 100644 index 000000000..95ce405cd --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/diagnostics.py @@ -0,0 +1,95 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import io +import sys +import time +import unittest +import xmlrunner + +# Import testingtoolkit module from parent directory +# sys.path.append('../') +# from testingtoolkit import DeviceUpdateTestHelper +# from testingtoolkit import UpdateId +# from azure.identity import DefaultAzureCredential +sys.path.append('./scenarios/') +from xmlrunner.extra.xunit_plugin import transform +from testingtoolkit import DuAutomatedTestConfigurationManager +from testingtoolkit import DeploymentStatusResponse +from testingtoolkit import UpdateId +from testingtoolkit import DeviceUpdateTestHelper + +# Note: the intention is that this script is called like: +# python ./scenarios//testscript.py +sys.path.append('./scenarios/ubuntu-20.04-amd64/') +from scenario_definitions import test_device_id, test_result_file_prefix, test_operation_id, test_connection_timeout_tries, retry_wait_time_in_seconds + +diagnostics_operation_status_retries = 15 +class DiagnosticsTest(unittest.TestCase): + def test_diagnostics(self): + self.aduTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + self.duTestHelper = self.aduTestConfig.CreateDeviceUpdateTestHelper() + + # + # We retrieve the apt deployment id to be used by the script from the scenario definitions file. It's important to keep + # things like the deployment id, device-id, module-id, and other scenario level definitions that might effect other + # tests in the scenario_definitions.py file. + # + self.operationId = test_operation_id + + # + # Before anything else we need to wait and check the device connection status + # We expect the device to connect within the configured amount of time of setting up the device in the step previous + # + connectionStatus = "" + for i in range(0, test_connection_timeout_tries): + connectionStatus = self.duTestHelper.GetConnectionStatusForDevice(test_device_id) + if (connectionStatus == "Connected"): + break + time.sleep(retry_wait_time_in_seconds) + + self.assertEqual(connectionStatus, "Connected") + + # + # The assumption is that we've already configured diagnostics in test account and already run a apt deployment + # Start a diagnostics log collection operation + # 201 indicate the device diagnostics log collection operation is created + # + diagnosticResponseStatus_code = self.duTestHelper.RunDiagnosticsOnDeviceOrModule( + test_device_id, operationId=self.operationId, description="") + + self.assertEqual(diagnosticResponseStatus_code, 201) + + # + # Get device diagnostics log collection operatio status and device status + # device status represent Log upload status, operatin status represent background operation status + # + for i in range(0, diagnostics_operation_status_retries): + diagnosticJsonStatus = self.duTestHelper.GetDiagnosticsLogCollectionStatus(self.operationId) + + if (diagnosticJsonStatus.status == "Succeeded"): + break + time.sleep(retry_wait_time_in_seconds) + + + self.assertEqual(diagnosticJsonStatus.status, "Succeeded") + self.assertEqual(len(diagnosticJsonStatus.deviceStatus),1) + self.assertEqual(diagnosticJsonStatus.deviceStatus[0].status, "Succeeded") + self.assertEqual(diagnosticJsonStatus.deviceStatus[0].resultCode, "200") + + +# +# Below is the section of code that uses the above class to run the test. It starts by running the test, capturing the output, transforming +# the output from Python Unittest to X/JUnit format. Then the function exports the values to an xml file in the testresults directory which is +# then uploaded by the Azure Pipelines PostTestResults job +# +if (__name__ == "__main__"): + out = io.BytesIO() + + unittest.main(testRunner=xmlrunner.XMLTestRunner(output=out), + failfast=False, buffer=False, catchbreak=False, exit=False) + + with open('./testresults/' + test_result_file_prefix + '-diagnostic-test.xml', 'wb') as report: + report.write(transform(out.getvalue())) diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/scenario_definitions.py b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/scenario_definitions.py new file mode 100644 index 000000000..2a66abff5 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/scenario_definitions.py @@ -0,0 +1,41 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +# +# These are the definitions used by the scenario to complete it's operations. It is used by all the TestCases held within +# the project. +# +import sys +import uuid + +sys.path.append('./') +from testingtoolkit import UpdateId + +# +# Base Device Definitions +# +test_device_id = "ubuntu20.04-amd64-deployment-test-device" + +test_adu_group = "Ubuntu2004AMD64TestGroup" + +test_apt_deployment_id = str(uuid.uuid4()) + +test_operation_id = str(uuid.uuid4()).replace('-', '') + +test_mcu_deployment_id = str(uuid.uuid4()) + +test_bundle_update_deployment_id = str(uuid.uuid4()) + +test_result_file_prefix = 'ubuntu20.04-amd64' + +test_connection_timeout_tries = 10 + +# For all retries this is the total amount of time we wait for all operations +retry_wait_time_in_seconds = 60 + + +# +# Other variables that should be available to all tests in this scenario may be added here +# diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/vm_setup/devicesetup.py b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/vm_setup/devicesetup.py new file mode 100644 index 000000000..c731c86c0 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/vm_setup/devicesetup.py @@ -0,0 +1,84 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import json +import sys + +# Note: the intention is that this script is called like: +# python .//vm-setup/devicesetup.py +sys.path.append('./scenarios/') +from testingtoolkit import DeviceUpdateTestHelper,DuAutomatedTestConfigurationManager + +# Note: the intention is that this script is called like: +# python ./scenarios//.py +sys.path.append('./scenarios/ubuntu-20.04-amd64/') +from scenario_definitions import test_device_id + + +def main(): + duTestConfig = DuAutomatedTestConfigurationManager.FromOSEnvironment() + # + # Instantiate the Wrapper Class from the config manager + # + duTestWrapper = duTestConfig.CreateDeviceUpdateTestHelper() + + # + # Delete the device if it exists, we don't care if we fail here. + # + duTestWrapper.DeleteDevice(test_device_id) + # + # Create the Device + # + connectionString = duTestWrapper.CreateDevice(test_device_id, isIotEdge=True) + + if (len(connectionString) == 0): + print_error("Failed to create the device in the IotHub") + sys.exit(1) + + + # + # Create the du-config.json JSON Object + # + duConfigJson = { + "schemaVersion": "1.1", + "aduShellTrustedUsers": [ + "adu", + "do" + ], + "iotHubProtocol": "mqtt", + "manufacturer": "contoso", + "model": "virtual-vacuum-v2", + "agents": [ + { + "name": "main", + "runas": "adu", + "connectionSource": { + "connectionType": "string", + "connectionData": connectionString + }, + "manufacturer": "contoso", + "model": "virtual-vacuum-v2" + } + ] + } + + # + # Write the configuration out to disk so we can install it as a part of the test + # + + with open('du-config.json', 'w',encoding="utf-8") as jsonFile: + configJson = json.dumps(duConfigJson) + jsonFile.write(configJson) + + +if __name__ == '__main__': + main() + +def print_error(msg): + print(msg, file=sys.stderr) + +# +# Output is now a newly created device and a du-config.json file that will work with it +# diff --git a/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/vm_setup/setup.sh b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/vm_setup/setup.sh new file mode 100644 index 000000000..b70212978 --- /dev/null +++ b/azurepipelines/e2e_test/scenarios/ubuntu-20.04-amd64/vm_setup/setup.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +# Setup Script for Test: Ubuntu-18.04-amd64-APT-deployment +# +# Should be run on the Virtual Machine being provisioned for the work. + +# This file should be included in an archive that is created at provisioning time +# with the following structure: +# setup.sh (this file) +# deviceupdate-package.deb (debian package under test) +# +# where the archive is named: +# testsetup.tar.gz +# The archive is manually mounted to the file system at: +# ~/testsetup.tar.gz +# Once the script has been extracted we'll be running from +# ~ while the script itself is at +# ~/testsetup/setup.sh +# So we need to localize the path to that. +# +# +# Install the Microsoft APT repository +# + +wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb +sudo dpkg -i packages-microsoft-prod.deb +rm packages-microsoft-prod.deb + +# +# Update the apt repositories +# +sudo apt-get update + +# +# Install Device Update Dependencies from APT +# +# Note: If there are other dependencies tht need to be installed via APT or other means they should +# be added here. You might be installing iotedge, another package to setup for a deployment test, or +# anything else. + +# Handle installing DO from latest build instead of packages.microsoft.com +wget https://github.com/microsoft/do-client/releases/download/v1.0.0/ubuntu2004_x64-packages.tar -O ubuntu20_x64-packages.tar +tar -xvf ubuntu20_x64-packages.tar +sudo apt-get install -y ./deliveryoptimization-agent_1.0.0_amd64.deb ./deliveryoptimization-plugin-apt_0.5.1_amd64.deb ./libdeliveryoptimization_1.0.0_amd64.deb + +# +# Install the Device Update Artifact Under Test +# +sudo apt-get install -y ./testsetup/deviceupdate-package.deb + +# +# Install the du-config.json so the device can be provisioned +# +# Note: In other setup scripts there may be more info that needs to +# go here. For instance you might have the config.toml for IotEdge, +# another kind of diagnostics file, or other kinds of data +# this is the area where such things can be added + +sudo cp ./testsetup/du-config.json /etc/adu/du-config.json + +mkdir ~/adu_srcs/ + +tar -xvf ./testsetup/adu_srcs_repo.tar.gz -C ~/adu_srcs/ + +sudo mkdir -p ~/demo/demo-devices/contoso-devices + +cd ~/adu_srcs/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo || exit + +chmod 755 ./tools/reset-demo-components.sh + +#copy components-inventory.json and adds it to the /usr/local/contoso-devices folder +sudo cp -a ./demo-devices/contoso-devices/. ~/demo/demo-devices/contoso-devices/ + +sh ./tools/reset-demo-components.sh + +#registers the extension +sudo /usr/bin/AducIotAgent -l 2 --extension-type componentEnumerator --register-extension /var/lib/adu/extensions/sources/libcontoso_component_enumerator.so + + +# +# Restart the deviceupdate-agent.service +# +# Note: We expect that everything should be setup for the deviceupdate agent at this point. Once +# we restart the agent we expect it to be able to boot back up and connect to the IotHub. Otherwise +# this test will be considered a failure. + +sudo systemctl restart deviceupdate-agent.service diff --git a/azurepipelines/e2e_test/terraform/host/DeviceUpdateHost.tf b/azurepipelines/e2e_test/terraform/host/DeviceUpdateHost.tf new file mode 100644 index 000000000..0c231624e --- /dev/null +++ b/azurepipelines/e2e_test/terraform/host/DeviceUpdateHost.tf @@ -0,0 +1,194 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +# Configure the Microsoft Azure Provider +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~>2.0" + } + } +} +provider "azurerm" { + features {} + + subscription_id = var.subscription_id + tenant_id = var.tenant_id + + # Must create a SP and plumb id/secret below using scripts/create-sp-terraform.sh + # if manually running from a machine these must be generated and plumbed through + client_id = var.client_id + client_secret = var.client_secret +} + +# Create public IPs +resource "azurerm_public_ip" "deviceupdatepublicip" { + name = "myPublicIP-${var.vm_name}" + resource_group_name = var.resource_group_name + location = "eastus" + allocation_method = "Dynamic" + + tags = { + environment = var.environment_tag + } +} + +# Create virtual network +resource "azurerm_virtual_network" "deviceupdatenetwork" { + name = "myVnet-${var.vm_name}" + address_space = ["10.0.0.0/16"] + resource_group_name = var.resource_group_name + location = "eastus" + + tags = { + environment = var.environment_tag + } +} + +# Create subnet +resource "azurerm_subnet" "deviceupdatesubnet" { + name = "mySubnet-${var.vm_name}" + resource_group_name = var.resource_group_name + virtual_network_name = azurerm_virtual_network.deviceupdatenetwork.name + address_prefixes = ["10.0.1.0/24"] +} + +# Create Network Security Group and rule +resource "azurerm_network_security_group" "deviceupdatensg" { + name = "myNetworkSecurityGroup-${var.vm_name}" + location = "eastus" + resource_group_name = var.resource_group_name + + + security_rule { + name = "SSH" + priority = 1001 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "22" + source_address_prefix = "*" + destination_address_prefix = "*" + } + + tags = { + environment = var.environment_tag + } +} + +# Create network interface +resource "azurerm_network_interface" "deviceupdatenic" { + name = "myNIC-${var.vm_name}" + location = "eastus" + resource_group_name = var.resource_group_name + + ip_configuration { + name = "myNicConfiguration-${var.vm_name}" + subnet_id = azurerm_subnet.deviceupdatesubnet.id + private_ip_address_allocation = "Dynamic" + public_ip_address_id = azurerm_public_ip.deviceupdatepublicip.id + } + + tags = { + environment = var.environment_tag + } +} + +# Connect the security group to the network interface +resource "azurerm_network_interface_security_group_association" "example" { + network_interface_id = azurerm_network_interface.deviceupdatenic.id + network_security_group_id = azurerm_network_security_group.deviceupdatensg.id +} + +# Create an SSH key - export to PKCS#8 (Dependency: OpenSSL) +resource "tls_private_key" "deviceupdate_ssh_key" { + algorithm = "RSA" + rsa_bits = 4096 +} + +# Add secret into keyvault +resource "azurerm_key_vault_secret" "vm_ssh_secret" { + name = "${var.resource_group_name}-${replace(var.vm_name, ".", "")}" + value = tls_private_key.deviceupdate_ssh_key.private_key_pem + key_vault_id = var.key_vault_id + # expire in 30 days + expiration_date = timeadd(timestamp(), "720h") +} +# Create virtual machine +resource "azurerm_linux_virtual_machine" "deviceupdatevm" { + name = "myVM-${var.vm_name}" + location = "eastus" + resource_group_name = var.resource_group_name + network_interface_ids = [azurerm_network_interface.deviceupdatenic.id] + size = var.vm_size + + os_disk { + name = "myOsDisk-${var.vm_name}" + caching = "ReadWrite" + storage_account_type = "Premium_LRS" + } + + source_image_reference { + publisher = var.image_publisher + offer = var.image_offer + sku = var.image_sku + version = var.image_version + } + + computer_name = "myvm-${var.vm_name}" + admin_username = "azureuser" + disable_password_authentication = true + + admin_ssh_key { + username = "azureuser" + public_key = tls_private_key.deviceupdate_ssh_key.public_key_openssh + } + + connection { + host = self.public_ip_address + type = "ssh" + port = 22 + user = "azureuser" + private_key = tls_private_key.deviceupdate_ssh_key.private_key_pem + timeout = "1m" + } + + # Use remote-exec to install + run + foregroud to not block terraform job + # expects there to be a tarball for the du setup at /tmp/testsetup.tar.gz + provisioner "file" { + source = var.test_setup_tarball + destination = "/tmp/testsetup.tar.gz" + } + + # + # Changes to provide the setup information should go here + # + provisioner "remote-exec" { + inline = [ + "sudo apt update", + var.vm_du_tarball_script + ] + } + + tags = { + environment = var.environment_tag + } +} + +resource "azurerm_dev_test_global_vm_shutdown_schedule" "vm_shutdown_schedule" { + virtual_machine_id = azurerm_linux_virtual_machine.deviceupdatevm.id + location = "eastus" + enabled = true + + // Shutdown 60-mins after creation time for cost savings + daily_recurrence_time = formatdate("hhmm", timeadd(timestamp(), "180m")) + timezone = "UTC" + + notification_settings { + enabled = false + } +} diff --git a/azurepipelines/e2e_test/terraform/host/variables.tf b/azurepipelines/e2e_test/terraform/host/variables.tf new file mode 100644 index 000000000..3fec127a0 --- /dev/null +++ b/azurepipelines/e2e_test/terraform/host/variables.tf @@ -0,0 +1,51 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +# Local testing - must create a SP and plumb client_id/client_secret below using scripts/create-sp-terraform.sh +variable "client_id" {} +variable "client_secret" {} +variable "subscription_id" {} +variable "tenant_id" {} +variable "key_vault_id" {} +variable "resource_group_name" {} + +# Tag used for managing generated resources in infrastructure tear down +variable "environment_tag" { + default = "du-e2etest" +} + +variable "vm_name" { + default = "autest-device" +} + +variable "vm_size" { + default = "Standard_DS1_v2" +} + +variable "image_publisher" { + default = "Canonical" +} + +variable "image_offer" { + default = "UbuntuServer" +} + +variable "image_sku" { + default = "18.04-LTS" +} + +variable "image_version" { + default = "latest" +} + +variable "test_setup_tarball"{ + default = "" +} + +variable "vm_du_tarball_script" { + default = "" +} + +# Other variables should be put here diff --git a/azurepipelines/e2e_test/terraform/resource_group/ResourceGroup.tf b/azurepipelines/e2e_test/terraform/resource_group/ResourceGroup.tf new file mode 100644 index 000000000..8714f94d6 --- /dev/null +++ b/azurepipelines/e2e_test/terraform/resource_group/ResourceGroup.tf @@ -0,0 +1,36 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~>2.0" + } + } +} +provider "azurerm" { + features {} + + subscription_id = var.subscription_id + tenant_id = var.tenant_id + + # Local testing - must create a SP and plumb id/secret below using scripts/create-sp-terraform.sh + client_id = var.client_id + client_secret = var.client_secret +} + +resource "random_pet" "rg-name" { + prefix = var.resource_group_name_prefix +} + +resource "azurerm_resource_group" "du-resource-group" { + name = random_pet.rg-name.id + location = var.resource_group_location + + tags = { + environment = var.environment_tag + } +} diff --git a/azurepipelines/e2e_test/terraform/resource_group/output.tf b/azurepipelines/e2e_test/terraform/resource_group/output.tf new file mode 100644 index 000000000..56e9e29d6 --- /dev/null +++ b/azurepipelines/e2e_test/terraform/resource_group/output.tf @@ -0,0 +1,8 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +output "du_resource_group_name" { + value = azurerm_resource_group.du-resource-group.name +} diff --git a/azurepipelines/e2e_test/terraform/resource_group/variables.tf b/azurepipelines/e2e_test/terraform/resource_group/variables.tf new file mode 100644 index 000000000..35a30ed7e --- /dev/null +++ b/azurepipelines/e2e_test/terraform/resource_group/variables.tf @@ -0,0 +1,26 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +# Local testing - must create a SP and plumb client_id/client_secret below +variable "client_id" {} +variable "client_secret" {} +variable "subscription_id" {} +variable "tenant_id" {} + +variable resource_group_name_prefix { + default = "du-e2etest-rg" + description = "Concatenated with a random two word petname to form the Resource Group name for the deployment" +} + + +variable "resource_group_location" { + default = "eastus" + description = "Location of the resource group." +} + +variable "environment_tag" { + default = "du-e2etest" + description = "Tag used for managing generated resources for infrastructure tear down" +} diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index 6a7f5d3f7..f8aab588a 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required (VERSION 3.5) install ( - FILES adu-agent.service + FILES deviceupdate-agent.service DESTINATION ${CMAKE_INSTALL_LIBDIR}/systemd/system PERMISSIONS OWNER_READ diff --git a/daemon/adu-agent.service b/daemon/deviceupdate-agent.service similarity index 86% rename from daemon/adu-agent.service rename to daemon/deviceupdate-agent.service index c5c226158..5bb46bd71 100644 --- a/daemon/adu-agent.service +++ b/daemon/deviceupdate-agent.service @@ -10,7 +10,7 @@ RestartSec=5 User=adu Group=adu # systemd will try to start the ADU executable 5 times and then give up. -# We can check logs with journalctl -f -u adu-agent.service +# We can check logs with journalctl -f -u deviceupdate-agent.service # Set log verbosity level to 'Debug' and enable IoT Hub tracing. ExecStart=/usr/bin/AducIotAgent -l 0 -e diff --git a/daemon/install.sh b/daemon/install.sh index 85f70be5b..406bdff85 100644 --- a/daemon/install.sh +++ b/daemon/install.sh @@ -3,7 +3,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Installs the adu-agent daemon and performs necessary setup/configuration. +# Installs the deviceupdate-agent daemon and performs necessary setup/configuration. # This script is meant to only be called from CMake as part of the install target. # Any error should exit the script (sort of) @@ -30,7 +30,7 @@ adu_user=adu adu_group=adu # Note: DO user and group are created by deliveryoptimization-agent Debian package, -# which is one of the dependencies declared in adu-agent control file. +# which is one of the dependencies declared in deviceupdate-agent control file. # We are assuming that both DO user and group currently exist at this point. # The user that the DO Agent daemon runs as. @@ -40,11 +40,12 @@ do_user='do' sample_du_config=$( cat << END_OF_JSON { - "schemaVersion": "1.0", + "schemaVersion": "1.1", "aduShellTrustedUsers": [ "adu", "do" ], + "iotHubProtocol": "mqtt", "manufacturer": , "model": , "agents": [ @@ -99,7 +100,7 @@ add_adu_user_and_group() { register_daemon() { systemctl daemon-reload - systemctl enable adu-agent + systemctl enable deviceupdate-agent } setup_dirs_and_files() { diff --git a/docs/agent-reference/configurations-file.md b/docs/agent-reference/configurations-file.md deleted file mode 100644 index 310ab8e66..000000000 --- a/docs/agent-reference/configurations-file.md +++ /dev/null @@ -1,9 +0,0 @@ -# Configuration Files - -## General Description - -Please find the description in [device-update-configuration-file](https://docs.microsoft.com/en-us/azure/iot-hub-device-update/device-update-configuration-file). - -## Migrate from adu-conf.txt to du-config.json - -Please find the details on migration of the configuration file in [upgrade-guide.md](./upgrade-guide.md) diff --git a/docs/agent-reference/device-diagnostic-service.md b/docs/agent-reference/device-diagnostic-service.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/agent-reference/device-update-agent-extended-result-codes.md b/docs/agent-reference/device-update-agent-extended-result-codes.md index 23dac43e4..4c141d4e7 100644 --- a/docs/agent-reference/device-update-agent-extended-result-codes.md +++ b/docs/agent-reference/device-update-agent-extended-result-codes.md @@ -4,650 +4,53 @@ The followings are Result Code that visible in the Device or Module Twin: -| Result Code | C Macro | -|---|---| -| 0 | ADUC_Result_Failure | -| -1 | ADUC_Result_Failure_Cancelled | -| 500 | ADUC_Result_Download_Success | -| 501 | ADUC_Result_Download_InProgress | -| 502 | ADUC_Result_Download_Skipped_FileExists | -| 503 | ADUC_Result_Download_Skipped_UpdateAlreadyInstalled | -| 504 | ADUC_Result_Download_Skipped_NoMatchingComponents | -| 600 | ADUC_Result_Install_Success | -| 601 | ADUC_Result_Install_InProgress | -| 603 | ADUC_Result_Install_Skipped_UpdateAlreadyInstalled | -| 604 | ADUC_Result_Install_Skipped_NoMatchingComponents | -| 700 | ADUC_Result_Apply_Success | -| 800 | ADUC_Result_Cancel_Success | -| 801 | ADUC_Result_Cancel_UnableToCancel | +| Result Code | C Macro | +| ----------- | --------------------------------------------------- | +| 0 | ADUC_Result_Failure | +| -1 | ADUC_Result_Failure_Cancelled | +| 500 | ADUC_Result_Download_Success | +| 501 | ADUC_Result_Download_InProgress | +| 502 | ADUC_Result_Download_Skipped_FileExists | +| 503 | ADUC_Result_Download_Skipped_UpdateAlreadyInstalled | +| 504 | ADUC_Result_Download_Skipped_NoMatchingComponents | +| 600 | ADUC_Result_Install_Success | +| 601 | ADUC_Result_Install_InProgress | +| 603 | ADUC_Result_Install_Skipped_UpdateAlreadyInstalled | +| 604 | ADUC_Result_Install_Skipped_NoMatchingComponents | +| 700 | ADUC_Result_Apply_Success | +| 800 | ADUC_Result_Cancel_Success | +| 801 | ADUC_Result_Cancel_UnableToCancel | +| 900 | ADUC_Result_IsInstalled_Installed | +| 901 | ADUC_Result_IsInstalled_NotInstalled | +| 1000 | ADUC_Result_Backup_Success | +| 1001 | ADUC_Result_Backup_Success_Unsupported | +| 1002 | ADUC_Result_Backup_InProgress | +| 1100 | ADUC_Result_Restore_Success | +| 1101 | ADUC_Result_Restore_Success_Unsupported | +| 1102 | ADUC_Result_Restore_InProgress | See [adu_core.h](../../src/adu/types/adu_core.h) for more detail. ## Extended Result Code Structure (32 bits) - -```text - 0 00 00000 Total 4 bytes (32 bits) - - -- ----- - | | | - | | | - | | +--------- Error code (20 bits) - | | - | +------------- Component/Area code (8 bits) - | - +--------------- Facility code (4 bits) - ``` -## Decoding an Error Code - -Find the facility code ( [**?**] ## ##### ) ? - -| [0](#result-codes-from-common-or-unknown-facility-facility-0) | [1](#result-codes-from-lower-platform-layer-facility-1) | [2](#result-codes-from-upper-layer-facility-2) | [3](#update-content-handler-result-codes-facility-3) | [4](#content-downloader-result-codes-facility-4) | 5 | 6 | [7](#components-enumerator-result-codes-facility-7) | [8](#result-codes-from-helper-functions-or-shared-utilities-facility-8) | 9 | A | B | C | [D](#additional-delivery-optimization-downloader-result-codes) | E | F - -## Facility Codes - -```text -typedef enum tagADUC_Facility -{ - /*indicates errors from unknown sources*/ - ADUC_FACILITY_UNKNOWN = 0x0, - - /*indicates errors from the lower layer*/ - ADUC_FACILITY_LOWERLAYER = 0x1, - - /*indicates errors from the upper layer. */ - ADUC_FACILITY_UPPERLAYER = 0x2, - - /*indicates errors from UPDATE CONTENT HANDLER Extension. */ - ADUC_FACILITY_EXTENSION_UPDATE_CONTENT_HANDLER = 0x3, - - /*indicates errors from CONTENT_DOWNLOADER Extension. */ - ADUC_FACILITY_EXTENSION_CONTENT_DOWNLOADER = 0x4, - - /*indicates errors from COMMUNICATION PROVIDER Extension. */ - ADUC_FACILITY_EXTENSION_COMMUNICATION_PROVIDER = 0x5, - - /*indicates errors from LOG PROVIDER Extension. */ - ADUC_FACILITY_EXTENSION_LOG_PROVIDER = 0x6, - - /*indicates errors from COMPONENT ENUMERATOR Extension. */ - ADUC_FACILITY_EXTENSION_COMPONENT_ENUMERATOR = 0x7, - - /*indicates errors from utility functions. */ - ADUC_FACILITY_UTILITY = 0x8, - - ADUC_FACILITY_UNUSED_9 = 0x9, - ADUC_FACILITY_UNUSED_A = 0xa, - ADUC_FACILITY_NSUSED_B = 0xb, - ADUC_FACILITY_UNUSED_C = 0xc, - - // NOTE: DO retuns HRESULT-like code. To minimize data lost, we assigned it's - /*indicates errors from Delivery Optimization. */ - ADUC_FACILITY_DELIVERY_OPTIMIZATION = 0xd, - - ADUC_FACILITY_UNUSED_E = 0xe, - ADUC_FACILITY_UNUSED_F = 0xf, - -} ADUC_Facility - -``` - -### Extended Result Codes from Common or Unknown facility (facility #0) - -0x0 ## ##### - -```text - 0 00 00000 Total 4 bytes (32 bits) - - -- ----- - | | | - | | | - | | +--------- Error code (20 bits) - | | - | +------------- (Always 00) - | - +--------------- Facility code (4 bits) - ``` - -###### POSIX 2001 Standard ErrNos - -Note: Errno Values taken from /usr/include/asm-generic/errno-base.h and /usr/include/asm-generic/errno.h - -See "man 3 errno" manpage for more details. - - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x00000001 | ADUC_ERC_VADUC_ERC_NOTPERMITTEDALIDATION_FILE_HASH_IS_EMPTY | -| 0x0000000C | ADUC_ERC_NOMEM (12) | -| 0x00000083 | ADUC_ERC_NOTRECOVERABLE (131) | - -### Extended Result Codes from the Lower (platform) Layer (facility #1) - -0x100 ##### - -```text - 1 00 00000 Total 4 bytes (32 bits) - - -- ----- - | | | - | | | - | | +--------- Error code (20 bits) - | | - | +------------- Component (Update Content Handler) code (8 bits) - | 0x01 : Cryptographic Component - | 0x20 - 0xff : Available for customers and partners - | - +--------------- Facility code (4 bits) - ``` - -##### Macro for creating extended result codes - -```c -static inline ADUC_Result_t MAKE_ADUC_LOWERLAYER_EXTENDEDRESULTCODE(const int value) -{ - return MAKE_ADUC_EXTENDEDRESULTCODE(ADUC_FACILITY_LOWERLAYER, 0 /* non component-specific */, value); -} -``` - -###### Lower Layer Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x10000001 |ADUC_ERC_LOWERLEVEL_INVALID_UPDATE_ACTION | -| 0x10000002 |ADUC_ERC_LOWERLEVEL_UPDATE_MANIFEST_VALIDATION_INVALID_HASH | -| 0x10000003 |ADUC_ERC_LOWERLEVEL_GET_FILE_ENTITY_FAILURE | -| 0x10000005 |ADUC_ERC_LOWERLEVEL_SANDBOX_CREATE_FAILURE_NO_ADU_USER | -| 0x10000006 |ADUC_ERC_LOWERLEVEL_SANDBOX_CREATE_FAILURE_NO_ADU_GROUP | -| 0x10000014 |ADUC_ERC_LOWERLEVEL_WORKFLOW_INIT_ERROR_NULL_PARAM | - -### Extended Result Codes from the Upper Layer (facility #2) - -0x100 ##### - -```text - 2 00 00000 Total 4 bytes (32 bits) - - -- ----- - | | | - | | | - | | +--------- Error code (20 bits) - | | - | +------------- (Always 00) - | - +--------------- Facility code (4 bits) - ``` - -##### Macro for creating extended result codes - -```c -static inline ADUC_Result_t MAKE_ADUC_UPPERLAYER_EXTENDEDRESULTCODE(const int value) -{ - return MAKE_ADUC_EXTENDEDRESULTCODE(ADUC_FACILITY_UPPERLAYER, 0 /* non component-specific */, value); -} -``` - -###### Upper Layer Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x20000001 |ADUC_ERC_UPPERLEVEL_WORKFLOW_UPDATE_ACTION_UNEXPECTED_STATE | -| 0x20000002 |ADUC_ERC_UPPERLEVEL_WORKFLOW_INSTALL_ACTION_IN_UNEXPECTED_STATE | - -### Update Content Handler Result Codes (facility #3) - -```text - 3 00 00000 Total 4 bytes (32 bits) - - -- ----- - | | | - | | | - | | +--------- Error code (20 bits) - | | - | +------------- Component (Update Content Handler) code (8 bits) - | 0x00 - 0x1f : RESERVED for DU-Provided content handlers - | 0x20 - 0xff : Available for customers and partners - | - +--------------- Facility code (4 bits) - ``` - -#### Update Content Handler Lookup - -3 [**??**] ###### - -[00]() | [01](#swupdate-content-handler-result-codes-0x301) | [02](#apt-update-content-handler-result-codes-0x302) | [04](#steps-handler-result-codes-0x304) | [05](#script-handler-result-codes-0x305) | [20]() - -```text -typedef enum tagADUC_Content_Handler -{ - /*indicates a common error from any handler. */ - ADUC_CONTENT_HANDLER_COMMON = 0x00, - - /*indicates errors from SWUPDATE Update Handler. */ - ADUC_CONTENT_HANDLER_SWUPDATE = 0x01, - - /*indicates errors from APT Update Handler. */ - ADUC_CONTENT_HANDLER_APT = 0x02, - - /*indicates errors from Simulator Update Handler. */ - ADUC_CONTENT_HANDLER_SIMULATOR = 0x03, - - /*indicates errors from Steps Handler. */ - ADUC_CONTENT_HANDLER_STEPS = 0x04, - - /*indicates errors from Script Update Handler. */ - ADUC_CONTENT_HANDLER_SCRIPT = 0x05, - - /*indicates errors from Custom Update handlers. */ - ADUC_CONTENT_HANDLER_EXTERNAL = 0x20, -} ADUC_Content_Handler; -``` - -#### SWUpdate Content Handler Result Codes (0x301#####) - -##### Macro for creating extended result codes - -```c -static inline ADUC_Result_t MAKE_ADUC_SWUPDATE_HANDLER_EXTENDEDRESULTCODE(const int32_t value) -{ - return MAKE_ADUC_CONTENT_HANDLER_EXTENDEDRESULTCODE(ADUC_CONTENT_HANDLER_SWUPDATE, value); -} -``` - -###### General Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x30100001 |ADUC_ERC_SWUPDATE_HANDLER_EMPTY_VERSION | -| 0x30100002 | ADUC_ERC_SWUPDATE_HANDLER_PREPARE_FAILURE_UNKNOWN_UPDATE_VERSION | -| 0x30100003 | ADUC_ERC_SWUPDATE_HANDLER_PREPARE_FAILURE_WRONG_UPDATE_VERSION | -| 0x30100004 |ADUC_ERC_SWUPDATE_HANDLER_PREPARE_FAILURE_WRONG_FILECOUNT | -| 0x30100005 |ADUC_ERC_SWUPDATE_HANDLER_PREPARE_FAILURE_BAD_FILE_ENTITY | - -###### Download Related Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x30100101 |ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_FAILURE_UNKNOW_UPDATE_VERSION | -| 0x30100102 |ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_FAILURE_WRONG_UPDATE_VERSION | -| 0x30100103 |ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_FAILURE_WRONG_FILECOUNT -| 0x30100104 |ADUC_ERC_SWUPDATE_HANDLER_DOWNLOADE_BAD_FILE_ENTITY | - -###### Install Related Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x30100201 |ADUC_ERC_SWUPDATE_HANDLER_INSTALL_FAILURE_CANNOT_OPEN_WORKFOLDER | -| 0x30100102 |ADUC_ERC_SWUPDATE_HANDLER_INSTALL_FAILURE_BAD_FILE_ENTITY - -###### Apply Related Result Codes - -```text - None -``` - -###### Cancel Related Result Codes - -```text - None -``` - -###### IsInstalled Related Result Codes - -```text - None -``` - -###### Extended Result Codes from Child Processes (0x30101000 + exitCode) - -All result codes start from 0x30101000 are exit code(s) returned from a child process that invoked by SWUpdate Content Handler. - -#### APT Update Content Handler Result Codes (0x302#####) - -##### Macro for creating extended result codes - -```c -static inline ADUC_Result_t MAKE_ADUC_APT_HANDLER_EXTENDEDRESULTCODE(const int32_t value) -{ - return MAKE_ADUC_CONTENT_HANDLER_EXTENDEDRESULTCODE(ADUC_CONTENT_HANDLER_APT, value); -} -``` - -###### General Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x30200000 |ADUC_ERC_APT_HANDLER_ERROR_NONE | -| 0x30200001 |ADUC_ERC_APT_HANDLER_INITIALIZATION_FAILURE | -| 0x30200002 |ADUC_ERC_APT_HANDLER_INVALID_PACKAGE_DATA | -| 0x30200003 |ADUC_ERC_APT_HANDLER_PACKAGE_PREPARE_FAILURE_WRONG_VERSION | -| 0x30200004 |ADUC_ERC_APT_HANDLER_PACKAGE_PREPARE_FAILURE_WRONG_FILECOUNT | -| 0x30200005 |ADUC_ERC_APT_HANDLER_GET_FILEENTITY_FAILURE | -| 0x30200006 |ADUC_ERC_APT_HANDLER_INSTALLCRITERIA_PERSIST_FAILURE | - -###### Download Related Extended Result Codes +For each extended result code there is a facility which is made up of components which in turn have results. These facilities, components, and results are described in the [result_code.json](../../scripts/error_code_generator_defs/result_codes.json) file. The json file is processed when building with the `build.sh` script to generate the `result.h` file which is then used for compiling the project. -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x30200100 | ADUC_ERC_APT_HANDLER_PACKAGE_DOWNLOAD_FAILURE | +There is always a version of the `result.h` file checked-in to the GitHub repository [here](../../src/inc/aduc/result.h), however if there are changes made to the `result_codes.json` file you will need to re-generate the `result.h` file. -###### Install Related Extended Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x30200200 |ADUC_ERC_APT_HANDLER_PACKAGE_INSTALL_FAILURE| - -###### Apply Related Result Codes - -```text - None -``` - -###### Cancel Related Extended Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x30200400 |ADUC_ERC_APT_HANDLER_PACKAGE_CANCEL_FAILURE| - -###### IsInstalled Related Extended Result Codes - -```text - None -``` - -#### Steps Handler Result Codes (0x304#####) - -##### Macro for creating extended result codes - -```c -static inline ADUC_Result_t MAKE_ADUC_STEPS_HANDLER_EXTENDEDRESULTCODE(const int32_t value) -{ - return MAKE_ADUC_CONTENT_HANDLER_EXTENDEDRESULTCODE(ADUC_CONTENT_HANDLER_STEPS, value); -} -``` - -###### General Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x30400001 |ADUC_ERC_STEPS_HANDLER_GET_FILE_ENTITY_FAILURE | -| 0x30400002 |ADUC_ERC_STEPS_HANDLER_INVALID_CHILD_MANIFEST_FILENAME | -| 0x30400003 |ADUC_ERC_STEPS_HANDLER_CHILD_WORKFLOW_CREATE_FAILED | -| 0x30400004 |ADUC_ERC_STEPS_HANDLER_CHILD_WORKFLOW_INSERT_FAILED | -| 0x30400005 |ADUC_ERC_STEPS_HANDLER_GET_REF_STEP_COMPATIBILITY_FAILED | -| 0x30400006 |ADUC_ERC_STEPS_HANDLER_SET_SELECTED_COMPONENTS_FAILED | -| 0x30400007 |ADUC_ERC_STEPS_HANDLER_GET_STEP_COMPATIBILITY_FAILED | -| 0x30400008 |ADUC_ERC_STEPS_HANDLER_SET_SELECTED_COMPONENTS_FAILURE | -| 0x30400009 |ADUC_ERC_STEPS_HANDLER_NESTED_REFERENCE_STEP_DETECTED | -| 0x3040000A |ADUC_ERC_STEPS_HANDLER_INVALID_COMPONENTS_DATA | -| 0x3040000B |ADUC_ERC_STEPS_HANDLER_CREATE_SANDBOX_FAILURE | - - -###### Download Related Extended Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x30400100 | ADUC_ERC_STEPS_HANDLER_DOWNLOAD_FAILURE_UNKNOWNEXCEPTION | -| 0x30400101 | ADUC_ERC_STEPS_HANDLER_DOWNLOAD_FAILURE_MISSING_CHILD_WORKFLOW | -| 0x30400102 | ADUC_ERC_STEPS_HANDLER_DOWNLOAD_UNKNOWN_EXCEPTION_DOWNLOAD_CONTENT | - -###### Install Related Extended Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x30400200 |ADUC_ERC_STEPS_HANDLER_INSTALL_FAILURES_UNKNOWNEXCEPTION| -| 0x30400201 |ADUC_ERC_STEPS_HANDLER_INSTALL_FAILURE_MISSING_CHILD_WORKFLOW| -| 0x30400202 |ADUC_ERC_STEPS_HANDLER_INSTALL_UNKNOWN_EXCEPTION_INSTALL_CHILD_STEP| -| 0x30400203 |ADUC_ERC_STEPS_HANDLER_INSTALL_UNKNOWN_EXCEPTION_APPLY_CHILD_STEP| - -###### Apply Related Result Codes - -```text - None -``` - -###### IsInstalled Related Extended Result Codes - -```text - None -``` - - -#### Script Handler Result Codes (0x305#####) - -##### Macro for creating extended result codes - -```c -static inline ADUC_Result_t MAKE_ADUC_STEPS_HANDLER_EXTENDEDRESULTCODE(const int32_t value) -{ - return MAKE_ADUC_CONTENT_HANDLER_EXTENDEDRESULTCODE(ADUC_CONTENT_HANDLER_SCRIPT, value); -} -``` - -###### General Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x30500000 |ADUC_ERC_SCRIPT_HANDLER_ERROR_NONE | -| 0x30500001 |ADUC_ERC_SCRIPT_HANDLER_SET_SELECTED_COMPONENTS_FAILURE | -| 0x30500002 |ADUC_ERC_SCRIPT_HANDLER_MISSING_INSTALLED_CRITERIA | -| 0x30500003 |ADUC_ERC_SCRIPT_HANDLER_TOO_MANY_COMPONENTS | -| 0x30500004 |ADUC_ERC_SCRIPT_HANDLER_MISSING_PRIMARY_SCRIPT_FILE | -| 0x30500005 |ADUC_ERC_SCRIPT_HANDLER_MISSING_SCRIPTFILENAME_PROPERTY | -| 0x30500006 |ADUC_ERC_SCRIPT_HANDLER_CREATE_SANDBOX_FAILURE | -| 0x30500007 |ADUC_ERC_SCRIPT_HANDLER_INVALID_COMPONENTS_DATA | - -###### Download Related Extended Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x30500101 | ADUC_ERC_SCRIPT_HANDLER_DOWNLOAD_ERROR_NULL_WORKFLOW | -| 0x30500102 | ADUC_ERC_SCRIPT_HANDLER_DOWNLOAD_FAILURE_INVALID_FILE_COUNT | -| 0x30500103 | ADUC_ERC_SCRIPT_HANDLER_DOWNLOAD_FAILURE_GET_PRIMARY_FILE_ENTITY | -| 0x30500104 | ADUC_ERC_SCRIPT_HANDLER_DOWNLOAD_FAILURE_GET_PAYLOAD_FILE_ENTITY | -| 0x305001FE | ADUC_ERC_SCRIPT_HANDLER_DOWNLOAD_PAYLOAD_FILE_FAILURE_UNKNOWNEXCEPTION | -| 0x305001FF | ADUC_ERC_SCRIPT_HANDLER_DOWNLOAD_PRIMARY_FILE_FAILURE_UNKNOWNEXCEPTION | - -###### Install Related Extended Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x30500201 |ADUC_ERC_SCRIPT_HANDLER_INSTALL_ERROR_NULL_WORKFLOW| -| 0x30500202 |ADUC_ERC_SCRIPT_HANDLER_INSTALL_INSTALLITEM_BAD_DATA| -| 0x30500203 |ADUC_ERC_SCRIPT_HANDLER_INSTALL_INSTALLITEM_NO_UPDATE_TYPE| -| 0x30500204 |ADUC_ERC_SCRIPT_HANDLER_INSTALL_FAILURE_MISSING_PRIMARY_SCRIPT_FILE| -| 0x30500205 |ADUC_ERC_SCRIPT_HANDLER_INSTALL_FAILURE_PARSE_RESULT_FILE| -| 0x305002FF |ADUC_ERC_SCRIPT_HANDLER_INSTALL_FAILURE_UNKNOWNEXCEPTION| - -###### Apply Related Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x3050103FF |ADUC_ERC_SCRIPT_HANDLER_APPLY_FAILURE_UNKNOWNEXCEPTION| - -###### IsInstalled Related Extended Result Codes - -```text - None -``` - -###### Extended Result Codes (from child process) - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x30501### |ADUC_ERC_SCRIPT_HANDLER_CHILD_PROCESS_FAILURE_EXITCODE | The last 12 bits contains exit code from child process | - -### Content Downloader Result Codes (facility #4) - -```text - 4 00 00000 Total 4 bytes (32 bits) - - -- ----- - | | | - | | | - | | +--------- Error code (20 bits) - | | - | +------------- Component (Content Downloader) code (8 bits) - | - +--------------- Facility code (4 bits) - ``` - -#### Content Downloader Lookup - -4 [**??**] ###### - -[00](#content-downloader-common-result-codes) | [01](#delivery-optimization-downloader-result-codes) | [03](#curl-downloader-result-codes) - -```text -typedef enum tagADUC_Content_Downloader -{ - /*indicates common errors */ - ADUC_CONTENT_DOWNLOADER_COMMON = 0x00, - - /*indicates errors from Delivery Optimzation agent. */ - ADUC_CONTENT_DOWNLOADER_DELIVERY_OPTIMIZATION = 0x01, - - /*indicates errors from Curl Downloader. */ - ADUC_CONTENT_DOWNLOADER_CURL_DOWNLOADER = 0x03, - -} ADUC_Content_Downloader -``` - -###### Content Downloader Common Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x40000001 |ADUC_ERC_CONTENT_DOWNLOADER_CREATE_FAILURE_NO_SYMBOL | -| 0x40000002 |ADUC_ERC_CONTENT_DOWNLOADER_INITIALIZEPROC_NOTIMP | -| 0x40000003 |ADUC_ERC_CONTENT_DOWNLOADER_DOWNLOADPROC_NOTIMP | -| 0x40000004 |ADUC_ERC_CONTENT_DOWNLOADER_INITIALIZE_EXCEPTION | -| 0x40000005 |ADUC_ERC_CONTENT_DOWNLOADER_DOWNLOAD_EXCEPTION | -| 0x40000006 |ADUC_ERC_CONTENT_DOWNLOADER_INVALID_FILE_ENTITY | -| 0x40000007 |ADUC_ERC_CONTENT_DOWNLOADER_INVALID_DOWNLOAD_URI | -| 0x40000008 |ADUC_ERC_CONTENT_DOWNLOADER_FILE_HASH_TYPE_NOT_SUPPORTED | -| 0x40000009 |ADUC_ERC_CONTENT_DOWNLOADER_BAD_CHILD_MANIFEST_FILE_PATH | -| 0x4000000a |ADUC_ERC_CONTENT_DOWNLOADER_CANNOT_DELETE_EXISTING_FILE | -| 0x4000000b |ADUC_ERC_CONTENT_DOWNLOADER_INVALID_FILE_ENTITY_NO_HASHES | - -###### Delivery Optimization Downloader Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x40100001 |ADUC_ERROR_DELIVERY_OPTIMIZATION_DOWNLOADER_NOT_INITIALIZE | -| 0x40100002 |ADUC_ERROR_DELIVERY_OPTIMIZATION_DOWNLOADER_BAD_INIT_DATA | -| 0x40101000 + (exitCode) |ADUC_ERROR_DELIVERY_OPTIMIZATION_DOWNLOADER_EXTERNAL_FAILURE | - -###### Additional Delivery Optimization Downloader Result Codes - -Another set of extended result codes returned from the Delivery Optimization start 0xD0000000. To understand these result codes, replace 0xD with 0x8, then look up a matching WIN32 HRESULT. - -###### Curl Downloader Result Codes - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x40300001 |ADUC_ERROR_CURL_DOWNLOADER_INVALID_FILE_HASH | -| 0x40301000 + (exitCode) |ADUC_ERROR_CURL_DOWNLOADER_EXTERNAL_FAILURE | - -### Component Enumerator Result Codes (facility #7) - -7 00 ##### - -##### Macro for creating extended result codes - -```c -static inline ADUC_Result_t MAKE_ADUC_COMPONENT_ENUMERATOR_EXTENDEDRESULTCODE(const int value) -{ - return MAKE_ADUC_EXTENDEDRESULTCODE(ADUC_FACILITY_EXTENSION_COMPONENT_ENUMERATOR, 0, value); -} -``` - -###### Component Enumerator Reserved Result Codes (0x7000000 - 0x70000200) - -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x70000001 |ADUC_ERC_COMPONENT_ENUMERATOR_GETALLCOMPONENTS_NOTIMP | -| 0x70000002 |ADUC_ERC_COMPONENT_ENUMERATOR_SELECTCOMPONENTS_NOTIMP | -| 0x70000003 |ADUC_ERC_COMPONENT_ENUMERATOR_FREECOMPONENTSDATASTRING_NOTIMP | -| 0x70000004 |ADUC_ERC_COMPONENT_ENUMERATOR_EXCEPTION_GETALLCOMPONENTS | -| 0x70000005 |ADUC_ERC_COMPONENT_ENUMERATOR_EXCEPTION_SELECTCOMPONENTS | -| 0x70000006 |ADUC_ERC_COMPONENT_ENUMERATOR_EXCEPTION_FREECOMPONENTSDATASTRING | - -**Note:** Custom component enumerator extension should return error codes between 0x70000200 and 0x700FFFFF - -### Extended Result Codes from helper functions or shared utilities (facility #8) - -0x8 [??] ##### - -| [01](#crypto-related-result-codes-0x801---0x801fffff) | [03](#data-parser-result-codes-0x803---0x803fffff) | [04](#workflow-utility-result-codes-0x804---0x804fffff) - -##### Macro for creating extended result codes - -```c -/** - * @brief Macros to convert results from helper functions or shared utilities. - */ -static inline ADUC_Result_t MAKE_ADUC_UTILITIES_EXTENDEDRESULTCODE(const int component, const int value) -{ - return MAKE_ADUC_EXTENDEDRESULTCODE(ADUC_FACILITY_UTILITY, component, value); -} - -``` - -##### Components - -```c -typedef enum tagADUC_Component -{ - /*indicates errors from Crypto-related functions or utilities. */ - ADUC_COMPONENT_CRYPTO = 0x01, - - /*indicates errors from Component Handler Factory. */ - ADUC_COMPONENT_COMPONENT_HANDLER_FACTORY = 0x02, - - /*indicates errors from parsers. */ - ADUC_COMPONENT_UPDATE_DATA_PARSER = 0x03, - - /*indicates errors from Workflow Utilities or Function. */ - ADUC_COMPONENT_WORKFLOW_UTIL = 0x04, -} ADUC_Component; - -``` +## Decoding an Error Code -###### Crypto Related Result Codes (0x801##### - 0x801FFFFF) +To decode an error code within the repository you have two options. -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x80100001 |ADUC_ERC_VALIDATION_FILE_HASH_IS_EMPTY | -| 0x80100002 |ADUC_ERC_VALIDATION_FILE_HASH_TYPE_NOT_SUPPORTED | -| 0x80100003 |ADUC_ERC_VALIDATION_FILE_HASH_INVALID_HASH | +1. Manually decrypt the facility, component, and error code and look it up in the result_codes.json +2. Take the whole Extended Result Code value in hexadecimal or decimal format (e.g. 807403522 or 0x30200002 ) and then search for the value in the `result.h` file. You should get the name of the error which can then be searched to find the origination point. -###### Data Parser Result Codes (0x803##### - 0x803FFFFF) +## Decoding Error Codes from a Subprocess or Child Process of Device Update -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x80300000 |ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_INVALID_ACTION_JSON | -| 0x80300001 |ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_MANIFEST_VALIDATION_FAILED | -| 0x80300002 |ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_NO_UPDATE_ACTION | -| 0x80300003 |ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_NO_UPDATE_ID | -| 0x80300004 |ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_NO_UPDATE_TYPE | -| 0x80300005 |ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_BAD_UPDATE_TYPE | -| 0x80300006 |ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_NO_INSTALLEDCRITERIA | -| 0x80300007 |ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_BAD_FILES_OR_FILEURLS | -| 0x80300008 |ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_NO_UPDATE_MANIFEST | -| 0x80300009 |ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_BAD_UPDATE_MANIFEST | -| 0x8030000A |ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_UNSUPPORTED_UPDATE_MANIFEST_VERSION | -| 0x8030000B |ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_MISSING_DETACHED_UPDATE_MANIFEST_ENTITY | -| 0x8030000C |ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_BAD_DETACHED_UPDATE_MANIFEST | -| 0x8030000D |ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_DETACHED_UPDATE_MANIFEST_DOWNLOAD_FAILED | +If the error code you are searching for does NOT come up from the search check to see if it is one that comes from a subcomponent of Device Update which does not record its result codes within Device Update (e.g. Delivery Optimization, APT Child Process, etc.). -###### Workflow Utility Result Codes (0x804##### - 0x804FFFFF) +Delivery Optmization Error Codes will have a facility code of `13` or `0xD` and extension codes will have reserved common error codes of 0-300 with their associated facility. -| Extended Result Code | C Macro | Note | -|:----|:----|:----| -| 0x80400001 |ADUC_ERC_UTILITIES_WORKFLOW_UTIL_ERROR_BAD_PARAM | -| 0x80400002 |ADUC_ERC_UTILITIES_WORKFLOW_UTIL_ERROR_NO_MEM | -| 0x80400003 |ADUC_ERC_UTILITIES_WORKFLOW_UTIL_INVALID_ACTION_JSON_STRING | -| 0x80400004 |ADUC_ERC_UTILITIES_WORKFLOW_UTIL_INVALID_ACTION_JSON_FILE | -| 0x80400005 |ADUC_ERC_UTILITIES_WORKFLOW_UTIL_INVALID_UPDATE_ID | -| 0x80400006 |ADUC_ERC_UTILITIES_WORKFLOW_UTIL_COPY_UPDATE_ACTION_FROM_BASE_FAILURE | -| 0x80400007 |ADUC_ERC_UTILITIES_WORKFLOW_UTIL_COPY_UPDATE_MANIFEST_FROM_BASE_FAILURE | -| 0x80400008 |ADUC_ERC_UTILITIES_WORKFLOW_UTIL_PARSE_INSTRUCTION_ENTRY_FAILURE | -| 0x80400009 |ADUC_ERC_UTILITIES_WORKFLOW_UTIL_PARSE_INSTRUCTION_ENTRY_NO_UPDATE_TYPE | -| 0x8040000A |ADUC_ERC_UTILITIES_WORKFLOW_UTIL_COPY_UPDATE_ACTION_SET_UPDATE_TYPE_FAILURE | +These will require manual decoding and use of the faciltiies and codes to check for their origin. Once you have found the facility and/or component you can search for the invokation of the static Extension and Child Proccess Error Code Generator macros in the code base to see where they originate from and/or how to handle them. ## References diff --git a/docs/agent-reference/device-update-agent-extensibility-points.md b/docs/agent-reference/device-update-agent-extensibility-points.md new file mode 100644 index 000000000..90d167ca9 --- /dev/null +++ b/docs/agent-reference/device-update-agent-extensibility-points.md @@ -0,0 +1,133 @@ + +# Device Update Agent Extensibility Points + +The core DeviceUpdate agent is essentially a daemon executable that: + +- Connects to IotHub, +- Receives property updates when the twin changes, +- Evaluates "update actions" according to the [device update protocol](./goal-state-support.md) in the "desired" section of the twin +- Delegates processing of the update metadata to a set of extension plugins, +- and Reports state changes and result codes by patching the twin's "reported" section according to the [device update protocol](./goal-state-support.md) + +The remainder of this document provides a brief overview of the extensibility points, info on 1st party extensions that implement them, and links to more information, including how to implement custom ones. + +## Contract Versions + +Every Extension Type shared library plugin includes a "GetContractInfo" function export symbol that explicitly opts into a particular version of the Agent<->ExtensionType extension contract. To learn more, see [extension contract versions](./extension-contract-versions.md). + +## Registration of Extension Shared Libraries + +See the [Registering Device Update Extensions doc](./registering-device-update-extensions.md) for more information about registering shared libraries as extensions for each extensibility point. + +## Extension Types + +There are currently 5 extension types: + +1. Content Handler, also known as Step Handler (Note: singular Step, not plural Steps) + - This is the "installer technology" for the update content type, but it actually handles all aspects of the update (Is the update installed? How to download it in addition to installing/applying the update). +2. Update Manifest Handler, + - Deals with how to handle one or more steps in the multi-step update. + - It is also a Content Handler. +3. Content Downloader, + - The wrapper for the "download technology" that will actually download update payload files and detached update manifests. +4. Download Handler, + - A pre-download hook that allows skipping of the update payload if the download handler can produce it by some other means (such as Delta Diffing technology, or "Delta Update"). +5. and Component Enumerator + - Allows getting and querying of component hardware modules for proxy updates. + +## Content Handler extension type + +The agent defers the business logic for `IsInstalled`, `Download`, `Install`, `Apply`, `Backup`, `Rollback`, and `Cancel` actions to the content handler registered for the update type. + +### List of 1st-Party Registered Content Handlers + +These are the content handlers that come pre-registered when installing the Debian package and can be found under [src/extensions/step_handlers/](../../src/extensions/step_handlers/): + +- apt handler ( [README.md](../../src/extensions/step_handlers/apt_handler/README.md) ) + - limited example that can install/remove one or more APT package updates specified in an apt-manifest.json update payload, +- script handler ( [README.md](../../src/extensions/step_handlers/script_handler/README.md) ) + - run a script that is included as an update payload, +- swupdate (version 1) handler ( [README.md](../../src/extensions/step_handlers/swupdate_handler/README.md) ) + - limited example that runs the adu-swupdate.sh script included with the Debian package. It only works on ext4 file system images that conform to ADU Yocto built images. +- swupdate (version 2) handler ( [README.md](../../src/extensions/step_handlers/swupdate_handler_v2/README.md) ) + - runs a script (specified as "scriptFileName" in the "handlerProperites") that's included as an update payload and that invokes swupdate command-line. + - Agent will also pass it the path to the downloaded .swu file included as "swuFileName" in the "handlerProperties" + +### Implementing a custom content handler + +See [How to implement custom update handler](./how-to-implement-custom-update-handler.md) and examples under [src/extensions/step_handlers](../../src/extensions/step_handlers/) + +## Update Manifest Handler extension type + +The update manifest handler handles the processing of steps in the instructions of a v4 or later update manifest. See `steps` in `instructions` of a [v4+ update manifest](./update-manifest-v4-schema.md) here. + +### Default Registered Update Manifest Handler + +The default Update Manifest Handler registered in deviceupdate-agent Debian package is the [Steps Handler](../../src/extensions/update_manifest_handlers/steps_handler/README.md) (Note: Plural Steps). The implementation is in [steps_handler.cpp](../../src/extensions/update_manifest_handlers/steps_handler/src/steps_handler.cpp). + +[Steps Handler](../../src/extensions/update_manifest_handlers/steps_handler/README.md) is a content handler that applies each action aggregated across every step: + +- IsInstalled() is true if every step has been installed (and recursively if there are children component updates), +- Download() downloads the update payloads for every step +- Install() will begin after Download() phase is done and will install every step in the order they appear in the update manifest, +- and after Install phase, Apply() will similarly be done for each step in order. + +### Custom steps handling example + +If, for example, not every update payload should be downloaded upfront, then a custom UpdateManifest Handler would need to be authored and registered to do the content handler actions for the first step, and then for the next step, and so on. Each step could decide to skip it's actions under certain conditions to avoid unnecessary downloads/processing. + +### Implementing a custom Update Manifest Handler + +To implement a custom Update Manifest Handler, which is a Content Handler, export the function symbols with signatures defined in [extension_content_handler_export_symbols.h](../../src/extensions/inc/aduc/exports/extension_content_handler_export_symbols.h). + +For detailed information, see: + +- [update-manifest-v4-schema](./update-manifest-v4-schema.md) +- [How to implement custom update handler](./how-to-implement-custom-update-handler.md). +- [Steps_Handler README.md](../../src/extensions/update_manifest_handlers/steps_handler/README.md) + +## Content Downloader extension type + +The content downloader extensibility point allows a single shared library to be registered for downloading update payloads and v4+ detached update manifests. + +### Default Content Downloader + +The debian package registers libmicrosoft_deliveryoptimization_content_downloader.so, which is the shared library specified in [src/extensions/content_downloaders/deliveryoptimization_downloader/CMakeLists.txt](../../src/extensions/content_downloaders/deliveryoptimization_downloader/CMakeLists.txt) + +See [DeliveryOptimization github](https://github.com/microsoft/do-client) for more details. + +### Implementing Custom Content Downloader + +The shared library must export the symbols with function symbols documented in [extension_content_downloader_export_symbols.h](../../src/extensions/inc/aduc/exports/extension_content_downloader_export_symbols.h) + +Examples include [deliveryoptimization-content-downloader](../../src/extensions/content_downloaders/deliveryoptimization_downloader/deliveryoptimization_content_downloader.EXPORTS.cpp) and [curl-content-downloader](../../src/extensions/content_downloaders/curl_downloader/curl_content_downloader.EXPORTS.cpp). + +## Download Handler extension type + +The DownloadHandler extensibility point allows registering a shared library to be called by the core agent when a payload file in a [v5 update manifest](./update-manifest-v5-schema.md) has a `downloadHandlerId` that matches the registered id. The main idea is that the download handler is called before downloading and if it can produce the update payload file, then the agent can skip the download; otherwise, it falls back to downloading the full update payload file. + +See the [download handler README.md](../../src/extensions/download_handlers/README.md) for more information. + +### 1st Party DownloadHandler included with Debian package + +The DeviceUpdate agent installed via the Debian package will include the [Microsoft Delta Download Handler](../../src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/plugin/src/microsoft_delta_download_handler_plugin.EXPORTS.c) plugin. This download handler implements `Delta Updates` using the [Diff API](https://github.com/Azure/iot-hub-device-update-diff#diff-api) to apply the diff to recreate the update payload that was generated by [Diff Generation](https://github.com/Azure/iot-hub-device-update-diff#diff-generation) from a source swupdate .swu file and the target .swu update payload. + +### Implementing Download Handler Extension + +A download handler must export symbols with the signatures defined in [extension_download_handler_export_symbols.h](../../src/extensions/inc/aduc/exports/extension_download_handler_export_symbols.h) + +Example implementation is [here](../../src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/plugin/src/microsoft_delta_download_handler_plugin.EXPORTS.c) + +## Component Enumerator + +Component Enumerator extension is used for Proxy updates. + +Read more about it in [multi-component updating](./multi-component-updating.md). + +### Implementing a custom component enumerator extension + +A component enumerator must export symbols with signatures defined in [extension_component_enumerator_export_symbols.h](../../src/extensions/inc/aduc/exports/extension_component_enumerator_export_symbols.h) + +Example component enumerator can be found in + [example contoso README.md](../../src/extensions/component_enumerators/examples/contoso_component_enumerator/README.md) + and [contoso component enumerator demo README.md](../../src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/README.md) diff --git a/docs/agent-reference/extension-contract-versions.md b/docs/agent-reference/extension-contract-versions.md new file mode 100644 index 000000000..753b41505 --- /dev/null +++ b/docs/agent-reference/extension-contract-versions.md @@ -0,0 +1,30 @@ + +# Extension Contract Versions + +Every Extension Type shared library plugin includes a "GetContractInfo" function export symbol that explicitly opts into a particular version of the Agent<->ExtensionType extension contract, which includes: + +- Function export symbols on the extension shared library that the Agent will call +- The expected function signature return types and function arguments for the particular contract version + +## GetContractInfo export symbol + +The `GetContractInfo` symbol is defined in [extension_common_export_symbols.h](../../src/extensions/inc/aduc/exports/extension_common_export_symbols.h) but the signature is documented separately for each extension type--look for `GetContractInfo` in: + +- [content handler and update manifest handler exports](../../src/extensions/inc/aduc/exports/extension_content_handler_export_symbols.h) +- [content download exports](../../src/extensions/inc/aduc/exports/extension_content_downloader_export_symbols.h) +- [download handler exports](../../src/extensions/inc/aduc/exports/extension_download_handler_export_symbols.h) +- [component enumerator exports](../../src/extensions/inc/aduc/exports/extension_component_enumerator_export_symbols.h) + +The contract versions follow semantic versioning(i.e. back compatibility break will increment the major version, else the minor version). + +## Agent usage of GetContractInfo + +The core agent will immediately lookup the `GetContractInfo` symbol (after loading the extension shared library [DSO](https://tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html) with [dlopen(3)](https://linux.die.net/man/3/dlopen)) using [dlsym(3)](https://linux.die.net/man/3/dlsym). + +If the symbol exists, it will be called to populate the ADUC_ExtensionContractInfo struct and it will store in memory the contract version on a per-extension-type manner for use in adjusting call patterns aligned with the contract version. + +If the agent does not support the contract version opted-into by the extension, then it will fail with an ExtendedResultCode matching the pattern `ADUC_ERC_{ExtensionType}_UNSUPPORTED_CONTRACT_VERSION` in [result_codes.json](../../scripts/error_code_generator_defs/result_codes.json) or in the build-generated [src/inc/aduc/result.h](../../src/inc/aduc/result.h). + +## Back compatibility + +To support older custom extensions prior to GA, not including `GetContractInfo` symbol will be implicitly conflated with extension version 1.0 for the extension type, but eventually a version of the agent will make omitting `GetContractInfo` a failure, so it is strongly suggested to include it and to update older extensions. diff --git a/docs/agent-reference/goal-state-support.md b/docs/agent-reference/goal-state-support.md index 1a4f1235a..8ab9c10ab 100644 --- a/docs/agent-reference/goal-state-support.md +++ b/docs/agent-reference/goal-state-support.md @@ -62,8 +62,16 @@ Here is each workflow step and what it does at a high-level: - `ADUCITF_WorkflowStep_Download` - Sets current state to `ADUCITF_State_DownloadStarted` - Kicks off Download worker thread, which will call the content handler's `Download()` method to download the update content to the work folder download sandbox. - - On success, it will set the state to `ADUCITF_State_DownloadCompleted` and auto-transitions to `ADUCITF_WorkflowStep_InstallStarted` + - On success, it will set the state to `ADUCITF_State_DownloadCompleted` and auto-transitions to `ADUCITF_WorkflowStep_Backup` - On failure, it sets the state to `ADUCITF_State_Failed` and reports the failure. +- `ADUCITF_WorkflowStep_Backup` + - Sets current state to `ADUCITF_WorkflowStep_Backup` + - Kicks off Backup worker thread, which will call the content handler's `Backup()` method to backup the content needed to + be backed up + - On success, it will set the state to `ADUCITF_State_BackupCompleted` and auto-transitions to `ADUCITF_WorkflowStep_InstallStarted` + - On failure, it sets the state to `ADUCITF_State_Failed` and reports the failure. + Note: The default behavior of backup is that if Backup fails, the workflow will end and report failure immediately. + To opt out of this design, in the content handler, the owner of the content handler will need to persist the result of ADUC_Workflow_MethodCall_Backup and return ADUC_Result_Backup_Success to let the workflow continue. - `ADUCITF_WorkflowStep_Install` - Sets the current state to `ADUCITF_State_InstallStarted` - Kicks off Install worker thread, which will call the content handler's `Install()` method to install the content that is in the work folder @@ -75,6 +83,7 @@ Here is each workflow step and what it does at a high-level: - `ADUC_Result_Install_RequiredImmediateAgentRestart` or - `ADUC_Result_Install_RequiredAgentRestart` - Otherwise, it will Auto-transition to `ADUCITF_WorkflowStep_Apply` + - On failure, it will transit to `ADUCITF_WorkflowStep_Restore` - `ADUCITF_WorkflowStep_Apply` - Sets the current state to `ADUCITF_State_ApplyStarted` - Kicks off Apply worker thread, which will call the content handler's `Apply()` method @@ -86,6 +95,19 @@ Here is each workflow step and what it does at a high-level: - `ADUC_Result_Apply_RequiredImmediateAgentRestart` or - `ADUC_Result_Apply_RequiredAgentRestart` - Otherwise, it will auto-transition to idle state + - On failure, it will transit to `ADUCITF_WorkflowStep_Restore` + +- `ADUCITF_WorkflowStep_Restore` + - Sets the current state to `ADUCITF_State_RestoreStarted` + - Kicks off Restore worker thread, which will call the content handler's `Restore()` method + - On success, it will: + - Reboot the system if the result code is either + - `ADUC_Result_Restore_RequiredImmediateReboot` or + - `ADUC_Result_Restore_RequiredReboot` + - Restart the agent if the result code is either + - `ADUC_Result_Restore_RequiredImmediateAgentRestart` or + - `ADUC_Result_Restore_RequiredAgentRestart` + - Otherwise, it will auto-transition to idle state ### State Machine States The states of the state machine are defined in the `ADUCITF_State` enum in [update_content.h](../../src/adu_types/inc/aduc/types/update_content.h). diff --git a/docs/agent-reference/how-to-build-agent-code.md b/docs/agent-reference/how-to-build-agent-code.md index 5716c1688..55ad2a9ba 100644 --- a/docs/agent-reference/how-to-build-agent-code.md +++ b/docs/agent-reference/how-to-build-agent-code.md @@ -2,30 +2,26 @@ Take a look at [dependencies](how-to-build-agent-code.md#dependencies-of-device-update-agent) before you get started. You can build the Device Update agent as a standlone solution or integrate it in your existing application or solution. -* [Dependencies](how-to-build-agent-code.md#dependencies-of-device-update-agent) -* [As a standalone solution](how-to-build-agent-code.md#as-a-standalone-solution) -* [Integrate the Device Update agent in your existing application or solution](how-to-build-agent-code.md#integrate-the-device-update-agent-in-your-existing-application-or-solution) +- [Dependencies](how-to-build-agent-code.md#dependencies-of-device-update-agent) +- [As a standalone solution](how-to-build-agent-code.md#as-a-standalone-solution) +- [Integrate the Device Update agent in your existing application or solution](how-to-build-agent-code.md#integrate-the-device-update-agent-in-your-existing-application-or-solution) -Take a look at [dependancies](how-to-build-agent-code.md#dependencies-of-device-update-agent) before you get started. You can build the Device Update agent as a standlone solution or integrate it in your existing application or solution. +## Dependencies of Device Update Agent -* [Dependencies](how-to-build-agent-code.md#dependencies-of-device-update-agent) -* [As a standalone solution](how-to-build-agent-code.md#as-a-standalone-solution) -* [Integrate the Device Update agent in your existing application or solution](how-to-build-agent-code.md#integrate-the-device-update-agent-in-your-existing-application-or-solution) +### Required Dependencies -# Dependencies of Device Update Agent +- Azure IoT C SDK +- Delivery Optimization SDK +- Azure Blob Storage File Upload Utility +- IotHub Device Update Delta -## Required Dependencies - -* Azure IoT C SDK -* Delivery Optimization SDK - -## Azure IoT C SDK +### Azure IoT C SDK Use the [Azure IoT C SDK](https://github.com/Azure/azure-iot-sdk-c) to connect to IoT Hub and call Azure IoT Plug and Play APIs. -## Delivery Optimization +### Delivery Optimization The [Delivery Optimization SDK](https://github.com/microsoft/do-client) @@ -56,16 +52,31 @@ To install only the dependencies necessary for the agent: ./scripts/install-deps.sh --install-aduc-deps --install-packages ``` -If you want to install dependencies for a different distro other than Ubuntu 18.04 (the default option), use option `-d`, for example `--deps-distro ubuntu2004`. (Installing DO dependencies using -[bootstrap script in do-client](https://github.com/microsoft/do-client/blob/v0.8.2/build/scripts/bootstrap.sh)) - `install-deps.sh` also provides several options for installing individual -dependencies. To see the usage info: +dependencies. To see the usage info: ```shell ./scripts/install-deps.sh -h ``` +#### Build Azure IotHub SDK for C WebSockets static library + +By default, both MQTT (`libiothub_client_mqtt_transport.a`) and MQTT over WebSockets(`libiothub_client_mqtt_ws_transport.a`) static libraries are built by `install-deps.sh` via the `use_mqtt` and `use_wsio` -D configs. + +```shell +# Verify mqtt transport static lib exists +$ locate libiothub_client_mqtt_transport.a | \ + grep '/usr/local/lib/' +/usr/local/lib/libiothub_client_mqtt_transport.a +``` + +```shell +# Verify mqtt over websockets static lib exists +$ locate libiothub_client_mqtt_ws_transport.a | \ + grep '/usr/local/lib/' +/usr/local/lib/libiothub_client_mqtt_ws_transport.a +``` + ## As a standalone solution ### Device Update Linux Build System @@ -74,11 +85,10 @@ The Device Update for IoT Hub reference agent code utilizes CMake for building. #### Build Using build.sh -To build the reference agent integrated with Delivery Optimization for -downloads but still mocks the `Install` and `Apply` actions: +To build the reference agent with the default parameters: ```shell -./scripts/build.sh -c -p linux +./scripts/build.sh -c ``` To see additional build options with build.sh: @@ -87,11 +97,35 @@ To see additional build options with build.sh: build.sh -h ``` +### Build and Run the unit tests + +To build and run the unit tests: + +```shell +./scripts/build.sh -c -u +pushd out +ctest +``` + +For more test run options: + +```shell +ctest -h +``` + +### Build the Debian package + +To build the debian package (will be output to the `out` directory): + +```shell +./scripts/build.sh --build-packages +``` + ### Build the agent using CMake Alternatively, you can build using CMake directly. Set the required product values for ADUC_DEVICEINFO_MANUFACTURER and ADUC_DEVICEINFO_MODEL in the top-level -[CMakeLists.txt](../../CMakeLists.txt) before building. Optional CMake values can be found there as well. +[CMakeLists.txt](../../CMakeLists.txt) before building. Optional CMake values can be found there as well. ```shell mkdir -p build && pushd build @@ -109,9 +143,82 @@ ninja popd > /dev/null ``` +You can do incremental builds with Ninja: + +```shell +pushd out && ninja +popd +``` + +### Build Options for MQTT and MQTT over WebSockets IotHub Transport Providers + +#### Allow MQTT or MQTT over Websockets IotHub Transport Protocols Driven from Config + +By Default, both mqtt and mqtt/WebSockets will be linked into the AducIotAgent binary agent and driven by the `"iotHubProtocol"` config property with valid values of `"mqtt"` or `"mqtt/ws"`, for MQTT and MQTT over WebSockets, respectively. + +Here is the default in top-level `CMakeLists.txt`: + +```shell +set ( + ADUC_IOT_HUB_PROTOCOL + "IotHub_Protocol_from_Config" + CACHE + STRING + "The protocol for Azure IotHub SDK communication. Options are MQTT, MQTT_over_WebSockets, and IotHub_Protocol_from_Config") +``` + +Sample `/etc/adu/du-config.json` that selects `MQTT` value for the `iotHubProtocol` property: + +```json +{ + ... + "iotHubProtocol": "mqtt" +} +``` + +Sample `/etc/adu/du-config.json` that selects `MQTT over WebSockets` value for the `iotHubProtocol` property: + +```json +{ + ... + "iotHubProtocol": "mqtt/ws" +} +``` + +#### Use only MQTT IotHub Transport Protocol + +If using only `MQTT`, then choosing `MQTT` for `ADUC_IOT_HUB_PROTOCOL` in the top-level `CMakeLists.txt` will reduce the size of Type=SizeMinRel AducIotAgent binary by about 60 KB, which is relatively small compared to the overall footprint that is on the order of 2-3 megabytes. + +To link in only MQTT transport provider, set this in the top-level `CMakeLists.txt`: + +```shell +set ( + ADUC_IOT_HUB_PROTOCOL + "MQTT" + CACHE + STRING + "The protocol for Azure IotHub SDK communication. Options are MQTT, MQTT_over_WebSockets, and IotHub_Protocol_from_Config") +``` + +#### Use only MQTT over WebSockets IotHub Transport Protocol + +After enabling WebSockets above using install-deps.sh so that it builds libiothub_client_mqtt_ws_transport.a static library, modify the top-level CMakeLists.txt to use MQTT_over_WebSockets: + +```shell +set ( + ADUC_IOT_HUB_PROTOCOL + "MQTT_over_WebSockets" + CACHE + STRING + "The protocol for Azure IotHub SDK communication. Options are MQTT, MQTT_over_WebSockets, and IotHub_Protocol_from_Config") +``` + +Doing ./build.sh after setting this to `"MQTT_over_WebSockets"` will have the MQTT traffic go over a websocket on port `443`. +Using `"MQTT"` will use SecureMQTT over port `8883`. + ## Install the Device Update Agent -To install the Device Update Agent after building: +### Install the Device Update Agent after building ```shell sudo cmake --build out --target install @@ -127,19 +234,51 @@ popd > /dev/null **Note** If the Device Update Agent was built as a daemon, the install targets will install and register the Device Update Agent as a daemon. +### Install the Device Update Agent and Extensions from Debian Package + +After building the Debian package using `build.sh --build-packages`, do: + +```shell +sudo apt install ./out/{PKG_NAME}.deb +``` + ## Run Device Update Agent Run Device Update Agent by following these [instructions](./how-to-run-agent.md) +### Verify MQTT Port + +If using MQTT, run the following netstat command to verify that it is connecting to the remote MQTT port of 8883: + +```shell +$ sudo ./out/bin/AducIotClient -l0 -e > /dev/null 2>&1 & +$ sudo netstat -pantu | grep Adu +tcp 0 0 : :8883 ESTABLISHED /./out/bin/Aduc +$ fg + +``` + +### Verify MQTT over WebSockets Port + +If using MQTT over WebSockets, run the following netstat command to verify that it is connecting to the remote WebSockets port of 443: + +```shell +$ sudo ./out/bin/AducIotClient -l0 -e > /dev/null 2>&1 & +$ sudo netstat -pantu | grep Adu +tcp 0 0 : :443 ESTABLISHED /./out/bin/Aduc +$ fg + +``` + ## Integrate the Device Update agent in your existing application or solution ### Pre-concepts Before integrating the Device Update agent in your existing application or solution review the below concepts. -* Learn how the Device Update service will communicate with the device client using IoT Hub Plug and Play properties to orchestrate over-the-air update actions from [here](../../src/adu_workflow/src/agent_workflow.c). -* Understand the update manifest to be able to write code to [respond to update actions from your client](update-manifest.md). -* Understand how to implement 'ADU Core' interface for the Device Update service to [communicate with your client on the Device](device-update-plug-and-play.md). +- Learn how the Device Update service will communicate with the device client using IoT Hub Plug and Play properties to orchestrate over-the-air update actions from [here](../../src/adu_workflow/src/agent_workflow.c). +- Understand the update manifest to be able to write code to [respond to update actions from your client](update-manifest.md). +- Understand how to implement 'ADU Core' interface for the Device Update service to [communicate with your client on the Device](device-update-plug-and-play.md). ### Steps @@ -147,10 +286,7 @@ Before integrating the Device Update agent in your existing application or solut 2. Once you have a IoT Hub Plug and Play enabled device, implement the 'ADU Core' interfaces for your application, see reference code [here](../../src/adu_workflow/src/agent_workflow.c). 3. Review the below Device Update agent implementation and source code so that you can modify your application to replicate the same behaviors: -* Agent Architecture -![Agent Architecture](images/agent-architecture.png) - -* Workflow phases and source code +- Workflow phases and source code Download phase: ![Download phase](images/download-phase.png) @@ -161,6 +297,6 @@ Install phase: Apply phase: ![Apply phase](images/apply-phase.png) -* [Source code](../../src/adu_workflow/src/agent_workflow.c) +- [Source code](../../src/adu_workflow/src/agent_workflow.c) -4.The result reported from your application should be in this format so that the Device Update service can work with your application. Learn more about [plug and play format](https://docs.microsoft.com/azure/iot-hub-device-update/device-update-plug-and-play), and Device Update agent [workflow](../../src/adu_workflow/src/agent_workflow.c). + 4.The result reported from your application should be in this format so that the Device Update service can work with your application. Learn more about [plug and play format](https://docs.microsoft.com/azure/iot-hub-device-update/device-update-plug-and-play), and Device Update agent [workflow](../../src/adu_workflow/src/agent_workflow.c). diff --git a/docs/agent-reference/how-to-build-swupdate-for-swupdate-handler-unit-tests.md b/docs/agent-reference/how-to-build-swupdate-for-swupdate-handler-unit-tests.md new file mode 100644 index 000000000..1373db85c --- /dev/null +++ b/docs/agent-reference/how-to-build-swupdate-for-swupdate-handler-unit-tests.md @@ -0,0 +1,187 @@ +# How To Build swupdate for SWUpdate Handler Unit Tests + +At the moment, **SWUpdate** is not available on Ubuntu 18.04. To run SWUpdate Handler unit tests, the private build of SWUpdate is required. + +## About SWUpdate + +See [SWUpdate Overview](https://sbabic.github.io/swupdate/swupdate.html) for more details. + +Follow the following steps to build the test version of SWUpdate: + +## Create Sources Folder + +```sh +mkdir -p /tmp/swupdate +cd /tmp/swupdate +``` + +## Clone SWUpdate Project Sources + +```sh +cd /tmp +git clone https://github.com/sbabic/swupdate +``` + +## Install Default Parser Library + +SWUpdate uses the library "libconfig" as default parser for the image description. However, it is possible to extend SWUpdate and add an own parser, based on a different syntax and language as the one supported by libconfig. + +See [SWUpdate: syntax and tags with the default parser](https://sbabic.github.io/swupdate/sw-description.html#swupdate-syntax-and-tags-with-the-default-parser) for more details. + +For development, we need to install **libconfig-dev** package: + +```sh +sudo apt install -y libconfig-dev +``` + +## Set Build Config for Test Version + +Build configurations for swupdate can be specified in `.config` file. + +For this version, we customized following options: + +- CONFIG_DEFAULT_CONFIG_FILE="/usr/local/du/tests/swupdate.cfg" +- CONFIG_HW_COMPATIBILITY_FILE="/usr/local/du/tests/hwrevision" +- CONFIG_SW_VERSIONS_FILE="/usr/local/du/tests/sw-versions" +- CONFIG_DEBUG=y +- CONFIG_DEBUG_PESSIMIZE=y +- CONFIG_LIBCONFIG=y +- CONFIG_PARSERROOT="software" + +**Copy the following configurations to `/tmp/swupdate/.config`** + +```conf +# +# Automatically generated file; DO NOT EDIT. +# SWUpdate Configuration +# + +# +# SWUpdate Settings +# + +# +# General Configuration +# +CONFIG_CURL=y +# CONFIG_CURL_SSL is not set +# CONFIG_DISKFORMAT is not set +# CONFIG_SYSTEMD is not set +CONFIG_DEFAULT_CONFIG_FILE="/usr/local/du/tests/swupdate.cfg" +CONFIG_SCRIPTS=y +CONFIG_HW_COMPATIBILITY=y +CONFIG_HW_COMPATIBILITY_FILE="/usr/local/du/tests/hwrevision" +CONFIG_SW_VERSIONS_FILE="/usr/local/du/tests/sw-versions" + +# +# Socket Paths +# +CONFIG_SOCKET_CTRL_PATH="" +CONFIG_SOCKET_PROGRESS_PATH="" +# CONFIG_MTD is not set +# CONFIG_LUA is not set +# CONFIG_FEATURE_SYSLOG is not set + +# +# Build Options +# +CONFIG_CROSS_COMPILE="" +CONFIG_SYSROOT="" +CONFIG_EXTRA_CFLAGS="-g" +CONFIG_EXTRA_LDFLAGS="" +CONFIG_EXTRA_LDLIBS="" + +# +# Debugging Options +# +CONFIG_DEBUG=y +CONFIG_DEBUG_PESSIMIZE=y +# CONFIG_WERROR is not set +CONFIG_NOCLEANUP=y + +# +# Bootloader support +# +# CONFIG_BOOTLOADER_EBG is not set +# CONFIG_UBOOT is not set +CONFIG_BOOTLOADER_NONE=y +# CONFIG_BOOTLOADER_GRUB is not set +CONFIG_UPDATE_STATE_CHOICE_NONE=y +# CONFIG_UPDATE_STATE_CHOICE_BOOTLOADER is not set + +# +# Interfaces +# +CONFIG_DOWNLOAD=y +# CONFIG_DOWNLOAD_SSL is not set +CONFIG_CHANNEL_CURL=y +# CONFIG_SURICATTA is not set +CONFIG_WEBSERVER=y +CONFIG_MONGOOSE=y +CONFIG_MONGOOSEIPV6=y +CONFIG_MONGOOSESSL=y + +# +# Security +# +# CONFIG_SSL_IMPL_NONE is not set +CONFIG_SSL_IMPL_OPENSSL=y +# CONFIG_SSL_IMPL_WOLFSSL is not set +# CONFIG_SSL_IMPL_MBEDTLS is not set +CONFIG_HASH_VERIFY=y +# CONFIG_SIGNED_IMAGES is not set +CONFIG_ENCRYPTED_IMAGES=y +# CONFIG_ENCRYPTED_SW_DESCRIPTION is not set +# CONFIG_PKCS11 is not set + +# +# Compressors (zlib always on) +# +CONFIG_GUNZIP=y +# CONFIG_ZSTD is not set + +# +# Parsers +# + +# +# Parser Features +# +CONFIG_LIBCONFIG=y +CONFIG_PARSERROOT="software" +# CONFIG_JSON is not set +# CONFIG_SETSWDESCRIPTION is not set + +# +# Handlers +# + +# +# Image Handlers +# +# CONFIG_ARCHIVE is not set +# CONFIG_BOOTLOADERHANDLER is not set +# CONFIG_DELTA is not set +# CONFIG_DISKPART is not set +# CONFIG_DISKFORMAT_HANDLER is not set +CONFIG_RAW=y +# CONFIG_RDIFFHANDLER is not set +# CONFIG_READBACKHANDLER is not set +# CONFIG_REMOTE_HANDLER is not set +CONFIG_SHELLSCRIPTHANDLER=y +# CONFIG_SWUFORWARDER_HANDLER is not set +# CONFIG_UCFWHANDLER is not set +# CONFIG_UNIQUEUUID is not set +``` + +## Build swupdate + +```sh +cd /usr/local/du/deps/sources/swupdate && make +``` + +## Install swupdate to /usr/bin + +```sh +cd /usr/local/du/deps/sources/swupdate && sudo make install +``` diff --git a/docs/agent-reference/how-to-implement-custom-update-handler.md b/docs/agent-reference/how-to-implement-custom-update-handler.md index eb202b202..34a7f6627 100644 --- a/docs/agent-reference/how-to-implement-custom-update-handler.md +++ b/docs/agent-reference/how-to-implement-custom-update-handler.md @@ -9,9 +9,9 @@ Device Update has previously supported four reference Device Update Agent binari - A simulator agent that supports 'microsoft/swupdate:1' update type - A simulator agent that supports 'microsoft/apt:1' update type -The Public Preview refresh Agent supports an Update Content Handler extension, which enables the Agent to support multiple Update Types at the same time. +The Public Preview refresh Agent supports an Update Content Handler extension, which enables the Agent to support multiple Update Types at the same time. -Device builders can define a custom Update Type, implement the associated custom Update Content Handler, and register it, if needed for their additional device updates and deployment scenarios. +Device builders can define a custom Update Type, implement the associated custom Update Content Handler, and register it, if needed for their additional device updates and deployment scenarios. ## Requirements @@ -43,11 +43,11 @@ See **ContentHandler** class definition in [content_handler.hpp](../../src/exte ## Consuming ADUC_WorkflowData -As noted above, DU Agent Core passes an [**ADUC_WorkflowData**](../../src/adu_types/inc/aduc/types/workflow.h) object when invoking a function provided by the handler. A '**WorkflowHandle**' field of the [**ADUC_WorkflowData**](../../src/adu_types/inc/aduc/types/workflow.h) object contains all data needed to perform each task. +As noted above, DU Agent Core passes an [**ADUC_WorkflowData**](../../src/adu_types/inc/aduc/types/workflow.h) object when invoking a function provided by the handler. A '**WorkflowHandle**' field of the [**ADUC_WorkflowData**](../../src/adu_types/inc/aduc/types/workflow.h) object contains all data needed to perform each task. -A group of helper functions declared in workflow_utils.h can be used to query workflow data from the WorkflowHandle. +A group of helper functions declared in workflow_utils.h can be used to query workflow data from the WorkflowHandle. -Below, you can find an example of useful helper functions used in most handlers provided in this project. +Below, you can find an example of useful helper functions used in most handlers provided in this project. | Function | Purpose | |:---|:---| @@ -61,11 +61,11 @@ workflow_get_installed_criteria|Get the 'installed' criteria string| ## Implementing A 'Component-Aware' Content Handler -Usually, a content handler is designed to install an update content on a Host Device. An example of this is the [APT Update Content Handler](../../src/content_handlers/apt_handler/README.md) provided in this project, which installs one or more Debian packages on the host device. +Usually, a content handler is designed to install an update content on a Host Device. An example of this is the [APT Update Content Handler](../../src/extensions/step_handlers/apt_handler/README.md) provided in this project, which installs one or more Debian packages on the host device. -In some case, a Device Builder may want to install an update content on one or more component(s) that connected to the Host Device instead. +In some case, a Device Builder may want to install an update content on one or more component(s) that connected to the Host Device instead. -In this case, if the Update has been authored and imported correctly, the DU Agent workflow will include a 'Selected Components' data in the ADUC_WorkflowHandle object that is passed to the handler's function. +In this case, if the Update has been authored and imported correctly, the DU Agent workflow will include a 'Selected Components' data in the ADUC_WorkflowHandle object that is passed to the handler's function. When implementing a handler that handles an update intended for connected-components, the following helper functions can be used to get selected components properties: @@ -73,23 +73,23 @@ When implementing a handler that handles an update intended for connected-compon |---|---| |workflow_peek_selected_components|Get a serialized JSON string containing a collection of components that the update should be installed.| -The component data is provided by Component Enumerator Extension, which usually implemented by Device Builder and registered on the Host Device. +The component data is provided by Component Enumerator Extension, which usually implemented by Device Builder and registered on the Host Device. -> See [Contoso Virtual Vacuum Component Enumerator](../../src/extensions/component-enumerators/examples/contoso-component-enumerator/README.md) example for more details +> See [Contoso Virtual Vacuum Component Enumerator](../../src/extensions/component_enumerators/examples/contoso_component_enumerator/README.md) example for more details ## How To Build A Content Handler Extension -To get started, take a look at existing Content Handler Extensions in [src/content_handlers](../../src/content_handlers) folder for reference. +To get started, take a look at existing Content Handler Extensions in [src/content_handlers](../../src/content_handlers) folder for reference. ## How To Register A Content Handler Extension To register a content handler, run following command on the device: ```sh -sudo /usr/bin/AducIotAgent --register--content-handler --update-type +sudo /usr/bin/AducIotAgent --extension-type updateContentHandler --register-extension --extension-id # For example -# sudo /usr/bin/AducIotAgent --register-content-handler /var/lib/adu/extensions/sources/libmicrosoft_apt_1.so --update-type 'microsoft/apt:1' +# sudo /usr/bin/AducIotAgent --extension-type updateContentHandler --register-extension /var/lib/adu/extensions/sources/libmicrosoft_apt_1.so --extension-id 'microsoft/apt:1' ``` ### Return Value diff --git a/docs/agent-reference/how-to-modify-the-agent-code.md b/docs/agent-reference/how-to-modify-the-agent-code.md index 4f4bf0292..963055d96 100644 --- a/docs/agent-reference/how-to-modify-the-agent-code.md +++ b/docs/agent-reference/how-to-modify-the-agent-code.md @@ -13,10 +13,20 @@ and ## Adjusting the Download, Install, and Apply actions for your device -You may want to implement your own version of Download, Install and Apply for +You may want to implement your own version of IsInstalled, Download, Install and Apply for your device or installer. You can provide your own implementation of the -[Content Handler APIs](../../src/content_handlers/inc/aduc/content_handler.hpp) -in [src/content_handlers](../../src/content_handlers) +[Content Handler APIs](../../src/extensions/inc/aduc/content_handler.hpp) +in [src/extensions/step_handlers](../../src/extensions/step_handlers) + +## Customize extensibility points + +In addition to customizing content handlers, you can customize: +- How update steps in the update metadata are orchestrated/processed, +- How update payloads and detached v4 update manifest files are downloaded, +- How hardware sub-module "components" are enumerated and queried, +- How update payloads can be pre-processed to reconstitute payloads (particularly large ones) without downloading them (e.g. Delta Updating). + +For comprehensive docs on all extensibility points, refer to the [Device Update Agent Extensibility points](./device-update-agent-extensibility-points.md) doc. ## Porting to a Different OS or Platform diff --git a/docs/agent-reference/how-to-run-agent.md b/docs/agent-reference/how-to-run-agent.md index 1868fab06..8da9f610a 100644 --- a/docs/agent-reference/how-to-run-agent.md +++ b/docs/agent-reference/how-to-run-agent.md @@ -70,13 +70,13 @@ configuration file. Place your device connection string in /adu/adu-conf.txt. To manually start the daemon: ```shell -sudo systemctl start adu-agent +sudo systemctl start deviceupdate-agent ``` To manually stop the daemon: ```shell -sudo systemctl stop adu-agent +sudo systemctl stop deviceupdate-agent ``` ## How To Create 'adu' Group and User diff --git a/docs/agent-reference/how-to-simulate-update-result.md b/docs/agent-reference/how-to-simulate-update-result.md index 146640499..27882bd0b 100644 --- a/docs/agent-reference/how-to-simulate-update-result.md +++ b/docs/agent-reference/how-to-simulate-update-result.md @@ -1,21 +1,21 @@ # How To Simulate Update Results -You can use the Simulator Update Handler to simulate the Device Update workflow without actually downloading and installing any files to the actual device for demonstration or testing purposes. +You can use the Simulator Update Handler to simulate the Device Update workflow without actually downloading and installing any files to the actual device for demonstration or testing purposes. -The source code for this Simulator Update Handler can be found at [src/content_handlers/simulator_handler](../../src/content_handlers/simulator_handler) +The source code for this Simulator Update Handler can be found at [src/extensions/step_handlers/simulator_handler](../../src/extensions/step_handlers/simulator_handler) ## Register Simulator Update Handler To register the Simulator Update Handle, use following command: ```sh -sudo /usr/bin/AducIotAgent --update-type --register-content-handler +sudo /usr/bin/AducIotAgent --extension-type updateContentHandler --extension-id --register-extension ``` Below is an example of how to register Simulator Update Handler for handle 'microsoft/apt:1' update: ```sh -sudo /usr/bin/AducIotAgent --update-type 'microsoft/apt:1' --register-content-handler /var/lib/adu/extensions/sources/libmicrosoft_simulator_1.so +sudo /usr/bin/AducIotAgent --extension-type updateContentHandler --extension-id 'microsoft/apt:1' --register-extension /var/lib/adu/extensions/sources/libmicrosoft_simulator_1.so ``` ## Create Simulator Data File @@ -30,9 +30,9 @@ The Simulator Update Handler by default will returns following results: | Cancel | { "resultCode" : 800 } | | IsInstalled | { "resultCode" : 900 } | -**Note:** See `ADUC_ResultCode` enum in [aduc_core.h](../../src/adu_types/inc/aduc/types/adu_core.h) +**Note:** See `ADUC_ResultCode` enum in [aduc_core.h](../../src/adu_types/inc/aduc/types/adu_core.h) -To override one or more default result above, the desired result can be specified in the Simulator Data file (**du-simulator-data.json**). +To override one or more default result above, the desired result can be specified in the Simulator Data file (**du-simulator-data.json**). The Simulator Update Handler will search for this file in `temp` directories specified by following system environment variables (in order): @@ -45,7 +45,7 @@ TEMPDIR If none of the above system environment variables are specified, the file must be place in `/tmp` directory (**/tmp/du-simulator-data.json**) -You can modify the Simulator Update Handler source code to specify your own search preference. See [simulator_handler.cpp](../../src/content_handlers/simulator_handler/src/simulator_handler.cpp) +You can modify the Simulator Update Handler source code to specify your own search preference. See [simulator_handler.cpp](../../src/extensions/step_handlers/simulator_handler/src/simulator_handler.cpp) ### Simulate Data File Schema @@ -54,61 +54,62 @@ You can modify the Simulator Update Handler source code to specify your own sear "version" : "1.0", "download" : { "" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" } "" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" }, ... "" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" - }, + }, "*" : { // A fall back result for all unmatched file names - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" } }, "install" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" }, "apply" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" }, "cancel" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" }, "isInstalled" : { "" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" }, "" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" }, ... "" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" }, "*" : { // A fall back result for all unmatched 'installedCriteria' string - "resultCode" : , + // IMPORTANT: For Update Manifest version 4.0 or later, only this fall-back result will be return. + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" } @@ -123,23 +124,23 @@ To simulate the desired result for 'download' action, place the following JSON d ```json "download" : { "" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" } "" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" }, ... "" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" - }, + }, "*" : { // A fall back result for all unmatched file names - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" } @@ -168,7 +169,7 @@ You can specify only one result for the 'install' action. To simulate the desire ```json "install" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" } @@ -190,7 +191,7 @@ You can specify only one result for the 'apply' action. To simulate the desired ```json "apply" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" } @@ -202,7 +203,7 @@ You can specify only one result for the 'cancel' action. To simulate the desired ```json "cancel" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" } @@ -215,23 +216,23 @@ To simulate the desired result for the 'isInstalled' check, place the following ```json "isInstalled" : { "" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" }, "" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" }, ... "" : { - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" }, "*" : { // A fall back result for all unmatched 'installedCriteria' string - "resultCode" : , + "resultCode" : , "extendedResultCode" : , "resultDetails" : "" } @@ -243,19 +244,19 @@ You can specify multiple results for 'isInstalled' check, for example: ```json "isInstalled" : { "1.0" : { - "resultCode" : 900, // ADUC_Result_IsInstalled_Installed + "resultCode" : 900, // ADUC_Result_IsInstalled_Installed "extendedResultCode" : 0, "resultDetails" : "" }, "1.2" : { - "resultCode" : 900, // ADUC_Result_IsInstalled_Installed + "resultCode" : 900, // ADUC_Result_IsInstalled_Installed "extendedResultCode" : 0, "resultDetails" : "" }, "*" : { // Indicates a fall back result for all unmatched 'installedCriteria' string - "resultCode" : 901, // ADUC_Result_IsInstalled_NotInstalled + "resultCode" : 901, // ADUC_Result_IsInstalled_NotInstalled "extendedResultCode" : 0, "resultDetails" : "This update is not installed." } diff --git a/docs/agent-reference/multi-component-updating.md b/docs/agent-reference/multi-component-updating.md index b230e7bb3..e99e7385c 100644 --- a/docs/agent-reference/multi-component-updating.md +++ b/docs/agent-reference/multi-component-updating.md @@ -17,8 +17,8 @@ In order to update a component or components that connected to a IoTHub-Connecte ## How to register multiple components with the Component Enumerator Extension -See [Contoso Component Enumerator](../../src/extensions/component-enumerators/examples/contoso-component-enumerator/README.md) for example on how to implement and register a custom Component Enumerator extension. +See [Contoso Component Enumerator](../../src/extensions/component_enumerators/examples/contoso_component_enumerator/README.md) for example on how to implement and register a custom Component Enumerator extension. ## Example Proxy Update -See [tutorial using the Device Update agent to do Proxy Updates](../../src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/README.md) with sample updates for components connected to a Contoso Virtual Vacuum device. +See [tutorial using the Device Update agent to do Proxy Updates](../../src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/README.md) with sample updates for components connected to a Contoso Virtual Vacuum device. diff --git a/docs/agent-reference/registering-device-update-extensions.md b/docs/agent-reference/registering-device-update-extensions.md new file mode 100644 index 000000000..bdd83e0a7 --- /dev/null +++ b/docs/agent-reference/registering-device-update-extensions.md @@ -0,0 +1,130 @@ +# Registering device update extensions + +For the 5 extension types, here is the breakdown of registration multiplicity: + +1. Content Handler - Multiple can be registered, each associated with updateType key +2. Update Manifest Handler - Multiple can be registered, with update manifest version key +3. Content Downloader - Exactly one can be registered +4. Download Handler - Multiple can be registered, each associated with `downloadHandlerId` +5. Component Enumerator - Exactly one can be registered + +Note: Below `"sha256"` property values can be generated via: + +```sh +openssl dgst -sha256 -binary /path/to/lib.so | openssl base64 +``` + +## First-Party Extension Registrations + +See `register_reference_extensions()` in the Debian package [postinst script](../../packages/debian/postinst) + +## Registering Content Handler extension shared library + +### Agent Command-line + +```sh + /usr/bin/AducIotAgent -l 2 --extension-type updateContentHandler --extension-id "microsoft/apt:1" --register-extension /path/to/libmicrosoft_apt_1.so +``` + +### Contents of resultant content_handler.json registration file + +```sh +$ cat /var/lib/adu/extensions/update_content_handlers/microsoft_apt_1/content_handler.json +{ + "fileName":"/var/lib/adu/extensions/sources/libmicrosoft_apt_1.so", + "sizeInBytes":2365288, + "hashes": { + "sha256":"/7IfVowZdcOYSNVqkrPHCa8aYBITg7gOj9/IIuX7CgU=" + }, + "handlerId":"microsoft/apt:1" +} +``` + +## Registering Update Manifest Handler extension shared library + +### Agent Command-line to register update manifest handler + +```sh + $adu_bin_path -l 2 --extension-type updateContentHandler --extension-id "microsoft/update-manifest" --register-extension $adu_extensions_sources_dir/$adu_steps_handler_file + $adu_bin_path -l 2 --extension-type updateContentHandler --extension-id "microsoft/update-manifest:4" --register-extension $adu_extensions_sources_dir/$adu_steps_handler_file + $adu_bin_path -l 2 --extension-type updateContentHandler --extension-id "microsoft/update-manifest:5" --register-extension $adu_extensions_sources_dir/$adu_steps_handler_file +``` + +### Contents of resultant content_handler.json for update manifest handler + +```sh +$ cat /var/lib/adu/extensions/update_content_handlers/microsoft_update-manifest_5/content_handler.json +{ + "fileName":"/var/lib/adu/extensions/sources/libmicrosoft_steps_1.so", + "sizeInBytes":2151872, + "hashes": { + "sha256":"PV13dnM1uOzzYguZl/jq27i/xv+HXwPauvYrJBsNXvI=" + }, + "handlerId":"microsoft/update-manifest:5" +} +``` + +## Registering Content Downloader extension shared library + +### Agent Command-line to register content downloader + +```sh + + $adu_bin_path -l 2 --extension-type contentDownloader --register-extension $adu_extensions_sources_dir/$adu_delivery_optimization_downloader_file +``` + +### Contents of resultant extension.json for content downloader + +```sh +cat /var/lib/adu/extensions/content_downloader/extension.json +{ + "fileName":"/var/lib/adu/extensions/sources/libdeliveryoptimization_content_downloader.so", + "sizeInBytes":254584, + "hashes": { + "sha256":"0amRRIkSZ/im/AoLahg7QZwzZWo837VPEa1zedXl9BA=" + } +} +``` + +## Registering Download Handler extension shared library + +### Agent Command-line to register download handler + +```sh + $adu_bin_path -l 2 --extension-type downloadHandler --extension-id "microsoft/delta:1" --register-extension $adu_extensions_sources_dir/$adu_delta_download_handler_file +``` + +### Contents of resultant extension.json for download handler + +```sh +$ cat /var/lib/adu/extensions/download_handlers/microsoft_delta_1/download_handler.json +{ + "fileName":"/var/lib/adu/extensions/sources/libmicrosoft_delta_download_handler.so", + "sizeInBytes":1774008, + "hashes": { + "sha256":"ldzJKSkUCjG7YdXxDbAFk/LwLPsmDzvXLwCZyGwnzBQ=" + }, + "handlerId":"microsoft/delta:1" +} +``` + +## Registering Component Enumerator extension shared library + +### Agent Command-line to register component enumerator + +```sh +$adu_bin_path -l 2 --extension-type componentEnumerator --register-extension $adu_extensions_sources_dir/$adu_example_component_enumerator_file +``` + +### Contents of extension.json for component enumerator + +```sh +cat /var/lib/adu/extensions/component_enumerator/extension.json +{ + "fileName":"/var/lib/adu/extensions/sources/libcontoso_component_enumerator.so", + "sizeInBytes":117896, + "hashes": { + "sha256":"z+X29flqe4MLbkDm5i2xWJAVJSyQTLamMZOhiszmw9k=" + } +} +``` diff --git a/docs/agent-reference/update-manifest-v4-schema.md b/docs/agent-reference/update-manifest-v4-schema.md index b85357e79..948de3db5 100644 --- a/docs/agent-reference/update-manifest-v4-schema.md +++ b/docs/agent-reference/update-manifest-v4-schema.md @@ -6,215 +6,33 @@ Please see the [Update Manifest](https://docs.microsoft.com/en-us/azure/iot-hub- ## Multi-Step Ordered Execution (MSOE) Support -Based on many Public Preview customers' requests for the ability to run pre-install and post-install tasks, a new feature called Multi-Step Ordered Execution (MSOE) has been added to Device Update platform. The MSOE data is part of the Update Manifest v4 schema. - -There are 2 types of Steps: - -- Inline Step -- Reference Step - -Example Update Manifest with 1 Inline Step: - -```json -{ - "updateId": {...}, - "isDeployable": true, - "compatibility": [ - { - "deviceManufacturer": "adu-device", - "deviceModel": "e2e-test" - } - ], - "instructions": { - "steps": [ - { - "description": "Example APT update that install libcurl4-doc on a host device.", - "handler": "microsoft/apt:1", - "files": [ - "apt-manifest-1.0.json" - ], - "handlerProperties": { - "installedCriteria": "apt-update-test-1.0" - } - } - ] - }, - "manifestVersion": "4.0", - "importedDateTime": "2021-11-16T14:54:55.8858676Z", - "createdDateTime": "2021-11-16T14:50:47.3511877Z" -} -``` - -Example Update Manifest with 2 Inline Steps: - -```json -{ - "updateId": {...}, - "isDeployable": true, - "compatibility": [ - { - "deviceManufacturer": "adu-device", - "deviceModel": "e2e-test" - } - ], - "instructions": { - "steps": [ - { - "description": "Install libcurl4-doc on host device", - "handler": "microsoft/apt:1", - "files": [ - "apt-manifest-1.0.json" - ], - "handlerProperties": { - "installedCriteria": "apt-update-test-2.2" - } - }, - { - "description": "Install tree on host device", - "handler": "microsoft/apt:1", - "files": [ - "apt-manifest-tree-1.0.json" - ], - "handlerProperties": { - "installedCriteria": "apt-update-test-tree-2.2" - } - } - ] - }, - "manifestVersion": "4.0", - "importedDateTime": "2021-11-16T20:21:33.6514738Z", - "createdDateTime": "2021-11-16T20:19:29.4019035Z" -} -``` - -Example Update Manifest with 1 Reference Step: - -- Parent Update - -```json -{ - "updateId": {...}, - "isDeployable": true, - "compatibility": [ - { - "deviceManufacturer": "adu-device", - "deviceModel": "e2e-test" - } - ], - "instructions": { - "steps": [ - { - "type": "reference", - "description": "Cameras Firmware Update", - "updateId": { - "provider": "contoso", - "name": "virtual-camera", - "version": "1.2" - } - } - ] - }, - "manifestVersion": "4.0", - "importedDateTime": "2021-11-17T07:26:14.7484389Z", - "createdDateTime": "2021-11-17T07:22:10.6014567Z" -} -``` - -- Child Update - -```json -{ - "updateId": { - "provider": "contoso", - "name": "virtual-camera", - "version": "1.2" - }, - "isDeployable": false, - "compatibility": [ - { - "group": "cameras" - } - ], - "instructions": { - "steps": [ - { - "description": "Cameras Update - pre-install step", - "handler": "microsoft/script:1", - "files": [ - "contoso-camera-installscript.sh" - ], - "handlerProperties": { - "scriptFileName": "contoso-camera-installscript.sh", - "arguments": "--pre-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path", - "installedCriteria": "contoso-virtual-camera-1.2-step-0" - } - }, - { - "description": "Cameras Update - firmware installation (failure - missing file)", - "handler": "microsoft/script:1", - "files": [ - "contoso-camera-installscript.sh", - "camera-firmware-1.1.json" - ], - "handlerProperties": { - "scriptFileName": "missing-contoso-camera-installscript.sh", - "arguments": "--firmware-file camera-firmware-1.1.json --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path", - "installedCriteria": "contoso-virtual-camera-1.2-step-1" - } - }, - { - "description": "Cameras Update - post-install step", - "handler": "microsoft/script:1", - "files": [ - "contoso-camera-installscript.sh" - ], - "handlerProperties": { - "scriptFileName": "contoso-camera-installscript.sh", - "arguments": "--post-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path", - "installedCriteria": "contoso-virtual-camera-1.2-step-2" - } - } - ] - }, - "referencedBy": [ - { - "provider": "ADU-Client-Eng", - "name": "MSOE-Update-Demo", - "version": "3.1" - } - ], - "manifestVersion": "4.0", - "importedDateTime": "2021-11-17T07:26:14.7376536Z", - "createdDateTime": "2021-11-17T07:22:09.2232968Z", - "etag": "\"ad7a553d-24a8-492b-9885-9af424d44d58\"" -} -``` +Please see the [Multi-Step Update Manifest](https://learn.microsoft.com/en-us/azure/iot-hub-device-update/device-update-multi-step-updates) documentation. ## Parent Update vs. Child Update -For Public Preview Refresh, we will refer to the top-level Update Manifest as `Parent Update` and refer to an Update Manifest specified in a Reference Step as `Child Update`. +For Public Preview Refresh, we will refer to the top-level Update Manifest as `Parent Update` and refer to an Update Manifest specified in a Reference Step as `Child Update`. Currently, a `Child Update` must not contain any Reference Steps. This restriction is validate at import time. If violated, the import will fail. ### Inline Step In Parent Update -Inline step(s) specified in `Parent Update` will be applied to the Host Device. Here the ADUC_WorkflowData object that is passed to a Step Handler (aka. Update Content Handler) will not contains a `Selected Components` data. The handler for this type of step should not be a `Component-Aware` handler. +Inline step(s) specified in `Parent Update` will be applied to the Host Device. Here the ADUC_WorkflowData object that is passed to a Step Handler (aka. Update Content Handler) will not contains a `Selected Components` data. The handler for this type of step should not be a `Component-Aware` handler. -> **Note** | See [Steps Content Handler](../../src/content_handlers/steps_handler/README.md) and [Implementing a Component-Aware Content Handler](./how-to-implement-custom-update-handler.md#implementing-a-component-aware-content-handler) for more details. +> **Note** | See [Steps Content Handler](../../src/extensions/step_handlers/steps_handler/README.md) and [Implementing a Component-Aware Content Handler](./how-to-implement-custom-update-handler.md#implementing-a-component-aware-content-handler) for more details. ### Reference Step In Parent Update -Reference step(s) specified in `Parent Update` will be applied to the component on or components connected to the Host Device. A **Reference Step** is a step that contains update identifier of another Update, called `Child Update`. When processing a Reference Step, Steps Handler will download a Detached Update Manifest file specified in the Reference Step data, then validate the file integrity. +Reference step(s) specified in `Parent Update` will be applied to the component on or components connected to the Host Device. A **Reference Step** is a step that contains update identifier of another Update, called `Child Update`. When processing a Reference Step, Steps Handler will download a Detached Update Manifest file specified in the Reference Step data, then validate the file integrity. -Next, the Steps Handler will parse the Child Update Manifest and create ADUC_Workflow object (aka. Child Workflow Data) by combining the data from Child Update Manifest and File URLs information from the Parent Update Manifest. This Child Workflow Data also has a 'level' property set to '1'. +Next, the Steps Handler will parse the Child Update Manifest and create ADUC_Workflow object (aka. Child Workflow Data) by combining the data from Child Update Manifest and File URLs information from the Parent Update Manifest. This Child Workflow Data also has a 'level' property set to '1'. -> Note: for Update Manfiest version v4, the Child Udpate cannot contain any Reference Steps. +> Note: for Update Manifest version v4, the Child Update cannot contain any Reference Steps. ## Detached Update Manifest To avoid deployment failure due to IoT Hub Twin Data Size Limit, any large Update Manifest will be delivered in a form of JSON data file, called 'Detached Update Manifest'. -If an update with large content is imported into Device Update for IoT Hub, the generated Update Manifest will contain an additional payload file called `Detached Update Manifest` which contians a full data of the Update Manifest. +If an update with large content is imported into Device Update for IoT Hub, the generated Update Manifest will contain an additional payload file called `Detached Update Manifest` which contains a full data of the Update Manifest. The `UpdateManifest` property in the Device or Module Twin will contains the Detached Update Manifest file information. diff --git a/docs/agent-reference/upgrade-guide.md b/docs/agent-reference/upgrade-guide.md deleted file mode 100644 index 2f6870c25..000000000 --- a/docs/agent-reference/upgrade-guide.md +++ /dev/null @@ -1,180 +0,0 @@ -# DU Agent Upgrade Guide - -## Configuration File Changes - -|| Public Preview | Public Preview Refresh| -|----|----|----| -| File name| /etc/adu/adu-conf.txt | /etc/adu/du-config.json | -| File name (Yocto Reference Image)| /adu/adu-conf.txt | /adu/du-config.json | -| File format| Text | JSON document| - -### DU Agent Settings Changes - -The DU Agent properties can be specified in the `agents` array. Note that for Public Preview Refresh, all agent properties will be read from `agents[0]`. - - - - - - - - - - - - - - - - - - -
DU Agent PropertiesPublic PreviewPublic Preview Refresh
-Manufacturer and Model - - -```text -... - -aduc_manufacturer= -aduc_model= - -... -``` - - - -The `manufacturer` and `model` must be specified in an `agents[0].manufacturer` and `agents[0].model`, respectively. - -For example: - -```json -{ - -... - - "agents": [ - { - "name": "...", - "runas": "...", - "connectionSource": { ... }, - "manufacturer": "", - "model": "" - } - ] - -... - -} -``` -
IoT Hub Connection - - -```text -... - -connection_string= - -... -``` - - - -```json -{ - -... - - "agents": [ - { - "name": "", - "runas": "adu", - "connectionSource": { - "connectionType": "string", - "connectionData": "" - }, - "manufacturer": "...", - "model": "..." - } - ] - -... - -} -``` - -
- -## Install Device Update Agent Package from packages.microsoft.com - -> **Prerequisite** | Ensure that the device can access the Microsoft installation packages by following [this instruction](https://docs.microsoft.com/azure/iot-edge/how-to-provision-single-device-linux-symmetric?view=iotedge-2020-11&preserve-view=true&tabs=azure-portal#access-the-microsoft-installation-packages) - -For a device that previously installed the DU Agent Public Preview version from packages.microsoft.com, you can choose from following upgrade options: - -### Option 1 - Manually install debian package - -You can manually install a new version of `deviceupdate-agent` debian package from packages.microsoft.com (for supported distros and architectures) by remotely running following command on the device: - -```sh -sudo apt-get purge adu-agent #In case you have an old adu-agent in the system -sudo apt-get purge deviceupdate-agent -sudo apt-get install deviceupdate-agent -``` - ->**Note** | Please mind potential data loss when you use `purge`. If needed, save a copy of your configuration file, log files, etc. - -### Option 2 - Deploy an APT update using ADU Service - -To upgrade devices that currently connected to IoT Hub using Device Update Service Deployment (APT Update) - -#### 1. Create an APT Update Manifest using this template - -> **Note** | replace `name` and `version` accordingly. - -````json -{ - "name": "", - "version": "", - "packages": [ - { - "name": "deviceupdate-agent", - } - ] -} -```` - -#### 2. Import the update to ADU service - -See [this document](../../tools/AduCmdlets/README.md) for how to import an update to ADU service. - ->**Note** ->1. Specify `provider`, `name`, and `version` as appropriate. ->2. The DU Agent Public Preview version only support an Update Manifest version 2. The Update Manifest version must be specified correctly when importing the update. - -#### 3. Deploy the update - -Follow [Deploy A Device Update Guide](https://docs.microsoft.com/en-us/azure/iot-hub-device-update/deploy-update) to deploy the update imported in above step to a desired group of device. - -Once the target device(s) installed and applied the update, the DU Agent on the device should automatically restart and reconnect to the IoT Hub. - -## Post-upgrade Step - -### Update the ADUGroup property in Device/Module Twin - -Since the new DU Agent using newer version of IoT Hub PnP (Device Update) Interface, every device with upgraded DU Agent will be removed from previous Device Group, and must be re-assigned to a new Device Group. - -See [Create Update Group](https://docs.microsoft.com/en-us/azure/iot-hub-device-update/create-update-group) for more details. - -### Fill in du-config.json - -Option 1: -Manually fill in the configuration file with the instructions in **du-config.json** file. Note that values like `aduShellTrustedUsers` and `runas` are default values, you can change them as needed. - -Option 2: -If you have an existing saved copy of **adu-conf.txt** file, You can run this script [config-integration.sh](../../scripts/config-integration.sh) to migrate from text format to the new **du-config.json** file in json format. - -```sh -. scripts/config-integration.sh [copy of adu-conf.txt file] -``` - -Please check the **du-config.json** file after calling the script, to ensure all mandatory fields are filled in. diff --git a/docs/agent-reference/whats-new.md b/docs/agent-reference/whats-new.md deleted file mode 100644 index e1772a9a1..000000000 --- a/docs/agent-reference/whats-new.md +++ /dev/null @@ -1,35 +0,0 @@ -# Welcome to the Device Update Agent Private Preview Refresh (PPR) Release - -## What's New In This Version? - -- New Device Update [Configurations file](./configurations-file.md) -- [Device Diagnostic Service](./device-diagnostic-service.md) Support -- Support new [Update Manifest Version 4](./update-manifest-v4-schema.md), which enables the following features: - - [Multi Step Ordered Execution](./update-manifest-v4-schema.md#multi-step-ordered-execution-msoe-support) (MSOE) - - [Multi Component Updating](./multi-component-updating.md) - - [Goal State](./goal-state-support.md) Deployment - - [Detached Update Manifest](./update-manifest-v4-schema.md) -- Agent Extensibility Support - - [Update Content Handler Extension](./how-to-implement-custom-update-handler.md) - - Added [APT Update Handler](../../src/content_handlers/apt_handler) - - Added Script Handler - - Added [Simulator Handler](../../src/content_handlers/simulator_handler) - - Added [Steps Handler](../../src/content_handlers/steps_handler/README.md) - - Added [SWUpdate Handler](../../src/content_handlers/swupdate_handler) - - - [Component Enumerator Extension](../../src/extensions/component-enumerators/examples/contoso-component-enumerator/README.md) - - [Content Downloader Extension](../../src/extensions/content-downloaders/README.md) - -## Backward Compatibility - -Device Update Agent PPR supports both Update Manifest version 2 and version 4. This means, the same Agent can process existing updates those were imported and intended for IoT Device with Device Update Agent Public Preview version installed. - -See [Update Manifest Version 4 Schema](./update-manifest-v4-schema.md) for more details. - -## Upgrade From Public Preview Agent to the Public Preview Refresh Agent - -See the [Upgrade Guide](./upgrade-guide.md) for step-by-step instructions on how to update existing devices to support the new Public Preview Refresh features. - -## Device Update Quickstart - -[Getting Started With Device Update](quickstarts/overview-quickstart.md) diff --git a/docs/management-api-reference/cancel-or-retry-deployment.md b/docs/management-api-reference/cancel-or-retry-deployment.md deleted file mode 100644 index 67205521a..000000000 --- a/docs/management-api-reference/cancel-or-retry-deployment.md +++ /dev/null @@ -1,51 +0,0 @@ -# Cancel or Retry Deployment - -Cancel or retry failed devices for a deployment. - -## HTTP Request - -```http -POST https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/deployments/{deploymentId}?action=cancel -``` - -## URI Parameters - -Name|Input|Required|Type|Description ----|---|---|---|--- -{account} | hostname | True | string | ADU account name -{instance} | hostname | True | string | ADU instance name -{deploymentId} | path | True | string | The deploymentId returned by the Get deployment API -action | path | True | string | The action to take on the deployment. To cancel, set the value to `cancel`. To retry failed devices, set the value to `retry` - -## Request Headers - -Name|Required|Description ----|---|--- -Authorization | True | Authorization request header with the caller's AAD token passed as an OAuth 2.0 bearer token. | - -## Request Body - -The request body should be empty. - -## Responses - -Name|Type|Description ----|---|--- -200 OK | deployment | Deployment was successfully canceled (synchronously). The deployment's status is returned as a `Deployment` object. -404 NOT FOUND | none | Deployment not found. - -## Definitions - -### Deployment object - -Name|Required|Type|Description ----|---|---|--- -DeploymentId | True | string | The Deployment ID. -DeviceClassId | True | string | The device class ID. -DeviceGroupType | True | DeviceGroupType enum | Indicates the group type of the deployment. Valid values are `All`, `Devices`, and `DeviceGroupDefinitions`. -DeploymentType | True | DeploymentType enum | Indicates the type of the deployment. The only valid values is `Complete`. -DeviceGroupDefinition | True | Array of strings | List of group definitions associated with the deployment. -StartDateTime | True | DateTime | Beginning of the time window during which the deployment may start. -EndDateTime | True | DateTime | End of the time window during which the deployment may start. -ContentId | True | string | The ContentId of the deployment. -IsCanceled | True | Boolean | Flag indicating whether the deployment has been requested to be canceled. diff --git a/docs/management-api-reference/create-or-update-deployment.md b/docs/management-api-reference/create-or-update-deployment.md deleted file mode 100644 index 9f8e28f18..000000000 --- a/docs/management-api-reference/create-or-update-deployment.md +++ /dev/null @@ -1,94 +0,0 @@ -# Create or Update Deployment - -Create or update a Deployment - -## HTTP Request - -```http -PUT https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/deployments/{deploymentId} -``` - -## Deployment Resource Object - -Name| Type | Description |Required -----|------|------|--------- -deploymentId | String | The name of your deployment that must be unique | True -deploymentType | Enum | The supported type is "complete" | True -deviceClassId | String | This is returned from the get device classes API, must not be included if deploying to a device group | True -startDateTime | Long Time | The start time requires ISO-8601 format | True -deviceGroupType| Enum | The DeviceGroupType enum |True -deviceGroupDefinition| String | In the case of deviceGroupType = all, this must be empty. For devices, it is the list of deviceIds for deviceGroupDefinitions it is the list of group Ids | True -updateId | Object | The update resource type | True - -```json - -{ - "deploymentId": "myDeploymentId", - "deploymentType": "complete", - "deviceClassId": "c83e3c87fbf98074c20c3269f1c9e58d255906dd", - "startDateTime": "2020-05-27T19:25:57.6448039Z", - "deviceGroupType": "devices", - "deviceGroupDefinition": [ - "myDevice1", - "myDevice2" - ], - "updateId": { - "provider": "contoso", - "name": "virtual-machine", - "version": "3.0" - } -} -``` - -## Deployment Group Type enum - -Name| Description| -----|------ -All | Specify all devices in device class -Devices | The exact list of devices specified in the device group list must at least have 1 and not exceed 100 -DeviceGroupDefinitions | Group Ids defined in ADU must number between 1 and not exceed 20 - -## Update Id - -Name| Type | Description| Required -----|------|------|------ -provider | string |The provider defined when update is imported | True -name | string | The name of the update defined when imported | True -version | string |The version of the update defined when imported | True - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required -Content-Type | application/json - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | Deployment was created or updated successfully (synchronously).| -| 400 BAD REQUEST | Returned if endDateTime is not at least 1 hour later than startDateTime, or if any of the provided values in the request are invalid data.| -| 404 NOT FOUND | Deployment not found | - -```json -{ - "deploymentId": "myDeploymentId", - "deploymentType": "Complete", - "deviceClassId": "c83e3c87fbf98074c20c3269f1c9e58d255906dd", - "startDateTime": "2020-05-27T19:25:57.6448039+00:00", - "deviceGroupType": "Devices", - "deviceGroupDefinition": [ - "myDevice1", - "myDevice2" - ], - "updateId": { - "provider": "contoso", - "name": "virtual-machine", - "version": "3.0" - }, - "isCanceled": false, - "isCompleted": false, - "isRetry": false -} -``` diff --git a/docs/management-api-reference/delete-deployment.md b/docs/management-api-reference/delete-deployment.md deleted file mode 100644 index 1654b9868..000000000 --- a/docs/management-api-reference/delete-deployment.md +++ /dev/null @@ -1,34 +0,0 @@ -# Delete Deployment - -Deletes the deployment from the ADU instance. - -## HTTP Request - -```http -DELETE https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/deployments/{deploymentId} -``` - -## URI Parameters - -Name|Input|Required|Type|Description ----|---|---|---|--- -{account} | hostname | True | string | ADU account name -{instance} | hostname | True | string | ADU instance name -{deploymentId} | path | True | string | The deploymentId returned by the Get deployment API - -## Request Headers - -Name|Required|Description ----|---|--- -Authorization | True | Authorization request header with the caller's AAD token passed as an OAuth 2.0 bearer token. | - -## Request Body - -The request body should be empty. - -## Responses - -Name|Type|Description ----|---|--- -200 OK | deployment | Deployment was deleted successfully (synchronously). -404 NOT FOUND | none | Deployment not found. diff --git a/docs/management-api-reference/get-available-device-tags.md b/docs/management-api-reference/get-available-device-tags.md deleted file mode 100644 index cd0746c8b..000000000 --- a/docs/management-api-reference/get-available-device-tags.md +++ /dev/null @@ -1,44 +0,0 @@ -# Get Available Device Tags - -Gets the list of ADUGroup IoT Hub tags that are available to use to create new device groups - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/devicetags - -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance| Hostname|True|String|ADU Instance Name - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | The list of available device tags | - -```json -{ - "value": [ - { - "tagName": "group1", - "deviceCount": 100 - }, - { - "tagName": "group2", - "deviceCount": 200 - } - ] -} -``` diff --git a/docs/management-api-reference/get-deployment-details.md b/docs/management-api-reference/get-deployment-details.md deleted file mode 100644 index 1439219c7..000000000 --- a/docs/management-api-reference/get-deployment-details.md +++ /dev/null @@ -1,45 +0,0 @@ -# Get Deployment Details - -Get the details of a deployment when it was created - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/deployments/{deploymentId} -``` - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required -Content-Type | application/json - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | The details of the deployment | -| 404 NOT FOUND | Deployment not found | - -```json -{ - "deploymentId": "myDeploymentId", - "deviceClassId": "c83e3c87fbf98074c20c3269f1c9e58d255906dd", - "deploymentType": "Complete", - "startDateTime": "2019-07-12T16:29:56.5770502Z", - "deviceGroupType": "Devices", - "deviceGroupDefinition": [ - "myDevice1", - "myDevice2" - ], - "updateId": { - "provider": "myProvider", - "name": "myName", - "version": "1.2.3.4" - }, - "isCanceled": true, - "isCompleted": false, - "isRetry": false -} -``` diff --git a/docs/management-api-reference/get-deployment-device-states.md b/docs/management-api-reference/get-deployment-device-states.md deleted file mode 100644 index 1f253b85c..000000000 --- a/docs/management-api-reference/get-deployment-device-states.md +++ /dev/null @@ -1,77 +0,0 @@ -# Get Deployment Device States - -Get devices in the deployment along with their deployment state. - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/deployments/{deploymentId}/devicestates -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance| Hostname|True|String|ADU Instance Name -deploymentId | path | True | string | The deploymentId returned by the Get Deployment API - -### deviceState query values - -Name| Description -----|------ -Incompatible | Incompatible device -InProgress | Update in progress -CompletedFailed | Deployment failed -CompletedSucceeded | Deployment succeeded -Canceled | Deployment Canceled -AlreadyInDeployment | Deployment being rolled out - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | The devices in the deployment | -| 404 NOT FOUND | Deployment not found | - -```json -{ - "value": [ - { - "DeviceId": "testdevice1", - "DeviceState": enum, - "RetryCount": 1, - "MovedOnToNewDeployment": "false" - }, - { - "DeviceId": "testdevice2", - "DeviceState": enum, - "RetryCount": 1, - "MovedOnToNewDeployment": "true" - }, - { - "DeviceId": "testdevice3", - "DeviceState": enum, - "RetryCount": 3, - "MovedOnToNewDeployment": "false" - } - ] -} -``` - -## Device State Type enum - -Name|Description -----|------| -Incompatible | Device is incompatible with the update in the deployment -AlreadyInDeployment | Device is not receiving the deployment because it is already in another deployment -InProgress | Device is currently in progress on the deployment -CompletedFailed | Device has completed the deployment but failed -CompletedSucceeded | Device has completed the deployment and succeeded -Canceled | Device has been canceled from this deployment diff --git a/docs/management-api-reference/get-deployment-devices.md b/docs/management-api-reference/get-deployment-devices.md deleted file mode 100644 index 67b87ce11..000000000 --- a/docs/management-api-reference/get-deployment-devices.md +++ /dev/null @@ -1,50 +0,0 @@ -# Get Deployment Devices - -Get devices in the deployment, including a subset of devices in a particular device state within the deployment. - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v1/management/deployments/{deploymentId}/devicelist -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance| Hostname|True|String|ADU Instance Name -deploymentId | path | True | string | The deploymentId returned by the Get Deployment API -deviceState | path | False | string | The state of a device. - -### deviceState query values - -Name| Description -----|------ -Incompatible | Incompatible device -NotStarted | Update has not started -InProgress | Update in progress -CompletedFailed | Deployment failed -CompletedSucceeded | Deployment succeeded -Canceled | Deployment Canceled -AlreadyInDeployment | Deployment being rolled out - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | The devices in the deployment | -| 404 NOT FOUND | Deployment not found | - -```json -[ - "device1", - "device2" -] -``` diff --git a/docs/management-api-reference/get-deployment-status.md b/docs/management-api-reference/get-deployment-status.md deleted file mode 100644 index 0122820a6..000000000 --- a/docs/management-api-reference/get-deployment-status.md +++ /dev/null @@ -1,51 +0,0 @@ -# Get Deployment Status - -Get the status of a deployment - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/deployments/{deploymentId}/status -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance| Hostname|True|String|ADU Instance Name -deploymentId | path | True | string | The deploymentId returned by the Get Deployment API - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required -Content-Type | application/json - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | The status of the deployment | -| 404 NOT FOUND | Deployment not found | - -```json -{ - "deploymentState": enum, - "TotalDevices": 0, - "DevicesIncompatibleCount": 0, - "DevicesAlreadyInDeploymentCount": 0, - "DevicesInProgressCount": 0, - "DevicesCompletedFailedCount": 0, - "DevicesCompletedSucceededCount": 0, - "DevicesCanceledCount": 0 -} -``` - -## Deployment State Type enum - -Name|Description -----|------| -Active | Deployment is Active and not Canceled -Canceled | Deployment has been Canceled diff --git a/docs/management-api-reference/get-deployments.md b/docs/management-api-reference/get-deployments.md deleted file mode 100644 index bac48c47c..000000000 --- a/docs/management-api-reference/get-deployments.md +++ /dev/null @@ -1,65 +0,0 @@ -# Get Deployments - -Get the list of all deployments that have been created in this instance - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/deployments -``` - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required -Content-Type | application/json - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | List of all deployments in this instance | - -```json -{ - "value": [ - { - "deploymentId": "testdeploymentid1", - "deploymentType": "Complete", - "deviceClassId": "c94e3c87fbf98063c20c3269f1c9e58d255906dd", - "startDateTime": "2019-09-01T16:29:56.5770502+00:00", - "deviceGroupType": "Devices", - "deviceGroupDefinition": [ - "test-device-1" - ], - "updateId": { - "provider": "contoso", - "name": "virtual-machine", - "version": "3.0" - }, - "isCanceled": true, - "isCompleted": false, - "isRetry": false - }, - { - "deploymentId": "e20dc00f-879a-46b0-a544-b1d1f64da83c", - "deploymentType": "Complete", - "deviceClassId": "abce3c87fbf98063c20c3269f1c9e58d255907ee", - "startDateTime": "2019-09-01T16:29:56.5770502+00:00", - "deviceGroupType": "Devices", - "deviceGroupDefinition": [ - "test-device-2" - ], - "updateId": { - "provider": "fabrikam", - "name": "thermometer", - "version": "10.0" - }, - "isCanceled": false, - "isCompleted": false, - "isRetry": false - } - ] -} -``` diff --git a/docs/management-api-reference/get-device-class-details.md b/docs/management-api-reference/get-device-class-details.md deleted file mode 100644 index ee200a80d..000000000 --- a/docs/management-api-reference/get-device-class-details.md +++ /dev/null @@ -1,51 +0,0 @@ -# Get Device Class Details - -Gets the details of a device class. - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v1/management/deviceclasses/{deviceClassId} - -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance| Hostname|True|String|ADU Instance Name -deviceClassId | path | True | string | A device class ID as returned by the Get Device Classes API - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | The details of the device class | -| 404 NOT FOUND | Device class not found | - -Name|Type|Description -----|------|------| -DeviceClassId| String|Unique identifier for the device. -Manufacturer| String|Manufacturer of the device class -Model| String | Model name of the device class -bestCompatibleUpdateId | UpdateId | The best compatible updateId for the device class - -```json -{ - "deviceClassId": "d74e3c87fbf98074c20c3269f1c9e58d255906dd", - "manufacturer": "Contoso", - "model": "Virtual-Machine", - "bestCompatibleUpdateId": { - "provider": "Contoso", - "name": "Virtual-Machine", - "version": "10.0" - } -} -``` diff --git a/docs/management-api-reference/get-device-classes-for-a-device.md b/docs/management-api-reference/get-device-classes-for-a-device.md deleted file mode 100644 index 397d52cb3..000000000 --- a/docs/management-api-reference/get-device-classes-for-a-device.md +++ /dev/null @@ -1,35 +0,0 @@ -# Get Device Classes for a Device - -Gets a list of device classes that the given device belongs to. - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v1/management/devices/{deviceId}/deviceclasslist -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance | Hostname|True|String|ADU Instance Name -deviceId | path | True | string | A device ID as returned by the Get Devices in Device Class API. - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | List of device classes that the device belongs to | - -```json -[ - "deviceClassId1", - "deviceClassId2" -] diff --git a/docs/management-api-reference/get-device-classes.md b/docs/management-api-reference/get-device-classes.md deleted file mode 100644 index d571ee64e..000000000 --- a/docs/management-api-reference/get-device-classes.md +++ /dev/null @@ -1,55 +0,0 @@ -# Get Device Classes - -Gets the list of device classes being managed by the ADU instance. - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/deviceclasses -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance | Hostname|True|String|ADU Instance Name - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | List of device classes in the instance | - -```json -{ - "value": [ - { - "deviceClassId": "c83e3c87fbf98074c20c3269f1c9e58d255906dd", - "manufacturer": "Contoso", - "model": "Virtual-Machine", - "bestCompatibleUpdateId": { - "provider": "Contoso", - "name": "Virtual-Machine", - "version": "16.0" - } - }, - { - "deviceClassId": "d20d7ecefcf9b64a9078aaed3a0e04d44c42779c", - "manufacturer": "Contoso", - "model": "Video" - }, - { - "deviceClassId": "8431f6d0c986d38a4162db561b7a494aec60c3e4", - "manufacturer": "fabrikam", - "model": "virtual-machine" - } - ] -} -``` diff --git a/docs/management-api-reference/get-device-ids-in-device-class.md b/docs/management-api-reference/get-device-ids-in-device-class.md deleted file mode 100644 index 88f8706e8..000000000 --- a/docs/management-api-reference/get-device-ids-in-device-class.md +++ /dev/null @@ -1,38 +0,0 @@ -# Get Device Ids in Device Class - -Gets the list of device ids in a device class. - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/deviceclasses/{deviceClassId}/deviceids -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance| Hostname|True|String|ADU Instance Name -deviceClassId | path | True | string | A device class ID as returned by the Get Device Classes API - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required - -## Responses - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | List of devices in the device class | - -```json -[ - "deviceId1", - "deviceId2" -] -``` diff --git a/docs/management-api-reference/get-device-tag.md b/docs/management-api-reference/get-device-tag.md deleted file mode 100644 index 1132c1298..000000000 --- a/docs/management-api-reference/get-device-tag.md +++ /dev/null @@ -1,36 +0,0 @@ -# Get Device Tag - -Gets details for an ADUGroup device tag - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/devicetags/{tagName} - -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance| Hostname|True|String|ADU Instance Name - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | The device tag details | - -```json -{ - "tagName": "group1", - "deviceCount": 100 -} -``` diff --git a/docs/management-api-reference/get-device.md b/docs/management-api-reference/get-device.md deleted file mode 100644 index 570d300e3..000000000 --- a/docs/management-api-reference/get-device.md +++ /dev/null @@ -1,52 +0,0 @@ -# Get Device - -Gets the properties and last deployment status for a device - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/devices/{deviceId} -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance| Hostname|True|String|ADU Instance Name -deviceId | path | True | string | The device ID - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | The properties and last deployment status for the device | - -```json -{ - "deviceId": "myDeviceId", - "deviceClassId": "c83e3c87fbf98074c20c3269f1c9e58d255906dd", - "manufacturer": "Contoso", - "model": "Virtual-Machine", - "lastAttemptedUpdateId": { - "provider": "contoso", - "name": "virtual-machine", - "version": "3.0" - }, - "installedUpdateId": { - "provider": "contoso", - "name": "virtual-machine", - "version": "3.0" - }, - "onLatestUpdate": false, - "deploymentStatus": "Succeeded", - "groupId": "myGroupId", - "lastDeploymentId": "myDeploymentId" -} -``` diff --git a/docs/management-api-reference/get-devices.md b/docs/management-api-reference/get-devices.md deleted file mode 100644 index aef941828..000000000 --- a/docs/management-api-reference/get-devices.md +++ /dev/null @@ -1,87 +0,0 @@ -# Get Devices - -Gets the list of devices being managed by the ADU instance. - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/devices -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance | Hostname|True|String|ADU Instance Name - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | List of device classes in the instance | - -```json -{ - "value": [ - { - "deviceId": "test-device-1", - "deviceClassId": "f38bc5237d50fee933b11146d6804f3acdd605dd", - "manufacturer": "Contoso", - "model": "testmodel1", - "lastAttemptedUpdateId": { - "provider": "Contoso", - "name": "testmodel1", - "version": "1.0.1354.1" - }, - "installedUpdateId": { - "provider": "Contoso", - "name": "testmodel1", - "version": "1.0.1354.1" - }, - "onLatestUpdate": false, - "deploymentStatus": "Failed", - "groupId": null, - "lastDeploymentId": "testdeploymentid1" - }, - { - "deviceId": "test-device-2", - "deviceClassId": "d08c9c774a1c541ecc88e8c65b6431fd8b7eafc3", - "manufacturer": "Microsoft-Corporation", - "model": "Virtual-Machine", - "lastAttemptedUpdateId": null, - "installedUpdateId": null, - "onLatestUpdate": false, - "deploymentStatus": "InProgress", - "groupId": "groupname1", - "lastDeploymentId": "testdeploymentid2" - }, - { - "deviceId": "test-device-3", - "deviceClassId": "d08c9c774a1c541ecc88e8c65b6431fd8b7eafc3", - "manufacturer": "microsoft-corporation", - "model": "virtual-machine", - "lastAttemptedUpdateId": { - "provider": "microsoft-corporation", - "name": "virtual-machine", - "version": "111.0" - }, - "installedUpdateId": { - "provider": "microsoft-corporation", - "name": "virtual-machine", - "version": "111.0" - }, - "onLatestUpdate": true, - "deploymentStatus": "Succeeded", - "groupId": null, - "lastDeploymentId": "testdeploymentid3" - } - ] -} -``` diff --git a/docs/management-api-reference/get-group-best-updates.md b/docs/management-api-reference/get-group-best-updates.md deleted file mode 100644 index 92386cc75..000000000 --- a/docs/management-api-reference/get-group-best-updates.md +++ /dev/null @@ -1,61 +0,0 @@ -# Get best updates for a group - -Get the best updates for a group - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/groups/{groupId}/bestupdates -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance| Hostname|True|String|ADU Instance Name -GroupId| Hostname|True|String|Group Id - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required -Content-Type | application/json - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | The best updates for a group - -```json -{ - "value": [ - { - "UpdateId": { - "provider": "Contoso", - "name": "Virtual-Machine", - "version": "16.0" - }, - "deviceCount": 100 - }, - { - "UpdateId": { - "provider": "Fabrikam", - "name": "thermometer", - "version": "1.0" - }, - "deviceCount": 200 - }, - { - "UpdateId": { - "provider": "Contoso", - "name": "lamp", - "version": "3.14" - }, - "deviceCount": 50 - } - ] -} -``` diff --git a/docs/management-api-reference/get-group-update-compliance.md b/docs/management-api-reference/get-group-update-compliance.md deleted file mode 100644 index 3888c074b..000000000 --- a/docs/management-api-reference/get-group-update-compliance.md +++ /dev/null @@ -1,39 +0,0 @@ -# Get Update Compliance for a group - -Get the update compliance status for a group - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/groups/{groupId}/updatecompliance -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance| Hostname|True|String|ADU Instance Name -GroupId| Hostname|True|String|Group Id - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required -Content-Type | application/json - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | The update compliance status of a group - -```json -{ - "totalDeviceCount": 5, - "onLatestUpdateDeviceCount": 3, - "newUpdatesAvailableDeviceCount": 1, - "updatesInProgressDeviceCount": 1 -} -``` diff --git a/docs/management-api-reference/get-group.md b/docs/management-api-reference/get-group.md deleted file mode 100644 index 62a4945fe..000000000 --- a/docs/management-api-reference/get-group.md +++ /dev/null @@ -1,40 +0,0 @@ -# Get Group Details - -Gets details for an ADU Device Group - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/groups/{groupId} - -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance| Hostname|True|String|ADU Instance Name -GroupId| Hostname|True|String|Group Id - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | The group details | - -```json -{ - "groupId": "mygroup", - "tags": [ "mygroup" ], - "createdDateTime": "2020-10-24T00:32:09.7197303+00:00", - "groupType": "IoTHubTag", - "deviceCount": 10 -} -``` diff --git a/docs/management-api-reference/get-groups.md b/docs/management-api-reference/get-groups.md deleted file mode 100644 index ad0dd4b4d..000000000 --- a/docs/management-api-reference/get-groups.md +++ /dev/null @@ -1,50 +0,0 @@ -# Get Groups - -Gets a list of ADU groups - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/groups - -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance| Hostname|True|String|ADU Instance Name - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | The group details | - -```json -{ - "value": [ - { - "groupId": "mygroup", - "tags": [ "mygroup" ], - "createdDateTime": "2020-10-24T00:32:09.7197303+00:00", - "groupType": "IoTHubTag", - "deviceCount": 10 - }, - { - "groupId": "mygroup2", - "tags": [ "mygroup2" ], - "createdDateTime": "2020-11-24T00:32:09.7197303+00:00", - "groupType": "IoTHubTag", - "deviceCount": 100 - } - ] -} -``` diff --git a/docs/management-api-reference/get-installable-update-for-device-class.md b/docs/management-api-reference/get-installable-update-for-device-class.md deleted file mode 100644 index 44f585575..000000000 --- a/docs/management-api-reference/get-installable-update-for-device-class.md +++ /dev/null @@ -1,51 +0,0 @@ -# Get Installable Update for a Device Class - -Gets a list of updates in the ADU instance that are compatible with the given device class. - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/deviceclasses/{deviceClassId}/installableupdates -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance| Hostname|True|String|ADU Instance Name -deviceClassId | path | True | string | A device class ID as returned by the Get Device Classes API - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | List of updates compatible with the device class | -| 404 NOT FOUND | Device class not found | - -Name|Type|Description -----|------|------| -Provider| string|Provider part of the update identity -Name| string|Name part of the update identity -Version| version | Version part of the update identity - -```json -[ - { - "provider": "myProvider1", - "name": "name", - "version": "1.2.3.4" - }, - { - "provider": "myProvider2", - "name": "name2", - "version": "2.3.4.5" - } -] -``` diff --git a/docs/management-api-reference/get-update-compliance.md b/docs/management-api-reference/get-update-compliance.md deleted file mode 100644 index fa25da7c4..000000000 --- a/docs/management-api-reference/get-update-compliance.md +++ /dev/null @@ -1,38 +0,0 @@ -# Get Update Compliance for all devices - -Get the update compliance status for all devices - -## HTTP Request - -```http -GET https://{account}.api.adu.microsoft.com/deviceupdate/{instance}/v2/management/updatecompliance -``` - -## URI Parameters - -Name|Input|Required|Type|Description -----|------|------|------|------| -Account| Hostname|True|String|ADU Account Name -Instance| Hostname|True|String|ADU Instance Name - -## Request Headers - -Name|Description -----|------| -Authorization| Bearer.Required -Content-Type | application/json - -## Responses - -| HTTP Code | Description | -| :--------- | :---- | -| 200 OK | The update compliance status of all devices - -```json -{ - "totalDeviceCount": 5, - "onLatestUpdateDeviceCount": 3, - "newUpdatesAvailableDeviceCount": 1, - "updatesInProgressDeviceCount": 1 -} -``` diff --git a/docs/management-api-reference/management-api-overview.md b/docs/management-api-reference/management-api-overview.md deleted file mode 100644 index 6c38df9e1..000000000 --- a/docs/management-api-reference/management-api-overview.md +++ /dev/null @@ -1,46 +0,0 @@ -# Management APIs - -The management APIs enable you to select applicable updates for devices, deploy updates to devices and then view the status of updates during the deployment process. You are also able to add or remove devices from device groups as well as being able to view the update compliance of devices and groups. - -## Getting Started - -### Selecting applicable updates for devices - -After update has been published to ADU, the first item to do before creating a deployment is to match a device class to installable updates for a device. -* To do this first call [Get Device](get-device.md) to get the details including the device class id for the device you want to update. -* Once you have found the specific device class you are interested in, then use the [Get Installable Update for a Device Class](get-installable-update-for-device-class.md) to view the updates for a given Device Class. - -### Creating a deployment - -Once you have identified the applicable updates for a device, you will then create a deployment. To create a deployment use the [Create or Update Deployment](create-or-update-deployment.md). This api will let you specify the update, device class, and also whether to deploy to a set of specific devices or use a group Id to set the devices that will receive an update. - -### Viewing the state of a deployment - -Once an update has been deployed, you can view the detailed status of a deployment, to get general status (active, canceled), using [Get Deployment Status](get-deployment-status.md). To view the details of a specific deployment that was created use [Get Deployment Details](get-deployment-details.md). - -### Verify update has been deployed to a device - -After an update has completed or any time you can view the installed update on a device by calling [Get Device](get-device.md) - -## Complete List Of Management APIs - -* [Get Device Classes](get-device-classes.md) -* [Get Device Class Details](get-device-class-details.md) -* [Get Device Ids in Device Class](get-device-ids-in-device-class.md) -* [Get Installable Update for a Device Class](get-installable-update-for-device-class.md) -* [Get Devices](get-devices.md) -* [Get Device](get-device.md) -* [Get Available Device Tags](get-available-device-tags.md) -* [Get Device Tag](get-device-tag.md) -* [Get Groups](get-groups.md) -* [Get Group](get-group.md) -* [Get Group Update Compliance](get-group-update-compliance.md) -* [Get Group Best Updates](get-group-best-updates.md) -* [Get Update Compliance](get-update-compliance.md) -* [Create or Update Deployment](create-or-update-deployment.md) -* [Cancel or Retry Deployment](cancel-or-retry-deployment.md) -* [Delete Deployment](delete-deployment.md) -* [Get Deployments](get-deployments.md) -* [Get Deployment Status](get-deployment-status.md) -* [Get Deployment Details](get-deployment-details.md) -* [Get Deployment Device States](get-deployment-device-states.md) diff --git a/docs/sample-artifacts/sample-package-update-1.0.1-importManifest.json b/docs/sample-artifacts/sample-package-update-1.0.1-importManifest.json index bc3a55158..966fcb2dc 100644 --- a/docs/sample-artifacts/sample-package-update-1.0.1-importManifest.json +++ b/docs/sample-artifacts/sample-package-update-1.0.1-importManifest.json @@ -8,8 +8,8 @@ "installedCriteria": "Sample package update-1.0.1", "compatibility": [ { - "deviceManufacturer": "Contoso", - "deviceModel": "Video" + "manufacturer": "Contoso", + "model": "Video" } ], "files": [ diff --git a/docs/sample-artifacts/sample-package-update-1.0.2-importManifest.json b/docs/sample-artifacts/sample-package-update-1.0.2-importManifest.json index 02564a255..019339f22 100644 --- a/docs/sample-artifacts/sample-package-update-1.0.2-importManifest.json +++ b/docs/sample-artifacts/sample-package-update-1.0.2-importManifest.json @@ -8,8 +8,8 @@ "installedCriteria": "Sample package update-1.0.2", "compatibility": [ { - "deviceManufacturer": "Contoso", - "deviceModel": "Video" + "manufacturer": "Contoso", + "model": "Video" } ], "files": [ diff --git a/docs/sample-artifacts/sample-package-update-2-2.0.1-importManifest.json b/docs/sample-artifacts/sample-package-update-2-2.0.1-importManifest.json index d71b43ef8..8333eaee3 100644 --- a/docs/sample-artifacts/sample-package-update-2-2.0.1-importManifest.json +++ b/docs/sample-artifacts/sample-package-update-2-2.0.1-importManifest.json @@ -8,8 +8,8 @@ "installedCriteria": "Sample package update 2-2.0.1", "compatibility": [ { - "deviceManufacturer": "Contoso", - "deviceModel": "Video" + "manufacturer": "Contoso", + "model": "Video" } ], "files": [ diff --git a/licenses/cgmanifest.json b/licenses/cgmanifest.json index bb4186247..d0112426a 100644 --- a/licenses/cgmanifest.json +++ b/licenses/cgmanifest.json @@ -6,8 +6,8 @@ "Type": "git", "git": { "RepositoryUrl": "https://github.com/Microsoft/do-client", - "CommitHash_Comment": "0.7.0", - "CommitHash": "3f00d1e0f841e6376f1c2852079e19ccc00f8ec4" + "CommitHash_Comment": "v1.0.0", + "CommitHash": "b61de2d347c8032562056b18f90ec710e531baf8" } }, "DevelopmentDependency": false @@ -38,9 +38,31 @@ "Component": { "Type": "git", "git": { - "RepositoryUrl": "https://github.com/Microsoft/GSL", - "CommitHash_Comment": "v2.0.0", - "CommitHash": "1995e86d1ad70519465374fb4876c6ef7c9f8c61" + "RepositoryUrl": "https://github.com/sbabic/swupdate.git", + "CommitHash_Comment": "2021.11", + "CommitHash": "14d5d196304d408d61d4f1a07264f248201f46cc" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "git", + "git": { + "RepositoryUrl": "https://github.com/koalaman/shellcheck", + "CommitHash_Comment": "0.8.0", + "CommitHash": "e5ad4cf420a7f7b8e5eaac872b14a1619051cf10" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "git", + "git": { + "RepositoryUrl": "https://github.com/Azure/azure-blob-storage-file-upload-utility.git", + "CommitHash_Comment": "main", + "CommitHash": "6969d0fc9d222a5c132d0b8fb4305697a6017383" } }, "DevelopmentDependency": false diff --git a/packages/CMakeLists.txt b/packages/CMakeLists.txt index 34f01d484..676cb7cad 100644 --- a/packages/CMakeLists.txt +++ b/packages/CMakeLists.txt @@ -43,7 +43,7 @@ set (CPACK_DEBIAN_PACKAGE_SECTION "admin") # If remove deliveryoptimization-agent from dependencies, preinst script must be updated accordingly. # See https://www.debian.org/doc/debian-policy/ch-relationships.html#s-binarydeps -set (CPACK_DEBIAN_PACKAGE_DEPENDS "deliveryoptimization-agent, libdeliveryoptimization, libcurl4-openssl-dev") +set (CPACK_DEBIAN_PACKAGE_DEPENDS "deliveryoptimization-agent (>= 1.0.0), libdeliveryoptimization (>= 1.0.0), libcurl4-openssl-dev") set (CPACK_DEBIAN_PACKAGE_SUGGESTS "deliveryoptimization-plugin-apt") # Use dpkg-shlibdeps to generate better package dependency list. diff --git a/packages/debian/postinst b/packages/debian/postinst index 5f4c81aaa..47bc603d6 100644 --- a/packages/debian/postinst +++ b/packages/debian/postinst @@ -10,6 +10,8 @@ # Any error should exit the script (sort of) # See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html#The-Set-Builtin +echo "******************** Running $0 $1 ***********************" + set -e adu_conf_dir=/etc/adu @@ -30,10 +32,13 @@ adu_script_handler_file=libmicrosoft_script_1.so adu_simulator_handler_file=libmicrosoft_simulator_1.so adu_steps_handler_file=libmicrosoft_steps_1.so adu_swupdate_handler_file=libmicrosoft_swupdate_1.so +adu_swupdate_handler_2_file=libmicrosoft_swupdate_2.so + +adu_example_component_enumerator_file=libcontoso_component_enumerator.so -adu_example_component_enumerator_file=libcontoso-component-enumerator.so +adu_delivery_optimization_downloader_file=libdeliveryoptimization_content_downloader.so -adu_delivery_optimization_downloader_file=libdeliveryoptimization-content-downloader.so +adu_delta_download_handler_file=libmicrosoft_delta_download_handler.so adu_eis_conf_file=adu.toml eis_idservice_dir=/etc/aziot/identityd/config.d @@ -55,11 +60,12 @@ ms_doclient_lite_service="deliveryoptimization-agent" sample_du_config=$( cat << END_OF_JSON { - "schemaVersion": "1.0", + "schemaVersion": "1.1", "aduShellTrustedUsers": [ "adu", "do" ], + "iotHubProtocol": "mqtt", "manufacturer": , "model": , "agents": [ @@ -118,27 +124,27 @@ setup_dirs_and_files() { # Ensure the right owners and permissions chown "$adu_user:$adu_group" "$adu_conf_dir" - chmod u=rwx,g=rx,o=rx "$adu_conf_dir" + chmod u=rwx,g=rx,o= "$adu_conf_dir" # Generate the template configuration file echo "Generate the template configuration file..." if [ ! -f "$adu_conf_dir/${adu_conf_file}.template" ]; then echo "$sample_du_config" > "$adu_conf_dir/${adu_conf_file}.template" chown "$adu_user:$adu_group" "$adu_conf_dir/${adu_conf_file}.template" - chmod u=r,g=r,o=r "$adu_conf_dir/${adu_conf_file}.template" + chmod u=r,g=r,o= "$adu_conf_dir/${adu_conf_file}.template" fi # Create configuration file from template if [ ! -f "$adu_conf_dir/$adu_conf_file" ]; then cp -a "$adu_conf_dir/${adu_conf_file}.template" "$adu_conf_dir/$adu_conf_file" - chmod u=rw,g=r,o=r "$adu_conf_dir/$adu_conf_file" + chmod u=rw,g=r,o= "$adu_conf_dir/$adu_conf_file" fi echo "Generating the diagnostics configuration file..." if [ ! -f "$adu_conf_dir/$adu_diagnostics_conf_file" ]; then echo "$sample_du_diagnostics_config" > "$adu_conf_dir/$adu_diagnostics_conf_file" chown "$adu_user:$adu_group" "$adu_conf_dir/$adu_diagnostics_conf_file" - chmod u=r,g=r,o=r "$adu_conf_dir/$adu_diagnostics_conf_file" + chmod u=r,g=r,o= "$adu_conf_dir/$adu_diagnostics_conf_file" fi # Create home dir @@ -196,7 +202,7 @@ setup_dirs_and_files() { chown "$adu_user:$adu_group" "$adu_extensions_sources_dir" chmod u=rwx,g=rwx,o= "$adu_extensions_sources_dir" - #Set adu-agent owner and permission + #Set deviceupdate-agent owner and permission if [ ! -f "$adu_shell_dir/$adu_shell_file" ]; then echo "ERROR! $adu_shell_dir/$adu_shell_file does not exists." >&2 else @@ -212,18 +218,23 @@ setup_dirs_and_files() { register_reference_extensions() { echo "Register all reference update content handlers..." - $adu_bin_path -l 2 --update-type "microsoft/apt:1" -C $adu_extensions_sources_dir/$adu_apt_handler_file - $adu_bin_path -l 2 --update-type "microsoft/swupdate:1" -C $adu_extensions_sources_dir/$adu_swupdate_handler_file - $adu_bin_path -l 2 --update-type "microsoft/script:1" -C $adu_extensions_sources_dir/$adu_script_handler_file - $adu_bin_path -l 2 --update-type "microsoft/steps:1" -C $adu_extensions_sources_dir/$adu_steps_handler_file - $adu_bin_path -l 2 --update-type "microsoft/update-manifest" -C $adu_extensions_sources_dir/$adu_steps_handler_file - $adu_bin_path -l 2 --update-type "microsoft/update-manifest:4" -C $adu_extensions_sources_dir/$adu_steps_handler_file + $adu_bin_path -l 2 --extension-type updateContentHandler --extension-id "microsoft/apt:1" --register-extension $adu_extensions_sources_dir/$adu_apt_handler_file + $adu_bin_path -l 2 --extension-type updateContentHandler --extension-id "microsoft/swupdate:1" --register-extension $adu_extensions_sources_dir/$adu_swupdate_handler_file + $adu_bin_path -l 2 --extension-type updateContentHandler --extension-id "microsoft/swupdate:2" --register-extension $adu_extensions_sources_dir/$adu_swupdate_handler_2_file + $adu_bin_path -l 2 --extension-type updateContentHandler --extension-id "microsoft/script:1" --register-extension $adu_extensions_sources_dir/$adu_script_handler_file + $adu_bin_path -l 2 --extension-type updateContentHandler --extension-id "microsoft/steps:1" --register-extension $adu_extensions_sources_dir/$adu_steps_handler_file + $adu_bin_path -l 2 --extension-type updateContentHandler --extension-id "microsoft/update-manifest" --register-extension $adu_extensions_sources_dir/$adu_steps_handler_file + $adu_bin_path -l 2 --extension-type updateContentHandler --extension-id "microsoft/update-manifest:4" --register-extension $adu_extensions_sources_dir/$adu_steps_handler_file + $adu_bin_path -l 2 --extension-type updateContentHandler --extension-id "microsoft/update-manifest:5" --register-extension $adu_extensions_sources_dir/$adu_steps_handler_file echo "Register a reference component enumerator..." - $adu_bin_path -l 2 -E $adu_extensions_sources_dir/$adu_example_component_enumerator_file + $adu_bin_path -l 2 --extension-type componentEnumerator --register-extension $adu_extensions_sources_dir/$adu_example_component_enumerator_file echo "Register content downloader extension..." - $adu_bin_path -l 2 -D $adu_extensions_sources_dir/$adu_delivery_optimization_downloader_file + $adu_bin_path -l 2 --extension-type contentDownloader --register-extension $adu_extensions_sources_dir/$adu_delivery_optimization_downloader_file + + echo "Register delta download handler..." + $adu_bin_path -l 2 --extension-type downloadHandler --extension-id "microsoft/delta:1" --register-extension $adu_extensions_sources_dir/$adu_delta_download_handler_file } register_adu_user_and_process_with_eis() { @@ -253,7 +264,24 @@ register_adu_user_and_process_with_eis() { register_and_enable_daemon() { systemctl daemon-reload - systemctl enable adu-agent + systemctl enable deviceupdate-agent +} + +# +# General migration tasks should be added here +# +complete_migration_steps() +{ + # For migrating to current version from 0.8.0 need + # to disable the old agent the old one + if systemctl is-active --quiet adu-agent; then + + #disable old agent and let it die + systemctl disable adu-agent + + # start the new one + systemctl start deviceupdate-agent + fi } case "$1" in @@ -266,7 +294,7 @@ case "$1" in register_and_enable_daemon # Note: DO user and group are created by deliveryoptimization-agent Debian package, - # which is one of the dependencies declared in adu-agent control file. + # which is one of the dependencies declared in deviceupdate-agent control file. # We are assuming that both DO user and group currently exist at this point. # Add 'do' user to 'adu' group to allow DO to write to ADU download sandbox. echo "Add the 'do' user to the 'adu' group and 'adu' to the 'do' group" @@ -280,13 +308,15 @@ case "$1" in systemctl restart "$ms_doclient_lite_service" fi - # Do not restart the current adu-agent because we want the update workflow + complete_migration_steps + + # Do not restart the current deviceupdate-agent because we want the update workflow # to complete, and reports the agent's state to ADU Management service. echo "============================================================" echo echo " ADU Agent service must be restarted." >&2 echo - echo " Please run 'sudo systemctl restart adu-agent'" >&2 + echo " Please run 'sudo systemctl restart deviceupdate-agent'" >&2 echo echo "============================================================" ;; diff --git a/packages/debian/postrm b/packages/debian/postrm index 3208550ec..e0dd12115 100644 --- a/packages/debian/postrm +++ b/packages/debian/postrm @@ -1,5 +1,7 @@ #!/bin/sh +echo "******************** Running $0 $1 $2 ***********************" + # postrm # post-remove script for ADU Agent debian package. # Cleans up files and directories created by package post-install @@ -42,14 +44,17 @@ deregister_eis() echo "Remove 'adu' user from 'aziotks' group." gpasswd -d $adu_user aziotks > /dev/null - echo "Restart IoT Identity Service" - systemctl restart aziot-{identity,cert,tpm,key}d + echo "Restart IoT Identity Services" + systemctl restart aziot-certd + systemctl restart aziot-tpmd + systemctl restart aziot-keyd + systemctl restart aziot-identityd } do_remove_tasks() { deregister_eis - # The adu-agent unit file is removed after 'prerm remove' step. + # The deviceupdate-agent unit file is removed after 'prerm remove' step. # We just need to reload the daemon here. systemctl daemon-reload @@ -104,7 +109,7 @@ case "$1" in # the '[new] postrm failed-upgrade new-ver#' will be invoked. # # In [new] postrm context: - # - We have another chance to verify whether the new adu-agent can function properly. + # - We have another chance to verify whether the new deviceupdate-agent can function properly. # - If yes, we will return 0 to tell APT to continue the upgrade with '[new] postinst configure old-ver#'. # - If no, we will return 1 to tell APT to abort the upgrade. # In order to restore the old package, the following maintainer scripts must return ok (exit 0): @@ -115,10 +120,11 @@ case "$1" in failed-upgrade) echo "[postrm failed-upgrade $2] Perform new ADU Agent health validation." >&2 - # When upgrading 0.8.0 to 0.8.1, adu-shell ownership and permissions can be incorrect. - echo "Attempting to fix-up $adu_shell_bin_path after postrm upgrade failure from $2 ..." + # When upgrading 0.8.0 to 0.8.1, adu-shell and adu-conf ownership and permissions can be incorrect. + echo "Attempting to fix-up $adu_shell_bin_path and $adu_conf_dir after postrm upgrade failure from $2 ..." chown root:adu "$adu_shell_bin_path" chmod u=rxs,g=rx,o= "$adu_shell_bin_path" + chmod u=rwx,g=rx,o= "$adu_conf_dir" echo "AducIotAgent version:" >&2 /usr/bin/AducIotAgent --version >&2 diff --git a/packages/debian/prerm b/packages/debian/prerm index bca0f44e3..cf5004fae 100644 --- a/packages/debian/prerm +++ b/packages/debian/prerm @@ -2,15 +2,15 @@ # prerm # pre-remove script for ADU Agent debian package. -# Deregister adu-agent daemon. +# Deregister deviceupdate-agent daemon. # See https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html -adu_agent_service=adu-agent.service -adu_agent_unit_file=/usr/lib/systemd/system/adu-agent.service +adu_agent_service=deviceupdate-agent.service +adu_agent_unit_file=/usr/lib/systemd/system/deviceupdate-agent.service deregister_daemon() { - # Stop and remove adu-agent service from the failed services list. No-op if never failed earlier. + # Stop and remove deviceupdate-agent service from the failed services list. No-op if never failed earlier. systemctl stop $adu_agent_service systemctl reset-failed $adu_agent_service } diff --git a/scripts/adu-diag.sh b/scripts/adu-diag.sh index f385ed5be..29c9ae4a4 100755 --- a/scripts/adu-diag.sh +++ b/scripts/adu-diag.sh @@ -1,13 +1,13 @@ -# !/bin/bash +#!/bin/bash # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # Ensure we dont end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi error() { echo -e "\033[1;31mError:\033[0m $*" >&2; } @@ -30,9 +30,9 @@ gather_aduc_info() { # Agent info. - cp /usr/lib/systemd/system/adu-agent.service . + cp /usr/lib/systemd/system/deviceupdate-agent.service . - systemctl status adu-agent.service > adu-agent.service.txt + systemctl status deviceupdate-agent.service > deviceupdate-agent.service.txt ls -l /usr/bin/AducIotAgent > AducIotAgent.txt } &>> adu-diag.log @@ -70,11 +70,11 @@ gather_do_info tar -czvf ~/adu-diag.tar.gz . || { error "tar command failed." - popd + popd || $ret $ret 1 } -popd +popd || $ret echo "" diff --git a/scripts/build.sh b/scripts/build.sh index 28cd78f0b..95d0ce949 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -6,11 +6,12 @@ # Ensure that getopt starts from first option if ". " was used. OPTIND=1 +ret='exit' # Ensure we dont end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi warn() { echo -e "\033[1;33mWarning:\033[0m $*" >&2; } @@ -21,13 +22,14 @@ header() { echo -e "\e[4m\e[1m\e[1;32m$*\e[0m"; } bullet() { echo -e "\e[1;34m*\e[0m $*"; } -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null 2>&1 && pwd)" root_dir=$script_dir/.. build_clean=false build_documentation=false build_packages=false platform_layer="linux" +trace_target_deps=false content_handlers="microsoft/swupdate,microsoft/apt,microsoft/simulator" build_type=Debug adu_log_dir="" @@ -38,7 +40,8 @@ declare -a static_analysis_tools=() log_lib="zlog" install_prefix=/usr/local install_adu=false -cmake_dir_path=/tmp/cmake-3.10.2 +work_folder=/tmp +cmake_dir_path="${work_folder}/deviceupdate-cmake" print_help() { echo "Usage: build.sh [options...]" @@ -56,6 +59,8 @@ print_help() { echo "-p, --platform-layer Specify the platform layer to build/use. Default is linux." echo " Option: linux" echo "" + echo "--trace-target-deps Traces dependencies of CMake targets debug info." + echo "" echo "--log-lib Specify the logging library to build/use. Default is zlog." echo " Options: zlog xlog" echo "" @@ -65,11 +70,10 @@ print_help() { echo "--install-prefix Install prefix to pass to CMake." echo "" echo "--install Install the following ADU components." - echo " From source: adu-agent.service & adu-swupdate.sh." + echo " From source: deviceupdate-agent.service & adu-swupdate.sh." echo " From build output directory: AducIotAgent & adu-shell." echo "" - echo "--cmake-path Specify the cmake path that we want to use to build ADU." - echo " Default will be the path same as install-deps.sh cmake_dir_path" + echo "--cmake-path Override the cmake path such that CMake binary is at /bin/cmake" echo "" echo "-h, --help Show this help message." } @@ -80,7 +84,7 @@ copyfile_exit_if_failed() { ret_val=$? if [[ $ret_val != 0 ]]; then error "failed to copy $1 to $2 (exit code:$ret_val)" - exit $ret_val + return $ret_val fi } @@ -95,7 +99,7 @@ install_adu_components() { groupadd --system adu useradd --system -p '' -g adu --no-create-home --shell /sbin/false adu - bullet "Add current user ('$USER') to 'adu' group, to allow launching adu-agent (for testing purposes only)" + bullet "Add current user ('$USER') to 'adu' group, to allow launching deviceupdate-agent (for testing purposes only)" usermod -a -G adu "$USER" bullet "Current user info:" id @@ -126,17 +130,17 @@ install_adu_components() { chmod u=rwxs,g=rx,o= "$adu_lib_dir/adu-shell" chmod u=rwx,g=rx,o=rx "$adu_lib_dir/adu-swupdate.sh" - # Only install adu-agent service on system that support systemd. + # Only install deviceupdate-agent service on system that support systemd. systemd_system_dir=/usr/lib/systemd/system/ if [ -d "$systemd_system_dir" ]; then - bullet "Install adu-agent systemd daemon..." - copyfile_exit_if_failed "$root_dir/daemon/adu-agent.service" "$systemd_system_dir" + bullet "Install deviceupdate-agent systemd daemon..." + copyfile_exit_if_failed "$root_dir/daemon/deviceupdate-agent.service" "$systemd_system_dir" systemctl daemon-reload - systemctl enable adu-agent - systemctl restart adu-agent + systemctl enable deviceupdate-agent + systemctl restart deviceupdate-agent else - warn "Directory $systemd_system_dir does not exist. Skip adu-agent.service installation." + warn "Directory $systemd_system_dir does not exist. Skip deviceupdate-agent.service installation." fi echo "ADU components installation completed." @@ -146,15 +150,13 @@ install_adu_components() { OS="" VER="" determine_distro() { - # shellcheck disable=SC1091 - # Checking distro name and version if [ -r /etc/os-release ]; then # freedesktop.org and systemd OS=$(grep "^ID\s*=\s*" /etc/os-release | sed -e "s/^ID\s*=\s*//") VER=$(grep "^VERSION_ID\s*=\s*" /etc/os-release | sed -e "s/^VERSION_ID\s*=\s*//") - VER=$(sed -e 's/^"//' -e 's/"$//' <<<"$VER") - elif type lsb_release >/dev/null 2>&1; then + VER=$(sed -e 's/^"//' -e 's/"$//' <<< "$VER") + elif type lsb_release > /dev/null 2>&1; then # linuxbase.org OS=$(lsb_release -si) VER=$(lsb_release -sr) @@ -217,7 +219,7 @@ while [[ $1 != "" ]]; do declare -a static_analysis_tools=(clang-tidy cppcheck cpplint iwyu lwyu) else IFS=',' - read -ra static_analysis_tools <<<"$1" + read -ra static_analysis_tools <<< "$1" IFS=' ' fi ;; @@ -228,7 +230,9 @@ while [[ $1 != "" ]]; do $ret 1 fi platform_layer=$1 - + ;; + --trace-target-deps) + trace_target_deps=true ;; --log-lib) shift @@ -300,6 +304,8 @@ fi runtime_dir=${output_directory}/bin library_dir=${output_directory}/lib +cmake_bin="${cmake_dir_path}/bin/cmake" +shellcheck_bin="${work_folder}/deviceupdate-shellcheck" # Output banner echo '' @@ -307,6 +313,7 @@ header "Building ADU Agent" bullet "Clean build: $build_clean" bullet "Documentation: $build_documentation" bullet "Platform layer: $platform_layer" +bullet "Trace target deps: $trace_target_deps" bullet "Content handlers: $content_handlers" bullet "Build type: $build_type" bullet "Log directory: $adu_log_dir" @@ -314,6 +321,10 @@ bullet "Logging library: $log_lib" bullet "Output directory: $output_directory" bullet "Build unit tests: $build_unittests" bullet "Build packages: $build_packages" +bullet "CMake: $cmake_bin" +bullet "CMake version: $(${cmake_bin} --version | grep version | awk '{ print $3 }')" +bullet "shellcheck: $shellcheck_bin" +bullet "shellcheck version: $("$shellcheck_bin" --version | grep 'version:' | awk '{ print $2 }')" if [[ ${#static_analysis_tools[@]} -eq 0 ]]; then bullet "Static analysis: (none)" else @@ -330,6 +341,7 @@ CMAKE_OPTIONS=( "-DADUC_LOG_FOLDER:STRING=$adu_log_dir" "-DADUC_LOGGING_LIBRARY:STRING=$log_lib" "-DADUC_PLATFORM_LAYER:STRING=$platform_layer" + "-DADUC_TRACE_TARGET_DEPS=$trace_target_deps" "-DCMAKE_BUILD_TYPE:STRING=$build_type" "-DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=ON" "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY:STRING=$library_dir" @@ -411,26 +423,31 @@ if [[ $build_clean == "true" ]]; then fi mkdir -p "$output_directory" -pushd "$output_directory" >/dev/null +pushd "$output_directory" > /dev/null || return # Generate build using cmake with options -if [[ $OS != "ubuntu" || $VER != "18.04" ]]; then - "$cmake_dir_path"/bin/cmake -G Ninja "${CMAKE_OPTIONS[@]}" "$root_dir" +if [ ! -f "$cmake_bin" ]; then + error "No '${cmake_bin}' file." + ret_val=1 else - cmake -G Ninja "${CMAKE_OPTIONS[@]}" "$root_dir" + "$cmake_bin" -G Ninja "${CMAKE_OPTIONS[@]}" "$root_dir" + ret_val=$? fi -# Do the actual building with ninja -# Save the return code of ninja so we can $ret with that return code. -ninja -ret_val=$? +if [ $ret_val -ne 0 ]; then + error "CMake failed to generate Ninja build with exit code: $ret_val" +else + # Do the actual building with ninja + ninja + ret_val=$? +fi if [[ $ret_val == 0 && $build_packages == "true" ]]; then cpack ret_val=$? fi -popd >/dev/null +popd > /dev/null || return if [[ $ret_val == 0 && $install_adu == "true" ]]; then install_adu_components diff --git a/scripts/clang-format.sh b/scripts/clang-format.sh index c66ebdfe1..e0c1c8b56 100755 --- a/scripts/clang-format.sh +++ b/scripts/clang-format.sh @@ -8,9 +8,9 @@ OPTIND=1 # Ensure we dont end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi if ! [ -x "$(command -v git)" ]; then @@ -30,11 +30,27 @@ if [ -z "$GITROOT" ]; then $ret 1 fi -pushd "$GITROOT" > /dev/null +issue_with_file=false +pushd "$GITROOT" > /dev/null || $ret # diff-filter=d will exclude deleted files. +# --cached for only the staged files IFS=$'\n' -for FILE in $(git diff --diff-filter=d --relative --name-only HEAD -- "*.[CcHh]" "*.[CcHh][Pp][Pp]"); do - clang-format -verbose -style=file -i "$FILE" +for FILE in $(git diff --diff-filter=d --relative --name-only --cached HEAD -- "*.[CcHh]" "*.[CcHh][Pp][Pp]"); do + # Use --dry-run and --Werror to have clang-format return non-zero error return if it + # had to reformat a file. + # Need to use git cat-file blob so that it gets staged file in case it got formatted in working set, which would + # allow it to bypass pre-commit hook even if staged file is not formatted correctly. + if ! git cat-file blob :"$FILE" 2>&1 | clang-format --dry-run --Werror --verbose --style=file; then + issue_with_file=true + clang-format --verbose --style=file -i "$FILE" + fi done IFS=' ' -popd > /dev/null +popd > /dev/null || $ret + +if [[ $issue_with_file == "true" ]]; then + echo 'Error: Some staged C/C++ files had issues reformatted. Fix issues and/or git add the reformatted changes.' + $ret 1 +fi + +$ret 0 diff --git a/scripts/cmake-format.sh b/scripts/cmake-format.sh index c3d5361bc..39ebf18f8 100755 --- a/scripts/cmake-format.sh +++ b/scripts/cmake-format.sh @@ -8,38 +8,58 @@ OPTIND=1 # Ensure we dont end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi +error() { echo -e "\033[1;31mError:\033[0m $*" >&2; } + # To force all cmakefiles to be reformatted, try: # find $(git rev-parse --show-toplevel) -name CMakeLists.txt -exec sh -c "echo "" >> {}" \; # then run this script. if ! [ -x "$(command -v git)" ]; then - echo 'Error: git is not installed.' >&2 + error 'git is not installed.' $ret 1 fi if ! [ -x "$(command -v cmake-format)" ]; then - echo 'Error: cmake-format is not installed. Try: sudo --set-home pip3 install cmake-format' >&2 + error 'cmake-format is not installed. Try: sudo --set-home pip3 install cmake-format' $ret 1 fi GITROOT="$(git rev-parse --show-toplevel 2> /dev/null)" if [ -z "$GITROOT" ]; then - echo 'Unable to determine git root.' >&2 + error 'Unable to determine git root.' $ret 1 fi -pushd "$GITROOT" > /dev/null +pushd "$GITROOT" > /dev/null || $ret # diff-filter=d will exclude deleted files. IFS=$'\n' -for FILE in $(git diff --diff-filter=d --relative --name-only HEAD -- "CMakeLists.txt" "*/CMakeLists.txt" "*.cmake" "*/*.cmake"); do +cmake_format_failed_or_reformatted=false +# Use --cached to only check the staged files before commit. +for FILE in $(git diff --diff-filter=d --relative --name-only --cached HEAD -- "CMakeLists.txt" "*/CMakeLists.txt" "*.cmake" "*/*.cmake"); do echo Formatting "$FILE" - cmake-format -i "$FILE" + # Unfortunately, cmake-format doesn't work when piping git cat-file blob + # into it so that it processes only staged file contents, but we use the + # --check argument so that a reformatting results in non-zero exit code. + if ! cmake-format --check "$FILE" 2> /dev/null; then + cmake_format_failed_or_reformatted=true + + # now, in-place format the file. Unfortunately, it will be in the + # unstaged working set, so we notify the user farther down. + cmake-format -i "$FILE" + fi done IFS=' ' -popd > /dev/null +popd > /dev/null || $ret + +if [[ $cmake_format_failed_or_reformatted == "true" ]]; then + error 'cmake-format failed or had to reformat a file. Fix the issues and/or git add the reformatted file(s).' + $ret 1 +fi + +$ret 0 diff --git a/scripts/config-integration.sh b/scripts/config-integration.sh index 4373f0ace..1042d6e53 100755 --- a/scripts/config-integration.sh +++ b/scripts/config-integration.sh @@ -5,15 +5,15 @@ # Ensure we dont end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi error() { echo -e "\033[1;31mError:\033[0m $*" >&2; } warn() { echo -e "\033[1;33mWarning:\033[0m $*" >&2; } -current_schema_version="1.0" +current_schema_version="1.1" conf_file=$1 new_conf_file="/etc/adu/du-config.json" @@ -43,6 +43,7 @@ json_content=$( "adu", "do" ], + "iotHubProtocol": "mqtt", <% MANUFACTURER %> <% MODEL %> "agents": [ diff --git a/scripts/debug_agent.sh b/scripts/debug_agent.sh index 71a5e0871..6dd441505 100755 --- a/scripts/debug_agent.sh +++ b/scripts/debug_agent.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -10,9 +10,9 @@ OPTIND=1 # Ensure we dont end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi usage() { diff --git a/scripts/docker/README.md b/scripts/docker/README.md new file mode 100644 index 000000000..c0f5ab522 --- /dev/null +++ b/scripts/docker/README.md @@ -0,0 +1,87 @@ +# DeviceUpdate Docker Image + +## Description + +build_docker_image.sh is a self-contained script that builds a fairly minimal docker image that runs a subset of the DU binary Agent. + +The purpose is for composing the DU Agent core with other extensions to build a dockerfile from those layers. +This allows for a minimal container image based on needs of the image. +For example, if all that is needed is a core agent that can handle a v4 update manifest and can handle APT package update content, then one can specify that and the resulting docker image will only contain just enough to support it. + +## Limitations + +The DU Agent (/usr/bin/AducIotAgent) in the container image can be setup to include the following extensibility points: + +- Content Downloader(one of following) + - download update content using libcurl + - (Not available yet) download update content using DeliveryOptimization (DO) +- Update Content Handler + - APT Package-based updates + - (Not yet available) Simulator update content handler + - (Not yet available) Script handler updates + - (Not yet available) SWUpdate V2 handler +- Download handler + - (Not yet available) Microsoft Delta Update Download handler +- Connectivity + - get connection string from du-config.json (NOT from AIS) + +## Pre-requisites + +- [install docker](https://docs.docker.com/engine/install/ubuntu/) +- create a separate file with the connection string + - can create it from existing /etc/adu/du-config.json by doing: + + ```sh + grep connectionData /etc/adu/du-config.json | \ + cut -d\" -f 4 > /path/to/connection_string + ``` + +## Defaults + +By default, it will use the following defaults if not overridden: + +### du config + +```json +manufacturer: contoso +model: toaster +``` + +### container config + +```json +image version: 1.0 +base image: public.ecr.aws/lts/ubuntu:18.04_stable +tag duagentaptcurlubuntu18.04stable:1.0 +``` + +### Print script help usage + +Use `-h` or `--help` for usage options. + +## Build the docker image + +To build the docker image, run: + +```sh +$ scripts/docker/build_docker_image.sh -c /path/to/connection_string --clean +``` + +### list docker images + +```sh +$ docker images +``` + +## Run Docker Image + +```sh +$ docker run -i -t duagentaptcurlubuntu18.04stable:1.0 +... +``` + +## Attach to Docker Image and run bash shell + +```sh +$ docker run -i -t duagentaptcurlubuntu18.04stable:1.0 bash +``` diff --git a/scripts/docker/build_docker_image.sh b/scripts/docker/build_docker_image.sh new file mode 100755 index 000000000..d90e43498 --- /dev/null +++ b/scripts/docker/build_docker_image.sh @@ -0,0 +1,411 @@ +#!/bin/bash + +BASE_SOURCE_DIR=$(pwd) +TEMPLATES_SOURCE_DIR=$BASE_SOURCE_DIR/templates + +DEB_FILE='deviceupdate-agent_0.8.0_public_preview_amd64.deb' +DU_AGENT_URL="https://github.com/Azure/iot-hub-device-update/releases/download/0.8.0/$DEB_FILE" +DEBFILE_EXPECTED_DIGEST="GDpgPJ7jcM/ngY0EGCT/7OfU7QN1uvEAH6Dn6YOPds0=" + +CLEAN=0 +DEB_PKG_PATH='' +SYSTEM_DEPS=" + ar + docker + sed + sha256sum + tar + wget +" + +ADD_DEBUGGING_PACKAGES=0 +DBG_APT_PACKAGES="binutils gdb git iputils-ping iproute2 net-tools" + +# Work directories +BASE_WORK_DIR=/tmp/adu/build_docker_img +APP_WORK_DIR=$BASE_WORK_DIR/app +APP_ROOT_DIR=$APP_WORK_DIR/root +APP_BIN_DIR=$APP_ROOT_DIR/usr/bin +APP_DEPS_LIB_DIR=$APP_ROOT_DIR/usr/lib +APP_LIB_DIR=$APP_DEPS_LIB_DIR/adu +APP_EXT_BASE_DIR=$APP_ROOT_DIR/var/lib/adu/extensions +APP_EXT_DIR=$APP_EXT_BASE_DIR/sources +APP_ETC_DIR=$APP_ROOT_DIR/etc/adu +TMP_WORK_DIR=$BASE_WORK_DIR/tmp +TMP_DATA_DIR=$TMP_WORK_DIR/data +TMP_CTRL_DIR=$TMP_WORK_DIR/control + +USR_BIN_SRC=$TMP_DATA_DIR/usr/bin +USR_LIB_SRC=$TMP_DATA_DIR/usr/lib/adu +EXT_SRC=$TMP_DATA_DIR/var/lib/adu/extensions/sources + +# commands +mkdir_cmd="mkdir -p" +wget_cmd="wget -q" +extract_debian_archive_cmd="ar x" +extract_tarball_cmd="tar -xzvf" +untar_cmd="tar -xvf" +create_tarball_cmd="tar -czvf" + +#container image +CONTAINER_IMAGE_VERSION="1.0" +CONTAINER_IMAGE_DESCRIPTION="Azure IotHub DeviceUpdate Agent image" +CONTAINER_IMAGE_BASE_IMAGE="mcr.microsoft.com/mirror/docker/library/ubuntu:18.04" # MCR golden image +CONTAINER_IMAGE_TAG="duagentaptcurlubuntu18.04stable:$CONTAINER_IMAGE_VERSION" +CONTAINER_IMAGE_DOCKER_CMD='exec /usr/bin/AducIotAgent -l0 -e > /var/log/adu/DUAgent.log 2>&1' # Use exec to honor OS signals and 'docker stop' + +# du-config vars +DUCONFIG_COMPAT_PROPERTY_NAMES="manufacturer,model" # used for update compatibility check and device groups +DUCONFIG_DEVICEINFO_MANUFACTURER="contoso" +DUCONFIG_DEVICEINFO_MODEL="toaster" +DUCONFIG_AGENT_NAME="dockerAptCurlAgent" +DUCONFIG_CONNECTION_STRING= + +# The following are used for update compatibility +DUCONFIG_DEVICEPROPERTIES_MANUFACTURER="$DUCONFIG_DEVICEINFO_MANUFACTURER" +DUCONFIG_DEVICEPROPERTIES_MODEL="$DUCONFIG_DEVICEINFO_MODEL" + +# +# Exit codes +# +EXIT_CODE_BAD_INVALID_CONNECTION_STRING_PATH=1 +EXIT_CODE_UNMET_SYSTEM_DEPS=2 +EXIT_CODE_BAD_DIGEST=3 + +usage() { + cat << EOF +Usage: ./scripts/build_container.sh -c + + Builds a docker container image with DU core agent, extensions, and dependencies. + + General options + -h | --help Shows this help + + DU Config options + -c | --connection-string (required) The path to file that contains connection string. + -d | --deb-package-path (optional) The path to DU agent .deb to use instead of downloading release asset. + -M | --manufacturer (optional) The value of manufacturer in du-config.json deviceProperties. + -m | --model (optional) The value of model in du-config.json deviceProperties. + + Container options + -i | --imgver (optional) The container image version. + -b | --base (optional) The base container image to use. + -t | --tag (optional) The container image tag. + -g | --add-debugging (optional) Adds gdb, binutils, net-tools, etc. for debugging purposes. + +EOF +} + +inline_expand_template_parameters() { + params_to_replace="$1" + output_file_path="$2" + + for var in $params_to_replace; do + pattern="%%${var}%%" + param_val=${!var} + param_val=${param_val/\./\\\.} # escape periods + param_val=${param_val/\&/\\\&} # escape ampersands + + sed -i "s|${pattern}|${param_val}|g" "$output_file_path" + done +} + +create_update_content_handler_extension_registration() { + handlerid=$1 + so_name=$2 + local libname=${handlerid/\//_} + libname=${libname/:/_} + + # SC2034 is irrelevant because base64digest is implicitly used by inline_expand_template_parameters via parameters_to_expand + # shellcheck disable=SC2034 + base64digest=$(openssl dgst -binary "$EXT_SRC/${so_name}.so" | openssl base64) + + $mkdir_cmd "$APP_EXT_BASE_DIR/update_extensions/step_handlers/$libname" + local target_filepath="$APP_EXT_BASE_DIR/update_content_handlers/${libname}/content_handler.json" + cp "$TEMPLATES_SOURCE_DIR/content_handler.template.json" "$target_filepath" + + parameters_to_expand=" + so_name + base64digest + handlerid + " + inline_expand_template_parameters "$parameters_to_expand" "$target_filepath" +} + +create_content_downloader_registration() { + so_name="$1" + + $mkdir_cmd "$APP_EXT_BASE_DIR/content_downloader" + local target_filepath="$APP_EXT_BASE_DIR/content_downloader/extension.json" + cp "$TEMPLATES_SOURCE_DIR/content_downloader.extension.template.json" "$target_filepath" + + # SC2034 is irrelevant because base64digest is implicitly used by inline_expand_template_parameters via parameters_to_expand + # shellcheck disable=SC2034 + base64digest=$(openssl dgst -binary "$EXT_SRC/${so_name}.so" | openssl base64) + + parameters_to_expand=" + so_name + base64digest + " + inline_expand_template_parameters "$parameters_to_expand" "$target_filepath" +} + +copy_deps_binaries() { + local do_url="https://github.com/microsoft/do-client/releases/download/v1.0.0/ubuntu1804_x64-packages.tar" + + cd $TMP_WORK_DIR || exit + $wget_cmd $do_url + $untar_cmd ./ubuntu1804_x64-packages.tar + mkdir libdo + cp libdeliveryoptimization_0.6.0_amd64.deb libdo + cd libdo || exit + $extract_debian_archive_cmd libdeliveryoptimization_0.6.0_amd64.deb + mkdir data + cp data.tar.gz data + cd data || exit + $extract_tarball_cmd data.tar.gz + cp usr/lib/* $APP_DEPS_LIB_DIR/ +} + +create_app_tarball() { + # + # Copy app binaries + # + + # Copy app agent core binaries + cp $USR_BIN_SRC/AducIotAgent $APP_BIN_DIR + cp $USR_LIB_SRC/adu-shell $APP_LIB_DIR + + # Copy update content handler plugin modules + supported_extensions=" + apt + steps + script + " + for ext in $supported_extensions; do + cp "$EXT_SRC/libmicrosoft_${ext}_1.so" $APP_EXT_DIR + done + + # Copy content downloader plugin modules + cp $EXT_SRC/libcurl_content_downloader.so $APP_EXT_DIR + + # Create app plugin modules registrations + create_update_content_handler_extension_registration 'microsoft/apt:1' 'libmicrosoft_apt_1' + create_update_content_handler_extension_registration 'microsoft/script:1' 'libmicrosoft_script_1' + create_update_content_handler_extension_registration 'microsoft/steps:1' 'libmicrosoft_steps_1' + create_update_content_handler_extension_registration 'microsoft/update-manifest:4' 'libmicrosoft_steps_1' # update manifest handler currently uses steps lib + create_content_downloader_registration 'libcurl_content_downloader' + + # Copy required assets for use by container image + cp "$TEMPLATES_SOURCE_DIR/setup_container.sh" $APP_LIB_DIR/ + + # Copy and expand template params for du-config.json + cp "$TEMPLATES_SOURCE_DIR/du-config.template.json" $APP_ETC_DIR/du-config.json + parameters_to_expand=" + DUCONFIG_COMPAT_PROPERTY_NAMES + DUCONFIG_DEVICEINFO_MANUFACTURER + DUCONFIG_DEVICEINFO_MODEL + DUCONFIG_AGENT_NAME + DUCONFIG_CONNECTION_STRING + DUCONFIG_DEVICEPROPERTIES_MANUFACTURER + DUCONFIG_DEVICEPROPERTIES_MODEL + " + inline_expand_template_parameters "$parameters_to_expand" "$APP_ETC_DIR/du-config.json" + + # Copy app du-diagnostics-config json + cp "$TEMPLATES_SOURCE_DIR/du-diagnostics-config.template.json" $APP_ETC_DIR/du-diagnostics-config.json + + copy_deps_binaries + + $create_tarball_cmd $APP_WORK_DIR/app.tar.gz -C $APP_ROOT_DIR . > /dev/null 2>&1 +} + +# +# MAIN +# + +# Parse cmd-line args +while [ "$1" != "" ]; do + case $1 in + -c | --connection-string) + shift + if [ ! -f "$1" ]; then + echo "ERROR: $1 is not a valid path" + exit $EXIT_CODE_BAD_INVALID_CONNECTION_STRING_PATH + fi + DUCONFIG_CONNECTION_STRING=$(cat "$1") + ;; + -C | --clean) + CLEAN=1 + ;; + -d | --deb-package-path) + shift + DEB_PKG_PATH="$1" + ;; + -g | --add-debugging) + ADD_DEBUGGING_PACKAGES=1 + ;; + -M | --manufacturer) + shift + DUCONFIG_DEVICEINFO_MANUFACTURER="$1" + DUCONFIG_DEVICEPROPERTIES_MANUFACTURER="$1" + ;; + -m | --model) + shift + DUCONFIG_DEVICEINFO_MODEL="$1" + DUCONFIG_DEVICEPROPERTIES_MODEL="$1" + ;; + + -i | --imgver) + shift + CONTAINER_IMAGE_VERSION="$1" + ;; + -b | --base) + shift + CONTAINER_IMAGE_BASE_IMAGE="$1" + ;; + -t | --tag) + shift + CONTAINER_IMAGE_TAG="$1" + ;; + + -h | --help) + usage + exit 0 + ;; + *) + usage + exit 1 + ;; + esac + shift +done + +if [ "$DUCONFIG_CONNECTION_STRING" = "" ]; then + printf "\nERROR: -c | --connection-string is required.\n\n" + usage + exit 1 +fi + +cat <<- EOF + Using the following: + CONTAINER_IMAGE_VERSION: $CONTAINER_IMAGE_VERSION + CONTAINER_IMAGE_DESCRIPTION: $CONTAINER_IMAGE_DESCRIPTION + CONTAINER_IMAGE_BASE_IMAGE: $CONTAINER_IMAGE_BASE_IMAGE + CONTAINER_IMAGE_TAG: $CONTAINER_IMAGE_TAG + CONTAINER_IMAGE_DOCKER_CMD: $CONTAINER_IMAGE_DOCKER_CMD + DUCONFIG_COMPAT_PROPERTY_NAMES: $DUCONFIG_COMPAT_PROPERTY_NAMES + DUCONFIG_DEVICEINFO_MANUFACTURER: $DUCONFIG_DEVICEINFO_MANUFACTURER + DUCONFIG_DEVICEINFO_MODEL: $DUCONFIG_DEVICEINFO_MODEL + DUCONFIG_AGENT_NAME: $DUCONFIG_AGENT_NAME + DUCONFIG_DEVICEPROPERTIES_MANUFACTURER: $DUCONFIG_DEVICEPROPERTIES_MANUFACTURER + DUCONFIG_DEVICEPROPERTIES_MODEL: $DUCONFIG_DEVICEPROPERTIES_MODEL +EOF + +# +# Ensure dependencies +# +DEP_FAILURE=0 +for sysdep in $SYSTEM_DEPS; do + which "$sysdep" > /dev/null 2>&1 + ret_val=$? + if [ $ret_val -ne 0 ]; then + DEP_FAILURE=1 + echo "Unmet system dependencies: $sysdep" + fi +done + +if [ $DEP_FAILURE -eq 1 ]; then + echo "exiting due to unmet system dependencies" + exit $EXIT_CODE_UNMET_SYSTEM_DEPS +fi + +# +# Cleanup +# +if [ $CLEAN -eq 1 ]; then + echo "Cleaning up '$APP_WORK_DIR' ..." + [ -d $APP_WORK_DIR ] && rm -rf $APP_WORK_DIR + + echo "Cleaning up '$TMP_WORK_DIR' ..." + [ -d $TMP_WORK_DIR ] && rm -rf $TMP_WORK_DIR +fi + +# +# Create necessary dirs +# +DIRS_TO_CREATE=" + $APP_WORK_DIR + $TMP_WORK_DIR + $APP_BIN_DIR + $APP_LIB_DIR + $APP_EXT_DIR + $APP_ETC_DIR +" +for d in $DIRS_TO_CREATE; do + if [ ! -d "$d" ]; then + $mkdir_cmd "$d" + fi +done + +# +# Download DU agent deb pkg +# + +cd $TMP_WORK_DIR || exit +if [ "$DEB_PKG_PATH" = "" ]; then + $wget_cmd $DU_AGENT_URL + digest=$(openssl dgst -binary $DEB_FILE | openssl base64) + if [ "$digest" != "$DEBFILE_EXPECTED_DIGEST" ]; then + echo "bad hash of $DEB_FILE. Expected '$DEBFILE_EXPECTED_DIGEST' but got '$digest'" + exit $EXIT_CODE_BAD_DIGEST + fi +else + DEB_FILE="$DEB_PKG_PATH" +fi + +# +# Extract deb pkg +# +cd $TMP_WORK_DIR || exit +$extract_debian_archive_cmd "$DEB_FILE" + +$mkdir_cmd $TMP_DATA_DIR +cp data.tar.gz $TMP_DATA_DIR +cd $TMP_DATA_DIR || exit +$extract_tarball_cmd data.tar.gz > /dev/null 2>&1 + +cd $TMP_WORK_DIR || exit +$mkdir_cmd $TMP_CTRL_DIR +cp control.tar.gz $TMP_CTRL_DIR +cd $TMP_CTRL_DIR || exit +$extract_tarball_cmd control.tar.gz > /dev/null 2>&1 + +create_app_tarball + +# +# Create Dockerfile from template +# +cd $APP_WORK_DIR || exit +cp "${TEMPLATES_SOURCE_DIR}/Dockerfile.template" "$APP_WORK_DIR/Dockerfile" + +# Replace template parameters with contents of variables with same name as template parameter. +APT_PACKAGES_FOR_DEBUGGING='' +if [ $ADD_DEBUGGING_PACKAGES -eq 1 ]; then + export APT_PACKAGES_FOR_DEBUGGING="$DBG_APT_PACKAGES" +fi + +params_to_replace=" + APT_PACKAGES_FOR_DEBUGGING + CONTAINER_IMAGE_BASE_IMAGE + CONTAINER_IMAGE_DESCRIPTION + CONTAINER_IMAGE_DOCKER_CMD + CONTAINER_IMAGE_VERSION +" +inline_expand_template_parameters "$params_to_replace" "$APP_WORK_DIR/Dockerfile" + +# +# Build Docker image +# +cd $APP_WORK_DIR || exit +docker build --pull -f "Dockerfile" --rm -t "$CONTAINER_IMAGE_TAG" . diff --git a/scripts/docker/dbg_docker.sh b/scripts/docker/dbg_docker.sh new file mode 100755 index 000000000..aef4328b2 --- /dev/null +++ b/scripts/docker/dbg_docker.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# e.g. $ ./dbg_docker.sh duagentaptcurlubuntu18.04stable:1.0 bash + +# SYS_PTRACE capability is needed to allow gdb to set breakpoints +docker run --rm -it --cap-add=SYS_PTRACE --security-opt seccomp=unconfined "$@" +ret_val=$? +if [ $ret_val -ne 0 ]; then + exit 1 +fi diff --git a/scripts/docker/setup_docker_container.sh b/scripts/docker/setup_docker_container.sh new file mode 100755 index 000000000..c4dfbc6df --- /dev/null +++ b/scripts/docker/setup_docker_container.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +if [[ $# -ne 4 ]]; then + echo "USAGE: $0 /path/to/dockerimage.tar.gz " + exit 1 +fi + +gunzip "$1" +dir_name=$(dirname "$1") +base_name=$(basename "$1" .gz) +docker load < "$dir_name/$base_name" +docker run -i -t --detach duagentaptcurlubuntu18.04stable:1.0 bash + +tmp_container_id=$(docker ps | grep duagentaptcurl | cut -f1 -d' ') +# echo $tmp_container_id + +docker cp "$tmp_container_id:/etc/adu/du-config.json" ./du-config.template.json +sed -i -e "s/CONNECTION_STRING_PLACEHOLDER/$2/g" du-config.template.json +sed -i -e "s/contoso/$3/g" du-config.template.json +sed -i -e "s/toaster/$4/g" du-config.template.json +docker cp du-config.template.json "$tmp_container_id:/etc/adu/du-config.json" + +echo +echo "Attaching to docker container $tmp_container_id... Ctrl + P, Ctrl + Q to detach" +echo +docker attach "$tmp_container_id" diff --git a/scripts/docker/templates/Dockerfile.template b/scripts/docker/templates/Dockerfile.template new file mode 100644 index 000000000..2b4c45d77 --- /dev/null +++ b/scripts/docker/templates/Dockerfile.template @@ -0,0 +1,19 @@ + +FROM %%CONTAINER_IMAGE_BASE_IMAGE%% + +ADD ./app.tar.gz / + +RUN apt-get update && apt-get install -y libcurl4-gnutls-dev \ +&& ln -s /usr/lib/x86_64-linux-gnu/libcurl-gnutls.so.4.5.0 /usr/lib/libcurl.so.4 \ +&& apt-get install -y libxml2 \ +&& apt-get install -y libboost-filesystem1.65.1 \ +&& ln -s /usr/lib/x86_64-linux-gnu/libboost_filesystem.so.1.65.1 /usr/lib/libboost_filesystem.so.1.65.1 \ +&& ln -s /usr/lib/x86_64-linux-gnu/libboost_system.so.1.65.1 /usr/lib/libboost_system.so.1.65.1 \ +&& apt-get install -y libcurl4-openssl-dev %%APT_PACKAGES_FOR_DEBUGGING%% + +RUN /bin/sh /usr/lib/adu/setup_container.sh + +LABEL version="%%CONTAINER_IMAGE_VERSION%%" +LABEL description="%%CONTAINER_IMAGE_DESCRIPTION%%" + +CMD %%CONTAINER_IMAGE_DOCKER_CMD%% diff --git a/scripts/docker/templates/content_downloader.extension.template.json b/scripts/docker/templates/content_downloader.extension.template.json new file mode 100644 index 000000000..f4adf9c86 --- /dev/null +++ b/scripts/docker/templates/content_downloader.extension.template.json @@ -0,0 +1,7 @@ +{ + "fileName":"/var/lib/adu/extensions/sources/%%so_name%%.so", + "sizeInBytes":0, + "hashes": { + "sha256":"%%base64digest%%" + } +} diff --git a/scripts/docker/templates/content_handler.template.json b/scripts/docker/templates/content_handler.template.json new file mode 100644 index 000000000..4176efb18 --- /dev/null +++ b/scripts/docker/templates/content_handler.template.json @@ -0,0 +1,8 @@ +{ + "fileName":"/var/lib/adu/extensions/sources/%%so_name%%.so", + "sizeInBytes":0, + "hashes": { + "sha256":"%%base64digest%%" + }, + "handlerId":"%%handlerid%%" +} diff --git a/scripts/docker/templates/du-config.template.json b/scripts/docker/templates/du-config.template.json new file mode 100644 index 000000000..98cff8c2e --- /dev/null +++ b/scripts/docker/templates/du-config.template.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": "1.1", + "aduShellTrustedUsers": [ + "adu", + "do" + ], + "compatPropertyNames": "%%DUCONFIG_COMPAT_PROPERTY_NAMES%%", + "manufacturer": "%%DUCONFIG_DEVICEINFO_MANUFACTURER%%", + "model": "%%DUCONFIG_DEVICEINFO_MODEL%%", + "agents": [ + { + "name": "%%DUCONFIG_AGENT_NAME%%", + "runas": "adu", + "connectionSource": { + "connectionType": "string", + "connectionData": "%%DUCONFIG_CONNECTION_STRING%%" + }, + "manufacturer": "%%DUCONFIG_DEVICEPROPERTIES_MANUFACTURER%%", + "model": "%%DUCONFIG_DEVICEPROPERTIES_MODEL%%" + } + ] +} diff --git a/scripts/docker/templates/du-diagnostics-config.template.json b/scripts/docker/templates/du-diagnostics-config.template.json new file mode 100644 index 000000000..ad4dbce9a --- /dev/null +++ b/scripts/docker/templates/du-diagnostics-config.template.json @@ -0,0 +1,9 @@ +{ + "logComponents":[ + { + "componentName":"adu", + "logPath":"/var/log/adu/" + } + ], + "maxKilobytesToUploadPerLogPath":50 +} diff --git a/scripts/docker/templates/setup_container.sh b/scripts/docker/templates/setup_container.sh new file mode 100644 index 000000000..6b0d833d4 --- /dev/null +++ b/scripts/docker/templates/setup_container.sh @@ -0,0 +1,105 @@ +#!/bin/sh + +# Note: This script is run by Dockerfile RUN directive when starting the container (see Dockerfile.template) + +set -e + +# from preinst +addgroup --system adu +adduser --system adu --ingroup adu --no-create-home --shell /bin/false + +# from postinst +adu_conf_dir=/etc/adu +adu_conf_file=du-config.json +adu_diagnostics_conf_file=du-diagnostics-config.json +adu_log_dir=/var/log/adu +adu_data_dir=/var/lib/adu +adu_downloads_dir="$adu_data_dir/downloads" +adu_shell_dir=/usr/lib/adu +adu_shell_file=adu-shell + +adu_extensions_dir="$adu_data_dir/extensions" +adu_extensions_sources_dir="$adu_extensions_dir/sources" + +adu_user=adu +adu_group=adu + +adu_home_dir=/home/adu + +mkdir -p "$adu_conf_dir" +chown "$adu_user:$adu_group" "$adu_conf_dir" +chmod u=rwx,g=rx,o=rx "$adu_conf_dir" +chmod u=rw,g=r,o=r "$adu_conf_dir/$adu_conf_file" + +chown "$adu_user:$adu_group" "$adu_conf_dir/$adu_diagnostics_conf_file" +chmod u=r,g=r,o=r "$adu_conf_dir/$adu_diagnostics_conf_file" + +if [ ! -d "$adu_home_dir" ]; then + mkdir -p "$adu_home_dir" +fi + +chown "$adu_user:$adu_group" "$adu_home_dir" +chmod u=rwx,g=rx "$adu_home_dir" + +# Create log dir +if [ ! -d "$adu_log_dir" ]; then + mkdir -p "$adu_log_dir" +fi + +# Ensure the right owners and permissions +chown "$adu_user:$adu_group" "$adu_log_dir" +chmod u=rwx,g=rx "$adu_log_dir" + +# Create data dir +echo "Create data dir..." +if [ ! -d "$adu_data_dir" ]; then + mkdir -p "$adu_data_dir" +fi + +# Ensure the right owners and permissions +chown "$adu_user:$adu_group" "$adu_data_dir" +chmod u=rwx,g=rwx,o= "$adu_data_dir" +ls -ld "$adu_data_dir" + +# Create downloads dir +if [ ! -d "$adu_downloads_dir" ]; then + mkdir -p "$adu_downloads_dir" +fi + +# Ensure the right owners and permissions +chown "$adu_user:$adu_group" "$adu_downloads_dir" +chmod u=rwx,g=rwx,o= "$adu_downloads_dir" +ls -ld "$adu_downloads_dir" + +# Create extensions dir +if [ ! -d "$adu_extensions_dir" ]; then + mkdir -p "$adu_extensions_dir" +fi + +# Ensure the right owners and permissions +chown "$adu_user:$adu_group" "$adu_extensions_dir" +chmod u=rwx,g=rwx,o= "$adu_extensions_dir" + +# Create extensions sources dir +if [ ! -d "$adu_extensions_sources_dir" ]; then + mkdir -p "$adu_extensions_sources_dir" +fi + +# Ensure the right owners and permissions +chown "$adu_user:$adu_group" "$adu_extensions_sources_dir" +chmod u=rwx,g=rwx,o= "$adu_extensions_sources_dir" + +# Set deviceupdate-agent owner and permission +chown "root:$adu_group" "$adu_shell_dir/$adu_shell_file" +chmod u=rxs "$adu_shell_dir/$adu_shell_file" + +# +# misc for healthcheck +# + +addgroup --system 'do' +adduser --system 'do' --ingroup 'do' --no-create-home --shell /bin/false +usermod -aG 'adu' 'do' +usermod -aG 'do' 'adu' +chown root:root /usr/bin/AducIotAgent +chown adu:adu /etc/adu/du-config.json diff --git a/scripts/error_code_generator_defs/error_code_defs_generator.py b/scripts/error_code_generator_defs/error_code_defs_generator.py new file mode 100755 index 000000000..1617d7586 --- /dev/null +++ b/scripts/error_code_generator_defs/error_code_defs_generator.py @@ -0,0 +1,701 @@ +#! /usr/bin/env python3 + +import getopt +import json +import os +from platform import version +import sys + +error_code_definition_file = """\ +/** +* @file result.h +* @brief Describes the ADUC result type. Generated from result_codes.json by ErrorCodeDefinitionGenerator.py +* +* @copyright Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ +#ifndef ADUC_RESULT_H +#define ADUC_RESULT_H + +#include // _Bool +#include // int32_t + +/** +* @brief Defines the type of an ADUC_Result. +*/ +typedef int32_t ADUC_Result_t; + +/** +* @brief Defines an ADUC_Result object which is used to indicate status. +*/ +typedef struct tagADUC_Result +{{ + ADUC_Result_t ResultCode; /**< Method-specific result. Value > 0 indicates success. */ + ADUC_Result_t ExtendedResultCode; /**< Implementation-specific extended result code. */ +}} ADUC_Result; + +/** +* @brief Return value for general API calls. +*/ +typedef enum tagADUC_GeneralResult +{{ + ADUC_GeneralResult_Failure = 0, /**< Failed. */ + ADUC_GeneralResult_Success = 1, /**< Succeeded. */ +}} ADUC_GeneralResult; + +/** +* @brief Determines if a result code is succeeded. +*/ +static inline _Bool IsAducResultCodeSuccess(const ADUC_Result_t resultCode) +{{ + return (resultCode > 0); +}} + +/** +* @brief Determines if a result code is failed. +*/ +static inline _Bool IsAducResultCodeFailure(const ADUC_Result_t resultCode) +{{ + return (resultCode <= 0); +}} + +/** +* Extended Result Code Structure (32 bits) +* +* Note: We're discussing a possibility to increase the size to 64 bits. +* +* 0 00 00000 Total 4 bytes (32 bits) +* - -- ----- +* | | | +* | | | +* | | +--------- Error code (20 bits) +* | | +* | +------------- Component/Area code (8 bits) +* | +* +--------------- Facility code (4 bits) +*/ +static inline ADUC_Result_t +MAKE_ADUC_EXTENDEDRESULTCODE(const unsigned int facility, const unsigned int component, const unsigned int value) +{{ + return ((facility & 0xF) << 0x1C) | ((component & 0xFF) << 0x14) | (value & 0xFFFFF); +}} + +// +// Facility and Component Code Definitions +// +{FacilityAndComponentEnums} +// +// Extended Result Code Make Functions +// +{ExtendedResultCodeGenerationFunctions} +// +// Extended Result Code Definitions +// +{ExtendedResultCodeDefines} +// +// STATIC FUNCTIONS, NOT GENERATED BUT USE GENERATED FUNCTIONS +// + + +/** + * @brief Creates an extended error code for errors from delta processor API. + * The facility code for these errors is ADUC_FACILITY_DOWNLOAD_HANDLER. + * The component code for these errors is ADUC_COMPONENT_DELTA_DOWNLOAD_HANDLER_DELTA_PROCESSOR. + * + * @param errorCode The error code. + */ +#define MAKE_DELTA_PROCESSOR_EXTENDEDRESULTCODE(errorCode) MAKE_ADUC_EXTENDEDRESULTCODE_FOR_COMPONENT_ADUC_COMPONENT_DELTA_DOWNLOAD_HANDLER_DELTA_PROCESSOR(errorCode) + +// +// Delivery Optimization HRESULT to Extended Result Code Functions +// + +/** + * @brief Macros to convert Delivery Optimization results to extended result code values. + */ +static inline ADUC_Result_t MAKE_ADUC_DELIVERY_OPTIMIZATION_EXTENDEDRESULTCODE(const int32_t value) +{{ + return ((ADUC_FACILITY_DELIVERY_OPTIMIZATION & 0xF) << 0x1C) | (value & 0xFFFFFFF); +}} + +// +// Reserved extension common error codes (0-300) +// + +#define ADUC_ERC_EXTENSION_ERROR_NONE(facility, component) MAKE_ADUC_EXTENDEDRESULTCODE(facility, component, 0) + +#define ADUC_ERC_EXTENSION_CREATE_FAILURE_INVALID_ARG(facility, component) MAKE_ADUC_EXTENDEDRESULTCODE(facility, component, 1) + +#define ADUC_ERC_EXTENSION_CREATE_FAILURE_UNKNOWN(facility, component) MAKE_ADUC_EXTENDEDRESULTCODE(facility, component, 2) + +#define ADUC_ERC_EXTENSION_CREATE_FAILURE_NOT_FOUND(facility, component) MAKE_ADUC_EXTENDEDRESULTCODE(facility, component, 3) + +#define ADUC_ERC_EXTENSION_CREATE_FAILURE_VALIDATE(facility, component) MAKE_ADUC_EXTENDEDRESULTCODE(facility, component, 4) + +#define ADUC_ERC_EXTENSION_CREATE_FAILURE_LOAD(facility, component) MAKE_ADUC_EXTENDEDRESULTCODE(facility, component, 5) + +#define ADUC_ERC_EXTENSION_FAILURE_REQUIRED_FUNCTION_NOTIMPL(facility, component) MAKE_ADUC_EXTENDEDRESULTCODE(facility, component, 6) + +#define ADUC_ERC_EXTENSION_CREATE_FAILURE_CREATE(facility, component) MAKE_ADUC_EXTENDEDRESULTCODE(facility, component, 7) + +// +// Child process error code generators +// + +#define ADUC_ERC_SWUPDATE_HANDLER_CHILD_FAILURE_PROCESS_EXITCODE(exitCode) MAKE_ADUC_EXTENDEDRESULTCODE_FOR_COMPONENT_ADUC_CONTENT_HANDLER_SWUPDATE((0x1000 + exitCode)) + +#define ADUC_ERC_SCRIPT_HANDLER_CHILD_PROCESS_FAILURE_EXITCODE(exitCode) MAKE_ADUC_EXTENDEDRESULTCODE_FOR_COMPONENT_ADUC_CONTENT_HANDLER_SCRIPT((0x1000 + exitCode)) + +#define ADUC_ERROR_DELIVERY_OPTIMIZATION_DOWNLOADER_EXTERNAL_FAILURE(exitCode) MAKE_ADUC_EXTENDEDRESULTCODE_FOR_COMPONENT_ADUC_CONTENT_DOWNLOADER_DELIVERY_OPTIMIZATION((0x1000 + exitCode)) + +#define ADUC_ERROR_CURL_DOWNLOADER_EXTERNAL_FAILURE(exitCode) MAKE_ADUC_EXTENDEDRESULTCODE_FOR_COMPONENT_ADUC_CONTENT_DOWNLOADER_CURL_DOWNLOADER((0x1000 + exitCode)) + +#endif // ADUC_RESULT_H +""" + +facility_func_definition = """\ +/** + * @brief Extended Result Codes for {FacilityName} Facility +*/ +static inline ADUC_Result_t {FacilityFuncDeclString}(const unsigned int component, const int32_t value) +{{ + return MAKE_ADUC_EXTENDEDRESULTCODE({FacilityName}, component, value); +}} +""" + +component_func_definition = """\ +/** +* @brief Function for generating Extended Result Codes for {ComponentName} Component +*/ +static inline ADUC_Result_t {ComponentDecl}(const int32_t value) +{{ + return {FacilityFuncDeclString}({ComponentName}, value); +}} +""" + + +tabsize = 2 + +def CreateBriefDoxyString(brief): + """ + Returns a doxygen style comment with the passed brief + """ + doxygen_string = f"""\ +/** + * @brief {brief} + */ + """ + + return doxygen_string + + +def CreateEnumDoxyString(brief): + """ + Creates an enum doxygen documentation string with the passed brief + """ + return f"//!< {brief}" + + + +def MakeERC(facility, component, result): + """ + Creates an ExtendedResultCode from the facility, component, and result + + Returns the ExtendedResultCode value calculated using the facility, component, and result codes + """ + return ((facility & 0xF) << 0x1C) | ((component & 0xFF) << 0x14) | (0xFFFFF & result) + + +class FacilityCode(object): + def __init__(self, _name, _value, _doc_string=""): + self.name = _name + self.value = _value + self.doc_string = _doc_string + self.components = [] + + def __eq__(self, __o): + + if (isinstance(__o, FacilityCode)): + if (self.value == __o.value or self.name == __o.name): + return True + return False + + def add_component(self,component): + if (component not in self.components): + self.components.append(component) + return True + return False + + def get_facility_name(self): + return self.name + + def get_components(self): + return self.components + + def get_brief_doxy_comment(self): + brief = f"{self.name} : {self.value}, {self.doc_string}" + return CreateBriefDoxyString(brief) + + def get_enum_doxy_comment(self): + brief = f"{self.name} : {self.value}, {self.doc_string}" + return CreateEnumDoxyString(brief) + + def get_c_define(self, define_offset=0): + offset = define_offset*" " + return f"#{offset}define {self.name} {self.value}\n" + + def get_enum_define_with_comment(self, define_offset=2): + offset = define_offset*" " + return f"{offset}{self.name}={self.value}, {self.get_enum_doxy_comment()}\n" + + def get_func_def_string(self): + return facility_func_definition.format(FacilityName=self.name, FacilityFuncDeclString=self.get_facility_func_decl_string()) + + def get_facility_func_decl_string(self): + return f"MAKE_ADUC_EXTENDEDRESULTCODE_FOR_FACILITY_{self.name}" + + +class ComponentCode(object): + def __init__(self, _name, _value, _doc_string, _facility_code): + self.name = _name + self.value = _value + self.doc_string = _doc_string + self.facility_code = _facility_code + + self.results = [] + + def __eq__(self, __o): + + if (isinstance(__o, ComponentCode)): + if ((self.value == __o.value and self.facility_code == __o.facility_code) or self.name == __o.name): + return True + return False + + def add_result(self, result): + if (result not in self.results): + self.results.append(result) + return True + return False + + def get_component_name(self): + return self.name + + def get_results(self): + return self.results + + def get_doxy_comment(self): + brief = self.name + " : " + str(self.value) + ", " + self.doc_string + return CreateBriefDoxyString(brief) + + def get_enum_doxy_comment(self): + brief = self.name + " : " + str(self.value) + ", " + self.doc_string + return CreateEnumDoxyString(brief) + + def get_c_define(self, define_offset=0): + offset = define_offset*" " + return f"#{offset}define {self.name} {self.value}\n" + + def get_enum_define_with_comment(self, define_offset=2): + offset = define_offset*" " + return f"{offset}{self.name}={self.value}, {self.get_enum_doxy_comment()}\n" + + def get_func_def_string(self, facility_func_decl): + return component_func_definition.format(ComponentName=self.name, ComponentDecl=self.get_component_func_decl_string(), FacilityFuncDeclString=facility_func_decl) + + def get_component_func_decl_string(self): + return f"MAKE_ADUC_EXTENDEDRESULTCODE_FOR_COMPONENT_{self.name}" + + +class ResultCode(object): + def __init__(self, _name, _rc_value, _fc_value, _cc_value): + self.name = _name + self.rc_value = _rc_value + self.full_erc_value = MakeERC(_fc_value, _cc_value, _rc_value) + + def __eq__(self, __o): + + if (isinstance(__o, ResultCode)): + if (self.full_erc_value == __o.full_erc_value or self.name == __o.name): + return True + return False + + def get_doxy_comment(self): + hex_string = hex(self.full_erc_value) + brief = f"{self.name}, ERC Value: {self.full_erc_value} ({hex_string})" + return CreateBriefDoxyString(brief) + + def get_c_definition(self, component_func_decl, define_offset=0): + offset = define_offset*" " + return f"#{offset}define {self.name} {component_func_decl}({self.rc_value})\n" + + def get_result_name(self): + return self.name + + +class ErrorCodeDefinitionGenerator(object): + def __init__(self, _json_path, _result_h_path="./result.h", *args): + """ + Initializer for the class that generates the result.h file from result_codes.json + + :param str: _json_path path to the json file defining the facilities, components, and results + :param str: _result_h_path path to generate the result.h file at + """ + super(ErrorCodeDefinitionGenerator, self).__init__(*args) + self.result_h_path = _result_h_path + self.json_path = _json_path + self.facilities = [] + + def add_facility_code(self, facility): + """ + Adds a facility code to the list of facilities if it does not already exist in the list + + Returns true if the facility is added; false otherwise + """ + if (facility not in self.facilities): + self.facilities.append(facility) + return True + return False + + def get_facility_enum_def(self): + """ + Processes the facilities and creates the C enum for defining these facilities + + Returns the string version of the C enum for the facilities + """ + facility_enum_body_string = "" + + # For building facilities enum + for facility in self.facilities: + + facility_enum_body_string += facility.get_enum_define_with_comment( + tabsize*2) + + full_facility_enum = f"""\ +/** + * @brief Facility codes to pass to MAKE_ADUC_EXTENDEDRESULTCODE. + */ +typedef enum tagADUC_Facility +{{ +{facility_enum_body_string}}} ADUC_Facility; +""" + + return full_facility_enum + + def get_component_enum_for_facility(self, facility): + """ + Creates the enumeration of components for the passed facility + + Returns the string definition of a C enum for the components in the facility + """ + component_enum_body_string = "" + + components = facility.get_components() + + for component in components: + component_enum_body_string += component.get_enum_define_with_comment( + tabsize*2) + + f_name = facility.get_facility_name() + full_component_body_string = f"""\ +typedef enum tag{f_name}_Components +{{ +{component_enum_body_string}}} {f_name}_Components; + +""" + + return full_component_body_string + + def get_facility_and_component_code_defs(self): + """ + Takes in the facilities and components and creates the enums which define the facilities and components + + Returns the string version of the C definitions of the facilities and components + """ + f_and_c_def_string = "" + + facility_enum = self.get_facility_enum_def() + + if (facility_enum == ""): + return "" + + component_enums = "" + # For building Components Enum + for facility in self.facilities: + + if (len(facility.get_components()) != 0): + + component_enum = self.get_component_enum_for_facility(facility) + + if (component_enum == ""): + return "" + + component_enums += component_enum + + return f"{facility_enum}\n{component_enums}" + + def get_erc_make_functions(self): + """ + Takes in the facilities and components and generates the static inline function for creating the ExtendedResultCodes + + Returns a string version of the C macro definitions for creating the ExtendedResultCodes + """ + erc_make_functions = "" + + for facility in self.facilities: + + facility_make_erc_function = facility.get_func_def_string() + + erc_make_functions += f"{facility_make_erc_function}\n" + + # Add the components below + components = facility.get_components() + for component in components: + component_make_erc_def_function = component.get_func_def_string( + facility.get_facility_func_decl_string()) + erc_make_functions += f"{component_make_erc_def_function}\n" + + return erc_make_functions + + def get_extended_result_code_defs(self): + """ + Takes in the facilities, components, and results and returns the #defines for the ExtendedResultCodes + + Return the string version of the C #definitions for the ExtendedResultCodes + """ + erc_defs = "" + for facility in self.facilities: + + components = facility.get_components() + + for component in components: + + results = component.get_results() + component_func_decl_string = component.get_component_func_decl_string() + + for result in results: + result_doxy_comment = result.get_doxy_comment() + result_def = result.get_c_definition( + component_func_decl_string) + erc_defs += f"{result_doxy_comment}{result_def}\n" + + return erc_defs + + def generate_result_h_file(self): + """ + Generates the result.h file to the result_h_path/result.h file + + Returns true on success; false on failure + """ + facility_and_component_code_defs = self.get_facility_and_component_code_defs() + erc_generation_functions = self.get_erc_make_functions() + extended_result_code_defs = self.get_extended_result_code_defs() + + if (facility_and_component_code_defs == "" or erc_generation_functions == "" or extended_result_code_defs == ""): + return False + + file_contents = error_code_definition_file.format(FacilityAndComponentEnums=facility_and_component_code_defs, + ExtendedResultCodeGenerationFunctions=erc_generation_functions, ExtendedResultCodeDefines=extended_result_code_defs) + + try: + with open(self.result_h_path, 'w') as result_h_file: + result_h_file.write(file_contents) + + except Exception as e: + + print("An unknown error occurred", e) + return False + + return True + + def parse_json(self): + """ + Parses the json file from the json_path passed at object creation and stores data + + Returns true on success; false on failure + """ + try: + file = open(self.json_path) + + except FileNotFoundError as e: + print("Could not JSON file for reading at: " + + str(self.json_path) + " ", e) + return False + except Exception as e: + print(f"An unknown error occurred while opening the json file path at: {self.json_path} ", e) + return False + + try: + result_json = json.load(file) + + except Exception as e: + print(f"An unknown error occurred while loading the json from file path: {self.json_path}", e) + return False + + facilities = result_json["facilities"] + + for facility_json_obj in facilities: + + f_code = facility_json_obj["code"] + + if (type(f_code) != int): + print("Facility codes must be of type int") + return False + + f_name = facility_json_obj["name"] + + if (type(f_name) != str): + print("Facility names must be of type str") + return False + + f_doc_string = facility_json_obj["doc_string"] + + if (type(f_doc_string) != str): + print("Facility doc strings must be of type str") + return False + + facility = FacilityCode(f_name, f_code, f_doc_string) + + if (not self.add_facility_code(facility)): + print(f"Facility {f_name} is a duplicate!") + return False + + for component_json_obj in facility_json_obj["components"]: + + c_code = component_json_obj["code"] + + if (type(c_code) != int): + print("Component codes must be of type int") + return False + + c_name = component_json_obj["name"] + + if (type(c_name) != str): + print("Component names must be of type str") + return False + + c_doc_string = component_json_obj["doc_string"] + + if (type(c_doc_string) != str): + print("Component doc strings must of type str") + return False + + component = ComponentCode(c_name, c_code, c_doc_string, f_code) + + if (not facility.add_component(component)): + print(f"Component {component.name} is a duplicate!") + return False + + for result_json_obj in component_json_obj["results"]: + + r_val = result_json_obj["value"] + + if (type(r_val) != int): + print("Result code must be of type int") + return False + + r_name = result_json_obj["name"] + + if (type(r_name) != str): + print("Result name must be of type str") + return False + + result = ResultCode(r_name, r_val, f_code, c_code) + + if (not component.add_result(result)): + print(f"Result code: {r_name} is a duplicate!") + return False + + return True + + +help_msg = """\ +Usage: python3 ./ErrorCodeDefinitionGenerator.py [options...] +-j, --json-file-path The file path to the json file that describes the results +-r, --result-file-path The file to write the error code definitions to +-h, --help Prints this message +""" + + +def print_help_msg(): + print(help_msg) + + +if (__name__ == '__main__'): + + shortopts = "j:r:h" + longopts = ["json-file-path", "result-file-path", "help"] + + optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) + + json_file_path = "" + result_file_path = "" + + for opt, val in optlist: + + if (opt == "-j" or opt == "--" + longopts[0]): + json_file_path = val + elif (opt == "-r" or opt == "--"+longopts[0]): + result_file_path = val + elif (opt == "-h" or opt == "--"+longopts[0]): + print_help_msg() + sys.exit(1) + else: + print("Unrecognized command " + opt) + print_help_msg() + sys.exit(1) + + if (json_file_path == "" or result_file_path == ""): + print("No arguments passed...") + print_help_msg() + sys.exit(1) + + if (not os.path.exists(json_file_path)): + print("The path: " + json_file_path + " does not exist!") + quit() + + if (not os.path.isfile(json_file_path)): + print("Option json-file-path must have a file path passed to it...") + print(json_file_path + " is not a file") + sys.exit(1) + + result_dest_dir = os.path.dirname(result_file_path) + if (not os.path.exists(result_dest_dir)): + print("The directory for: " + result_dest_dir + " does not exist!") + sys.exit(1) + + if (not os.path.isdir(result_dest_dir)): + print("The parent directory for the file to be generated is not a directory!") + print(result_dest_dir + " is not a directory") + sys.exit(1) + + if ( os.path.exists(result_file_path) ): + print("Overwriting file at " + result_file_path) + + file_name = os.path.basename(result_file_path) + if (file_name == "" ): + print("result-file-path must end with a file to create!") + sys.exit(1) + + result_h_abs_path = os.path.abspath(result_file_path) + + json_file_abs_path = os.path.abspath(json_file_path) + + print("Generating result.h file at: " + str(result_h_abs_path)) + + result_generator = ErrorCodeDefinitionGenerator( + json_file_abs_path, result_h_abs_path) + + print("Parsing JSON file...") + + if (not result_generator.parse_json()): + print("Failed to parse json file: " + str(json_file_path)) + sys.exit(1) + + print("Generating result.h file...") + + if (not result_generator.generate_result_h_file()): + print("Failed to generate result.h file at " + str(result_h_abs_path)) + sys.exit(1) + + # Exit successfully + sys.exit(0) diff --git a/scripts/error_code_generator_defs/result_codes.json b/scripts/error_code_generator_defs/result_codes.json new file mode 100644 index 000000000..c07a15048 --- /dev/null +++ b/scripts/error_code_generator_defs/result_codes.json @@ -0,0 +1,1025 @@ +{ + "facilities": [ + { + "components": [ + { + "code": 0, + "doc_string": "General Error Codes, that come from POSIX 2001 Standard Errnos", + "name": "ERRNO", + "results": [ + { + "name": "ADUC_ERC_NOTPERMITTED", + "value": 1 + }, + { + "name": "ADUC_ERC_NOMEM", + "value": 12 + }, + { + "name": "ADUC_ERC_NOTRECOVERABLE", + "value": 131 + } + ] + }, + { + "code": 1, + "doc_string": "validation and cryptographic component", + "name": "ADUC_COMPONENT_CRYPTO", + "results": [ + { + "name": "ADUC_ERC_VALIDATION_FILE_HASH_IS_EMPTY", + "value": 1 + }, + { + "name": "ADUC_ERC_VALIDATION_FILE_HASH_TYPE_NOT_SUPPORTED", + "value": 2 + }, + { + "name": "ADUC_ERC_VALIDATION_FILE_HASH_INVALID_HASH", + "value": 3 + } + ] + } + ], + "code": 0, + "doc_string": "indicates errors from unknown sources", + "name": "ADUC_FACILITY_UNKNOWN" + }, + { + "components": [ + { + "code": 0, + "doc_string": "non component specific error codes", + "name": "ADUC_LOWERLAYER_COMMON", + "results": [ + { + "name": "ADUC_ERC_LOWERLEVEL_INVALID_UPDATE_ACTION", + "value": 1 + }, + { + "name": "ADUC_ERC_LOWERLEVEL_UPDATE_MANIFEST_VALIDATION_INVALID_HASH", + "value": 2 + }, + { + "name": "ADUC_ERC_LOWERLEVEL_GET_FILE_ENTITY_FAILURE", + "value": 3 + }, + { + "name": "ADUC_ERC_LOWERLEVEL_SANDBOX_CREATE_FAILURE_NO_ADU_USER", + "value": 5 + }, + { + "name": "ADUC_ERC_LOWERLEVEL_SANDBOX_CREATE_FAILURE_NO_ADU_GROUP", + "value": 6 + }, + { + "name": "ADUC_ERC_LOWERLEVEL_WORKFLOW_INIT_ERROR_NULL_PARAM", + "value": 20 + } + ] + } + ], + "code": 1, + "doc_string": "indicates errors from the lower layer", + "name": "ADUC_FACILITY_LOWERLAYER" + }, + { + "components": [ + { + "code": 0, + "doc_string": "non component specific error codes", + "name": "ADUC_UPPERLAYER_COMMON", + "results": [ + { + "name": "ADUC_ERC_UPPERLEVEL_WORKFLOW_UPDATE_ACTION_UNEXPECTED_STATE", + "value": 1 + }, + { + "name": "ADUC_ERC_UPPERLEVEL_WORKFLOW_INSTALL_ACTION_IN_UNEXPECTED_STATE", + "value": 2 + }, + { + "name": "ADUC_ERC_UPPERLEVEL_WORKFLOW_FAILED_RESTORE_FAILED", + "value": 3 + } + ] + } + ], + "code": 2, + "doc_string": "indicates errors from the upper layer.", + "name": "ADUC_FACILITY_UPPERLAYER" + }, + { + "components": [ + { + "code": 0, + "doc_string": "General error codes that can come from any content handler", + "name": "ADUC_CONTENT_HANDLER_COMMON", + "results": [ + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_INVALID_ARG", + "value": 1 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_UNKNOWN", + "value": 2 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_NOT_FOUND", + "value": 3 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_VALIDATE", + "value": 4 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_LOAD", + "value": 5 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_NO_SYMBOL", + "value": 6 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_CREATE", + "value": 7 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_PREPARE_FAILURE_WRONG_VERSION", + "value": 101 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_PARSE_BAD_FORMAT", + "value": 104 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_DOWNLOAD_FAILURE_BADFILECOUNT", + "value": 201 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_DOWNLOAD_FAILURE_UNKNOWNEXCEPTION", + "value": 202 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_INSTALL_FAILURE_NULL_WORKFLOW", + "value": 301 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_INSTALL_FAILURE_MISSING_PRIMARY_FILE", + "value": 302 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_INSTALL_FAILURE_MISSING_PRIMARY_COMPONENT", + "value": 303 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_INSTALL_FAILURE_UNKNOWNEXCEPTION", + "value": 304 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_ISINSTALLED_FAILURE_BAD_UPDATETYPE", + "value": 501 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_ISINSTALLED_FAILURE_COMPONENT_UNAVAILABLE", + "value": 502 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_ISINSTALLED_FAILURE_NULL_WORKFLOW", + "value": 503 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_ISINSTALLED_FAILURE_NULL_WORKFLOW_HANDLE", + "value": 504 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_ISINSTALLED_FAILURE_NO_STEPS", + "value": 505 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_ISINSTALLED_FAILURE_ACCESS_DATA", + "value": 506 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_GET_CONTRACT_INFO_CALL_FAILURE", + "value": 507 + }, + { + "name": "ADUC_ERC_UPDATE_CONTENT_HANDLER_UNSUPPORTED_CONTRACT_VERSION", + "value": 508 + } + ] + }, + { + "code": 1, + "doc_string": "Results that originate from the SwUpdate Content Handler", + "name": "ADUC_CONTENT_HANDLER_SWUPDATE", + "results": [ + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_EMPTY_VERSION", + "value": 1 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_PREPARE_FAILURE_UNKNOWN_UPDATE_VERSION", + "value": 2 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_PREPARE_FAILURE_WRONG_UPDATE_VERSION", + "value": 3 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_PREPARE_FAILURE_WRONG_FILECOUNT", + "value": 4 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_PREPARE_FAILURE_BAD_FILE_ENTITY", + "value": 5 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_CREATE_SANDBOX_FAILURE", + "value": 6 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_MISSING_SCRIPT_FILE_NAME", + "value": 7 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_MISSING_SWU_FILE_NAME", + "value": 8 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_TOO_MANY_COMPONENTS", + "value": 9 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_INVALID_COMPONENTS_DATA", + "value": 10 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_BAD_SWUPDATE_CONFIG_FILE", + "value": 32 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_FAILURE_UNKNOWN_UPDATE_VERSION", + "value": 257 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_FAILURE_WRONG_UPDATE_VERSION", + "value": 258 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_FAILURE_WRONG_FILECOUNT", + "value": 259 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_BAD_FILE_ENTITY", + "value": 260 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_ERROR_NULL_WORKFLOW", + "value": 261 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_FAILURE_GET_PRIMARY_FILE_ENTITY", + "value": 262 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_FAILURE_GET_SCRIPT_FILE_ENTITY", + "value": 263 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_FAILURE_GET_PAYLOAD_FILE_ENTITY", + "value": 264 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_PAYLOAD_FILE_FAILURE_UNKNOWNEXCEPTION", + "value": 510 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_PRIMARY_FILE_FAILURE_UNKNOWNEXCEPTION", + "value": 511 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_INSTALL_FAILURE_CANNOT_OPEN_WORKFOLDER", + "value": 513 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_INSTALL_FAILURE_IMAGE_FILE_NOT_FOUND", + "value": 514 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_INSTALL_ERROR_NULL_WORKFLOW", + "value": 515 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_INSTALL_FAILURE_BAD_FILE_ENTITY", + "value": 516 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_INSTALL_FAILURE_PARSE_RESULT_FILE", + "value": 517 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_MISSING_INSTALLED_CRITERIA", + "value": 518 + }, + { + "name": "ADUC_ERC_SWUPDATE_HANDLER_INSTALL_FAILURE_UNKNOWNEXCEPTION", + "value": 767 + } + ] + }, + { + "code": 2, + "doc_string": "Results that originate from the APT Content Handler", + "name": "ADUC_CONTENT_HANDLER_APT", + "results": [ + { + "name": "ADUC_ERC_APT_HANDLER_ERROR_NONE", + "value": 0 + }, + { + "name": "ADUC_ERC_APT_HANDLER_INITIALIZATION_FAILURE", + "value": 1 + }, + { + "name": "ADUC_ERC_APT_HANDLER_INVALID_PACKAGE_DATA", + "value": 2 + }, + { + "name": "ADUC_ERC_APT_HANDLER_PACKAGE_PREPARE_FAILURE_WRONG_VERSION", + "value": 3 + }, + { + "name": "ADUC_ERC_APT_HANDLER_PACKAGE_PREPARE_FAILURE_WRONG_FILECOUNT", + "value": 4 + }, + { + "name": "ADUC_ERC_APT_HANDLER_GET_FILEENTITY_FAILURE", + "value": 5 + }, + { + "name": "ADUC_ERC_APT_HANDLER_INSTALLCRITERIA_PERSIST_FAILURE", + "value": 6 + }, + { + "name": "ADUC_ERC_APT_HANDLER_MISSING_INSTALLED_CRITERIA", + "value": 7 + }, + { + "name": "ADUC_ERC_APT_HANDLER_PACKAGE_DOWNLOAD_FAILURE", + "value": 256 + }, + { + "name": "ADUC_ERC_APT_HANDLER_PACKAGE_INSTALL_FAILURE", + "value": 512 + }, + { + "name": "ADUC_ERC_APT_HANDLER_PACKAGE_CANCEL_FAILURE", + "value": 768 + } + ] + }, + { + "code": 3, + "doc_string": "Results that originate from the simulator, only included here for completeness", + "name": "ADUC_CONTENT_HANDLER_SIMULATOR", + "results": [] + }, + { + "code": 4, + "doc_string": "Results that originate from the steps handler part of the content handlers", + "name": "ADUC_CONTENT_HANDLER_STEPS", + "results": [ + { + "name": "ADUC_ERC_STEPS_HANDLER_GET_FILE_ENTITY_FAILURE", + "value": 1 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_INVALID_CHILD_MANIFEST_FILENAME", + "value": 2 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_CHILD_WORKFLOW_CREATE_FAILED", + "value": 3 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_CHILD_WORKFLOW_INSERT_FAILED", + "value": 4 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_GET_REF_STEP_COMPATIBILITY_FAILED", + "value": 5 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_SET_SELECTED_COMPONENTS_FAILED", + "value": 6 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_GET_STEP_COMPATIBILITY_FAILED", + "value": 7 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_SET_SELECTED_COMPONENTS_FAILURE", + "value": 8 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_NESTED_REFERENCE_STEP_DETECTED", + "value": 9 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_INVALID_COMPONENTS_DATA", + "value": 10 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_CREATE_SANDBOX_FAILURE", + "value": 11 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_DOWNLOAD_FAILURE_UNKNOWNEXCEPTION", + "value": 256 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_DOWNLOAD_FAILURE_MISSING_CHILD_WORKFLOW", + "value": 257 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_DOWNLOAD_UNKNOWN_EXCEPTION_DOWNLOAD_CONTENT", + "value": 258 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_INSTALL_FAILURES_UNKNOWNEXCEPTION", + "value": 512 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_ISINSTALLED_FAILURE_MISSING_CHILD_WORKFLOW", + "value": 501 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_INSTALL_FAILURE_MISSING_CHILD_WORKFLOW", + "value": 513 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_INSTALL_UNKNOWN_EXCEPTION_INSTALL_CHILD_STEP", + "value": 514 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_INSTALL_UNKNOWN_EXCEPTION_APPLY_CHILD_STEP", + "value": 515 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_INSTALL_UNKNOWN_EXCEPTION_BACKUP_CHILD_STEP", + "value": 516 + }, + { + "name": "ADUC_ERC_STEPS_HANDLER_INSTALL_UNKNOWN_EXCEPTION_RESTORE_CHILD_STEP", + "value": 517 + } + ] + }, + { + "code": 5, + "doc_string": "Results that originate from the script handler part of the content handlers", + "name": "ADUC_CONTENT_HANDLER_SCRIPT", + "results": [ + { + "name": "ADUC_ERC_SCRIPT_HANDLER_ERROR_NONE", + "value": 0 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_SET_SELECTED_COMPONENTS_FAILURE", + "value": 1 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_MISSING_INSTALLED_CRITERIA", + "value": 2 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_TOO_MANY_COMPONENTS", + "value": 3 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_MISSING_PRIMARY_SCRIPT_FILE", + "value": 4 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_MISSING_SCRIPTFILENAME_PROPERTY", + "value": 5 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_CREATE_SANDBOX_FAILURE", + "value": 6 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_INVALID_COMPONENTS_DATA", + "value": 7 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_DOWNLOAD_ERROR_NULL_WORKFLOW", + "value": 257 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_DOWNLOAD_FAILURE_INVALID_FILE_COUNT", + "value": 258 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_DOWNLOAD_FAILURE_GET_PRIMARY_FILE_ENTITY", + "value": 259 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_DOWNLOAD_FAILURE_GET_PAYLOAD_FILE_ENTITY", + "value": 260 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_DOWNLOAD_PAYLOAD_FILE_FAILURE_UNKNOWNEXCEPTION", + "value": 510 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_DOWNLOAD_PRIMARY_FILE_FAILURE_UNKNOWNEXCEPTION", + "value": 511 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_INSTALL_ERROR_NULL_WORKFLOW", + "value": 513 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_INSTALL_INSTALLITEM_BAD_DATA", + "value": 514 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_INSTALL_INSTALLITEM_NO_UPDATE_TYPE", + "value": 515 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_INSTALL_FAILURE_MISSING_PRIMARY_SCRIPT_FILE", + "value": 516 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_INSTALL_FAILURE_PARSE_RESULT_FILE", + "value": 517 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_INSTALL_FAILURE_SCRIPT_RESULT_EXTENDEDRESULTCODE_ZERO", + "value": 518 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_INSTALL_FAILURE_UNKNOWNEXCEPTION", + "value": 767 + }, + { + "name": "ADUC_ERC_SCRIPT_HANDLER_APPLY_FAILURE_UNKNOWNEXCEPTION", + "value": 1023 + } + ] + }, + { + "code": 32, + "doc_string": "indicates errors from Custom Update handlers, only included here for completeness", + "name": "ADUC_CONTENT_HANDLER_EXTERNAL", + "results": [] + } + ], + "code": 3, + "doc_string": "indicates errors from an update content handler", + "name": "ADUC_FACILITY_EXTENSION_UPDATE_CONTENT_HANDLER" + }, + { + "components": [ + { + "code": 0, + "doc_string": "indicates common errors from the downloader extension", + "name": "ADUC_CONTENT_DOWNLOADER_COMMON", + "results": [ + { + "name": "ADUC_ERC_CONTENT_DOWNLOADER_CREATE_FAILURE_NO_SYMBOL", + "value": 1 + }, + { + "name": "ADUC_ERC_CONTENT_DOWNLOADER_INITIALIZEPROC_NOTIMP", + "value": 2 + }, + { + "name": "ADUC_ERC_CONTENT_DOWNLOADER_DOWNLOADPROC_NOTIMP", + "value": 3 + }, + { + "name": "ADUC_ERC_CONTENT_DOWNLOADER_INITIALIZE_EXCEPTION", + "value": 4 + }, + { + "name": "ADUC_ERC_CONTENT_DOWNLOADER_DOWNLOAD_EXCEPTION", + "value": 5 + }, + { + "name": "ADUC_ERC_CONTENT_DOWNLOADER_INVALID_FILE_ENTITY", + "value": 6 + }, + { + "name": "ADUC_ERC_CONTENT_DOWNLOADER_INVALID_DOWNLOAD_URI", + "value": 7 + }, + { + "name": "ADUC_ERC_CONTENT_DOWNLOADER_FILE_HASH_TYPE_NOT_SUPPORTED", + "value": 8 + }, + { + "name": "ADUC_ERC_CONTENT_DOWNLOADER_BAD_CHILD_MANIFEST_FILE_PATH", + "value": 9 + }, + { + "name": "ADUC_ERC_CONTENT_DOWNLOADER_CANNOT_DELETE_EXISTING_FILE", + "value": 10 + }, + { + "name": "ADUC_ERC_CONTENT_DOWNLOADER_INVALID_FILE_ENTITY_NO_HASHES", + "value": 11 + }, + { + "name": "ADUC_ERC_CONTENT_DOWNLOADER_INVALID_FILE_HASH", + "value": 12 + }, + { + "name": "ADUC_ERC_CONTENT_DOWNLOADER_UNSUPPORTED_CONTRACT_VERSION", + "value": 13 + } + ] + }, + { + "code": 1, + "doc_string": "indicates errors from Delivery Optimization agent. ", + "name": "ADUC_CONTENT_DOWNLOADER_DELIVERY_OPTIMIZATION", + "results": [ + { + "name": "ADUC_ERROR_DELIVERY_OPTIMIZATION_DOWNLOADER_NOT_INITIALIZE", + "value": 1 + }, + { + "name": "ADUC_ERROR_DELIVERY_OPTIMIZATION_DOWNLOADER_BAD_INIT_DATA", + "value": 2 + } + ] + }, + { + "code": 2, + "doc_string": "indicates errors from Simple Http Downloader. ", + "name": "ADUC_CONTENT_DOWNLOADER_SIMPLE_HTTP_DOWNLOADER", + "results": [] + }, + { + "code": 3, + "doc_string": "indicates errors from Curl Downloader.", + "name": "ADUC_CONTENT_DOWNLOADER_CURL_DOWNLOADER", + "results": [ + { + "name": "ADUC_ERROR_CURL_DOWNLOADER_INVALID_FILE_HASH", + "value": 1 + } + ] + } + ], + "code": 4, + "doc_string": "indicates errors from CONTENT_DOWNLOADER Extension", + "name": "ADUC_FACILITY_EXTENSION_CONTENT_DOWNLOADER" + }, + { + "components": [], + "code": 5, + "doc_string": "indicates errors from COMMUNICATION PROVIDER Extension.", + "name": "ADUC_FACILITY_EXTENSION_COMMUNICATION_PROVIDER" + }, + { + "components": [], + "code": 6, + "doc_string": "indicates errors from LOG PROVIDER Extension.", + "name": "ADUC_FACILITY_EXTENSION_LOG_PROVIDER" + }, + { + "components": [ + { + "code": 0, + "doc_string": "General errors from the Extension Component Enumerator", + "name": "ADUC_COMPONENT_EXTENSION_ENUMERATOR_COMMON", + "results": [ + { + "name": "ADUC_ERC_COMPONENT_ENUMERATOR_GETALLCOMPONENTS_NOTIMP", + "value": 1 + }, + { + "name": "ADUC_ERC_COMPONENT_ENUMERATOR_SELECTCOMPONENTS_NOTIMP", + "value": 2 + }, + { + "name": "ADUC_ERC_COMPONENT_ENUMERATOR_FREECOMPONENTSDATASTRING_NOTIMP", + "value": 3 + }, + { + "name": "ADUC_ERC_COMPONENT_ENUMERATOR_EXCEPTION_GETALLCOMPONENTS", + "value": 4 + }, + { + "name": "ADUC_ERC_COMPONENT_ENUMERATOR_EXCEPTION_SELECTCOMPONENTS", + "value": 5 + }, + { + "name": "ADUC_ERC_COMPONENT_ENUMERATOR_EXCEPTION_FREECOMPONENTSDATASTRING", + "value": 6 + }, + { + "name": "ADUC_ERC_COMPONENT_ENUMERATOR_UNSUPPORTED_CONTRACT_VERSION", + "value": 7 + } + ] + } + ], + "code": 7, + "doc_string": "indicates errors from COMPONENT ENUMERATOR Extension. ", + "name": "ADUC_FACILITY_EXTENSION_COMPONENT_ENUMERATOR" + }, + { + "components": [ + { + "code": 3, + "doc_string": "indicates errors from parsers.", + "name": "ADUC_COMPONENT_UPDATE_DATA_PARSER", + "results": [ + { + "name": "ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_INVALID_ACTION_JSON", + "value": 0 + }, + { + "name": "ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_MANIFEST_VALIDATION_FAILED", + "value": 1 + }, + { + "name": "ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_NO_UPDATE_ACTION", + "value": 2 + }, + { + "name": "ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_NO_UPDATE_ID", + "value": 3 + }, + { + "name": "ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_NO_UPDATE_TYPE", + "value": 4 + }, + { + "name": "ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_BAD_UPDATE_TYPE", + "value": 5 + }, + { + "name": "ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_NO_INSTALLEDCRITERIA", + "value": 6 + }, + { + "name": "ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_BAD_FILES_OR_FILEURLS", + "value": 7 + }, + { + "name": "ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_NO_UPDATE_MANIFEST", + "value": 8 + }, + { + "name": "ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_BAD_UPDATE_MANIFEST", + "value": 9 + }, + { + "name": "ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_UNSUPPORTED_UPDATE_MANIFEST_VERSION", + "value": 10 + }, + { + "name": "ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_MISSING_DETACHED_UPDATE_MANIFEST_ENTITY", + "value": 11 + }, + { + "name": "ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_BAD_DETACHED_UPDATE_MANIFEST", + "value": 12 + }, + { + "name": "ADUC_ERC_UTILITIES_UPDATE_DATA_PARSER_DETACHED_UPDATE_MANIFEST_DOWNLOAD_FAILED", + "value": 13 + } + ] + }, + { + "code": 4, + "doc_string": "indicates errors from Workflow Utilities or Function.", + "name": "ADUC_COMPONENT_WORKFLOW_UTIL", + "results": [ + { + "name": "ADUC_ERC_UTILITIES_WORKFLOW_UTIL_ERROR_BAD_PARAM", + "value": 1 + }, + { + "name": "ADUC_ERC_UTILITIES_WORKFLOW_UTIL_ERROR_NO_MEM", + "value": 2 + }, + { + "name": "ADUC_ERC_UTILITIES_WORKFLOW_UTIL_INVALID_ACTION_JSON_STRING", + "value": 3 + }, + { + "name": "ADUC_ERC_UTILITIES_WORKFLOW_UTIL_INVALID_ACTION_JSON_FILE", + "value": 4 + }, + { + "name": "ADUC_ERC_UTILITIES_WORKFLOW_UTIL_INVALID_UPDATE_ID", + "value": 5 + }, + { + "name": "ADUC_ERC_UTILITIES_WORKFLOW_UTIL_COPY_UPDATE_ACTION_FROM_BASE_FAILURE", + "value": 6 + }, + { + "name": "ADUC_ERC_UTILITIES_WORKFLOW_UTIL_COPY_UPDATE_MANIFEST_FROM_BASE_FAILURE", + "value": 7 + }, + { + "name": "ADUC_ERC_UTILITIES_WORKFLOW_UTIL_PARSE_INSTRUCTION_ENTRY_FAILURE", + "value": 8 + }, + { + "name": "ADUC_ERC_UTILITIES_WORKFLOW_UTIL_PARSE_INSTRUCTION_ENTRY_NO_UPDATE_TYPE", + "value": 9 + }, + { + "name": "ADUC_ERC_UTILITIES_WORKFLOW_UTIL_COPY_UPDATE_ACTION_SET_UPDATE_TYPE_FAILURE", + "value": 10 + }, + { + "name": "ADUC_ERC_UTILITIES_WORKFLOW_UTIL_INVALID_STEP_INDEX", + "value": 11 + }, + { + "name": "ADUC_ERC_UTILITIES_WORKFLOW_UTIL_PARSE_STEP_ENTRY_NO_HANDLER_TYPE", + "value": 12 + }, + { + "name": "ADUC_ERC_UTILITIES_WORKFLOW_UTIL_COPY_UPDATE_ACTION_COPY_HANDLER_PROPERTIES_FAILED", + "value": 13 + } + ] + } + ], + "code": 8, + "doc_string": "indicates errors from utility functions.", + "name": "ADUC_FACILITY_UTILITY" + }, + { + "components": [ + { + "code": 0, + "doc_string": "0x00 - 0x07 are reserved for download handler. Indicates errors from extension manager download handler logic.", + "name": "ADUC_COMPONENT_DOWNLOAD_HANDLER_EXTENSION_MANAGER", + "results": [ + { + "name": "ADUC_ERC_DOWNLOAD_HANDLER_EXTENSION_MANAGER_CREATE_FAILURE_CREATE", + "value": 1 + }, + { + "name": "ADUC_ERC_DOWNLOAD_HANDLER_EXTENSION_MANAGER_UNSUPPORTED_CONTRACT_VERSION", + "value": 2 + } + ] + }, + { + "code": 1, + "doc_string": "Indicates errors with usage of download handler plugin shared libraries.", + "name": "ADUC_COMPONENT_DOWNLOAD_HANDLER_PLUGIN", + "results": [ + { + "name": "ADUC_ERC_DOWNLOAD_HANDLER_PLUGIN_ON_UPDATE_WORKFLOW_COMPLETED_FAILURE", + "value": 1 + }, + { + "name": "ADUC_ERC_DOWNLOAD_HANDLER_PLUGIN_MISSING_EXPORT_SYMBOL", + "value": 2 + }, + { + "name": "ADUC_ERC_DOWNLOAD_HANDLER_PLUGIN_EXPORT_CALL_EXCEPTION", + "value": 3 + } + ] + }, + { + "code": 8, + "doc_string": "0x08 - 0x0F are reserved for Delta download handler. Indicates errors in Delta Download Handler extension top-level logic.", + "name": "ADUC_COMPONENT_DELTA_DOWNLOAD_HANDLER_COMMON", + "results": [ + { + "name": "ADUC_ERC_DDH_BAD_ARGS", + "value": 1 + }, + { + "name": "ADUC_ERC_DDH_RELATEDFILE_NO_PROPERTIES", + "value": 2 + }, + { + "name": "ADUC_ERC_DDH_RELATEDFILE_BAD_OR_MISSING_HASH_PROPERTIES", + "value": 3 + }, + { + "name": "ADUC_ERC_DDH_MAKE_DELTA_UPDATE_PATH", + "value": 4 + }, + { + "name": "ADUC_ERC_DDH_PROCESSOR_LOAD_LIB", + "value": 5 + }, + { + "name": "ADUC_ERC_DDH_PROCESSOR_ENSURE_SYMBOLS", + "value": 6 + }, + { + "name": "ADUC_ERC_DDH_PROCESSOR_CREATE_SESSION", + "value": 7 + }, + { + "name": "ADUC_ERC_DDH_SOURCE_UPDATE_CACHE_MISS", + "value": 8 + } + ] + }, + { + "code": 9, + "doc_string": "Indicates errors in Delta Download handler extension Source Update Cache.", + "name": "ADUC_COMPONENT_DELTA_DOWNLOAD_HANDLER_SOURCE_UPDATE_CACHE", + "results": [ + { + "name": "ADUC_ERC_MOVE_PREPURGE", + "value": 1 + }, + { + "name": "ADUC_ERC_MOVE_PAYLOAD", + "value": 2 + }, + { + "name": "ADUC_ERC_MOVE_POSTPURGE", + "value": 3 + }, + { + "name": "ADUC_ERC_MOVE_CREATE_CACHE_PATH", + "value": 4 + }, + { + "name": "ADUC_ERC_MOVE_COPYFALLBACK", + "value": 5 + }, + { + "name": "ADUC_ERC_LOOKUP_CREATE_PATH", + "value": 6 + }, + { + "name": "ADUC_ERC_MISSING_SOURCE_SANDBOX_FILE", + "value": 7 + } + ] + }, + { + "code": 10, + "doc_string": "error code for errors from delta processor API", + "name": "ADUC_COMPONENT_DELTA_DOWNLOAD_HANDLER_DELTA_PROCESSOR", + "results": [] + } + ], + "code": 9, + "doc_string": "Indicates errors from download handler infrastructure or download handler extensions.", + "name": "ADUC_FACILITY_DOWNLOAD_HANDLER" + }, + { + "components": [], + "code": 10, + "doc_string": "unused", + "name": "ADUC_FACILITY_UNUSED_A" + }, + { + "components": [], + "code": 11, + "doc_string": "unused", + "name": "ADUC_FACILITY_UNUSED_B" + }, + { + "components": [], + "code": 12, + "doc_string": "unused", + "name": "ADUC_FACILITY_UNUSED_C" + }, + { + "components": [], + "code": 13, + "doc_string": "indicates errors from Delivery Optimization. DO returns HRESULT-like code. The HResult is transformed into our custom format.", + "name": "ADUC_FACILITY_DELIVERY_OPTIMIZATION" + }, + { + "components": [], + "code": 14, + "doc_string": "unused", + "name": "ADUC_FACILITY_UNUSED_E" + }, + { + "components": [], + "code": 15, + "doc_string": "unused", + "name": "ADUC_FACILITY_UNUSED_F" + } + ] + } diff --git a/scripts/generate_error_code_defs.sh b/scripts/generate_error_code_defs.sh new file mode 100755 index 000000000..c6a92a1cd --- /dev/null +++ b/scripts/generate_error_code_defs.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Ensure that getopt starts from first option if ". " was used. +OPTIND=1 + +# Ensure we dont end the user's terminal session if invoked from source ("."). +if [[ $0 != "${BASH_SOURCE[0]}" ]]; then + ret='return' +else + ret='exit' +fi + +error() { echo -e "\033[1;31mError:\033[0m $*" >&2; } + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null 2>&1 && pwd)" + +result_generator_dir_path=$script_dir/error_code_generator_defs + +json_file_path="" +result_file_path="" +print_help=false +skip_clang_format=false + +print_help() { + python3 "$result_generator_dir_path/error_code_defs_generator.py" -h +} + +while [[ $1 != "" ]]; do + case $1 in + -j | --json-file-path) + shift + json_file_path=$1 + ;; + -r | --result-file-path) + shift + result_file_path=$1 + ;; + -h | --help) + shift + print_help=true + ;; + -s | --skip-clang-format) + shift + skip_clang_format=true + ;; + *) + error "Invalid argument: $1" + $ret 1 + ;; + esac + shift +done + +if [[ $print_help == "true" ]]; then + print_help + $ret 0 +fi + +if [ "$json_file_path" == "" ]; then + error "Must pass a file path to the json file defining the result codes" + print_help + $ret 1 +fi + +if [ "$result_file_path" == "" ]; then + error "Must pass directory to which the error code define file must be passed" + print_help + $ret 1 +fi + +# Make sure that result_h_generator.py is executable +chmod u+x "$result_generator_dir_path/error_code_defs_generator.py" + +python3 "$result_generator_dir_path/error_code_defs_generator.py" -j "$json_file_path" -r "$result_file_path" + +python_ret=$? + +if [[ $python_ret != 0 ]]; then + # error "Failed to generate error code definition file" + $python_ret 1 +fi + +if [ $skip_clang_format == "false" ]; then + clang-format -verbose -style=file -i "$result_file_path" +fi + +$ret 0 diff --git a/scripts/githooks/pre-commit.sh b/scripts/githooks/pre-commit.sh new file mode 100755 index 000000000..4c693ec09 --- /dev/null +++ b/scripts/githooks/pre-commit.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# Ensure we dont end the user's terminal session if invoked from source ("."). +if [[ $0 != "${BASH_SOURCE[0]}" ]]; then + ret='return' +else + ret='exit' +fi + +if ! [ -x "$(command -v git)" ]; then + echo 'Error: git is not installed.' >&2 + $ret 1 +fi + +if ! GITROOT="$(git rev-parse --show-toplevel 2> /dev/null)"; then + echo "'git rev-parse' failed with $?" + $ret 1 +fi + +if [ -z "$GITROOT" ]; then + echo 'Unable to determine git root.' >&2 + $ret 1 +fi + +# Redirect output to stderr. +exec 1>&2 + +# Run shell format first in case below scripts have issues +if ! "$GITROOT/scripts/sh-format.sh" -v; then + $ret 1 +fi + +if ! "$GITROOT/scripts/clang-format.sh"; then + $ret 1 +fi + +# Unfortunately, cmake-format cannot take in piped file contents, so +# not using it for pre-commit hook currently. +# if ! "$GITROOT/scripts/cmake-format.sh"; then +# $ret 1 +# fi diff --git a/scripts/install-deps.sh b/scripts/install-deps.sh old mode 100755 new mode 100644 index c5f65f97c..7741eb857 --- a/scripts/install-deps.sh +++ b/scripts/install-deps.sh @@ -13,9 +13,9 @@ OPTIND=1 # Ensure we dont end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi # Use sudo if user is not root @@ -44,18 +44,33 @@ install_aduc_deps=false install_azure_iot_sdk=false azure_sdk_ref=LTS_07_2021_Ref01 +# ADUC Test Deps + install_catch2=false default_catch2_ref=v2.11.0 catch2_ref=$default_catch2_ref +install_swupdate=false +default_swupdate_ref=2021.11 +swupdate_ref=$default_swupdate_ref install_azure_blob_storage_file_upload_utility=false azure_blob_storage_file_upload_utility_ref=main -install_cmake_from_source=false -install_cmake_version=3.10.2 +install_cmake=false +supported_cmake_version='3.23.2' +install_cmake_version="$supported_cmake_version" +cmake_force_source=false +cmake_prefix="$work_folder" +cmake_installer_dir="" +cmake_dir_symlink="${work_folder}/deviceupdate-cmake" + +install_shellcheck=false +supported_shellcheck_version='0.8.0' + +install_githooks=false # DO Deps -default_do_ref=v0.8.2 +default_do_ref=v1.0.0 install_do=false do_ref=$default_do_ref @@ -63,16 +78,19 @@ do_ref=$default_do_ref aduc_packages=('git' 'make' 'build-essential' 'cmake' 'ninja-build' 'libcurl4-openssl-dev' 'libssl-dev' 'uuid-dev' 'python2.7' 'lsb-release' 'curl' 'wget' 'pkg-config') static_analysis_packages=('clang' 'clang-tidy' 'cppcheck') compiler_packages=("gcc-[68]") -do_packages=('libproxy-dev' 'libssl-dev' 'zlib1g-dev' 'libboost-all-dev') +do_packages=('libproxy-dev' 'libssl-dev' 'libboost-all-dev') -# Distro info +# Distro and arch info OS="" VER="" +is_amd64=false +is_arm64=false +is_arm32=false print_help() { echo "Usage: install-deps.sh [options...]" echo "-a, --install-all-deps Install all dependencies." - echo " Implies --install-aduc-deps, --install-do, --install-do-deps, and --install-packages." + echo " Implies --install-aduc-deps, --install-do, --install-do-deps, --install-packages, --install-cmake, and --install-shellcheck." echo " Can be used with --install-packages-only." echo " This is the default if no options are specified." echo "" @@ -85,10 +103,20 @@ print_help() { echo "--install-abs-file-upload-utility Install the Azure Blob Storage File Upload Utility from source." echo "--abs-file-upload-utility-ref Install the Azure Blob Storage File Upload Utility from a specific branch or tag." echo "--install-catch2 Install Catch2 from source." + echo "--install-cmake Installs supported version of cmake from installer if on ubuntu, else installs it from source." + echo "--install-shellcheck Installs supported version of shellcheck." + echo "--cmake-prefix Set the install path prefix when --install-cmake is used. Default is /tmp." + echo "--cmake-version Override the version of CMake. e.g. 3.23.2 that will be installed if --install-cmake is used." + echo "--cmake-force-source Force building cmake from source when --install-cmake is used." + echo "--install-githooks Install githooks required by the repository." echo "--catch2-ref Install Catch2 from a specific branch or tag." echo " This value is passed to git clone as the --branch argument." echo " Default is $default_catch2_ref." echo "" + echo "--install-swupdate Build and install the SWUpdate project. (required for SWUpdate unit tests on Ubuntu)" + echo "--swupdate-ref Clone the SWUpdate project from a specific branch or tag." + echo " Default is $default_swupdate_ref." + echo "" echo "--install-do Install Delivery Optimization from source." echo " In order to install the correct dependencies, " echo "--do-ref Install the DO source from this branch or tag." @@ -111,6 +139,22 @@ print_help() { echo "Example: ${BASH_SOURCE[0]} --install-all-deps --work-folder ~/adu-linux-client-deps --keep-source-code" } +do_install_githooks() { + echo "Installing githooks..." + + GITROOT="$(git rev-parse --show-toplevel 2> /dev/null)" + + if [ -z "$GITROOT" ]; then + echo 'Unable to determine git root.' >&2 + $ret 1 + fi + + if ! $SUDO ln -sf "$GITROOT/scripts/githooks/pre-commit.sh" "$GITROOT/.git/hooks/pre-commit"; then + echo "Unable to symlink pre-commit. exit code: $?" + $ret 1 + fi +} + do_install_aduc_packages() { echo "Installing dependency packages for ADU Agent..." @@ -135,10 +179,6 @@ do_install_aduc_packages() { # Note that clang-tidy requires clang to be installed so that it can find clang headers. $SUDO apt-get install --yes "${static_analysis_packages[@]}" || return - - if [[ $install_cmake_from_source ]]; then - do_install_cmake_from_source - fi } do_install_azure_iot_sdk() { @@ -160,18 +200,19 @@ do_install_azure_iot_sdk() { echo -e "Building azure-iot-sdk-c ...\n\tBranch: $azure_sdk_ref\n\tFolder: $azure_sdk_dir" mkdir -p $azure_sdk_dir || return - pushd $azure_sdk_dir > /dev/null + pushd $azure_sdk_dir > /dev/null || return git clone --branch $azure_sdk_ref $azure_sdk_url . || return git submodule update --init || return mkdir cmake || return - pushd cmake > /dev/null + pushd cmake > /dev/null || return # use_http is required for uHTTP support. local azureiotsdkc_cmake_options=( "-Duse_amqp:BOOL=OFF" "-Duse_http:BOOL=ON" "-Duse_mqtt:BOOL=ON" + "-Duse_wsio:BOOL=ON" "-Dskip_samples:BOOL=ON" "-Dbuild_service_client:BOOL=OFF" "-Dbuild_provisioning_service_client:BOOL=OFF" @@ -189,8 +230,8 @@ do_install_azure_iot_sdk() { cmake --build . || return $SUDO cmake --build . --target install || return - popd > /dev/null - popd > /dev/null + popd > /dev/null || return + popd > /dev/null || return if [[ $keep_source_code != "true" ]]; then $SUDO rm -rf $azure_sdk_dir || return @@ -217,26 +258,177 @@ do_install_catch2() { echo -e "Building Catch2 ...\n\tBranch: $catch2_ref\n\tFolder: $catch2_dir" mkdir -p $catch2_dir || return - pushd $catch2_dir > /dev/null + pushd $catch2_dir > /dev/null || return git clone --recursive --single-branch --branch $catch2_ref --depth 1 $catch2_url . || return mkdir cmake || return - pushd cmake > /dev/null + pushd cmake > /dev/null || return cmake .. || return cmake --build . || return $SUDO cmake --build . --target install || return - popd > /dev/null - popd > /dev/null + popd > /dev/null || return + popd > /dev/null || return if [[ $keep_source_code != "true" ]]; then $SUDO rm -rf $catch2_dir fi } +do_install_swupdate() { + echo "Installing SWupdate ($swupdate_ref) ..." + + # Currently only support building SWUpdate on following distros: + # - Ubuntu 18.04 + # - Ubuntu 20.04 + lsb_release -a | grep -e 'Ubuntu 18.04' -e 'Ubuntu 20.04' + local grep_res=$? + if [[ $grep_res -ne "0" ]]; then + echo "Only need to build SWUpdate for Ubuntu 18.04 and Ubuntu 20.04. Skipping..." + return 0 + fi + + local swupdate_dir=$work_folder/swupdate + + if [[ $keep_source_code != "true" ]]; then + $SUDO rm -rf $swupdate_dir || return + elif [[ -d $swupdate_dir ]]; then + warn "$swupdate_dir already exists! Skipping SWUpdate." + return 0 + fi + + local swupdate_url + if [[ $use_ssh == "true" ]]; then + swupdate_url=git@github.com:sbabic/swupdate.git + else + swupdate_url=https://github.com/sbabic/swupdate.git + fi + + echo -e "Building SWUpdate ...\n\tBranch: $swupdate_ref\n\tFolder: $swupdate_dir" + mkdir -p $swupdate_dir || return + pushd $swupdate_dir > /dev/null || return + git clone --recursive --single-branch --branch $swupdate_ref --depth 1 $swupdate_url . || return + + popd > /dev/null || return + echo -e "Customizing SWUpdate build configurations..." + cp src/deps/swupdate/.config "$swupdate_dir" || return + pushd $swupdate_dir > /dev/null || return + + echo -r "Building SWUpdate..." + make || return + + echo -e "Installing SWUpdate..." + $SUDO make install || return + popd > /dev/null || return + + if [[ $keep_source_code != "true" ]]; then + $SUDO rm -rf $swupdate_dir + fi +} + +do_install_do_release_tarball() { + local ret=0 + local dist='' + local arch='' + local do_release_tarball_url='' + local tarball_filename='' + local do_dir="$work_folder/do" + + echo -e "Attempting to install libdeliveryoptimization from release tarball...\n" + + local os_lowercase="${OS,,}" + + echo "os_lowercase => $os_lowercase" + echo "VER => $VER" + echo "is_arm32 => $is_arm32" + echo "is_arm64 => $is_arm64" + echo "is_amd64 => $is_amd64" + + if [[ $os_lowercase == "debian" && $VER == "9" ]]; then + echo "evaluating debian9 for supported DO tarball ..." + dist='debian9' + if [[ $is_arm32 == "true" ]]; then + arch='arm32' + else + warn "unsupported arch for DO release asset on Debian9. Supported: arm32" + return 1 + fi + elif [[ $os_lowercase == "debian" && $VER == "10" ]]; then + echo "evaluating debian10 for supported DO tarball ..." + dist='debian10' + if [[ $is_amd64 == "true" ]]; then + arch='x64' + elif [[ $is_arm64 == "true" ]]; then + arch='arm64' + elif [[ $is_arm32 == "true" ]]; then + arch='arm32' + else + warn "unsupported arch for DO release asset on Debian10. Supported: amd64 arm64 arm32" + return 1 + fi + elif [[ $os_lowercase == "ubuntu" ]]; then + echo "evaluating ubuntu for supported DO tarball ..." + + if [[ $VER == "18.04" ]]; then + dist='ubuntu1804' + elif [[ $VER == "20.04" ]]; then + dist='ubuntu2004' + else + warn "unsupported ubuntu version: $VER. Supported: 18.04 20.04" + return 1 + fi + + if [[ $dist != '' ]]; then + if [[ $is_amd64 == "true" ]]; then + arch='x64' + elif [[ $is_arm64 == "true" ]]; then + arch='arm64' + else + warn "unsupported arch for DO release asset on ${dist}. Supported: amd64 arm64" + return 1 + fi + fi + fi + + if [[ $dist != '' && $arch != '' ]]; then + tarball_filename="${dist}_${arch}-packages.tar" + do_release_tarball_url="https://github.com/microsoft/do-client/releases/download/${do_ref}/${tarball_filename}" + + if [[ ! -e $do_dir ]]; then + echo "creating $do_dir dir..." + mkdir -p "$do_dir" || return + fi + + # v0.9.0 DO has libboost-filesystem and libboost-system deps + echo "Installing libboost-filesystem-dev and libboost-system-dev DO deps for ${dist} ..." + $SUDO apt-get install -y libboost-filesystem-dev libboost-system-dev || return + + echo "wget DO release tarball from $do_release_tarball_url ..." + wget -P "$do_dir" "${do_release_tarball_url}" || return + + echo "extracting $tarball_filename tarball ..." + pushd "$do_dir" || return + ls -latr || return + tar -xvf "$tarball_filename" || return + + echo "apt-get installing DO .deb ..." + $SUDO apt-get install -y ./deliveryoptimization-agent_*.deb ./libdeliveryoptimization_*.deb ./libdeliveryoptimization-dev*.deb || return + popd || return + fi + + return 0 +} + do_install_do() { echo "Installing DO ..." local do_dir=$work_folder/do + if [[ $install_packages == "true" || $install_packages_only == "true" ]]; then + if do_install_do_release_tarball; then + echo "Install libdeliveryoptimization from tarball succeeded!" + return 0 + fi + fi + if [[ $keep_source_code != "true" ]]; then $SUDO rm -rf $do_dir || return elif [[ -d $do_dir ]]; then @@ -246,7 +438,7 @@ do_install_do() { echo -e "Building DO ...\n\tBranch: $do_ref\n\tFolder: $do_dir" mkdir -p $do_dir || return - pushd $do_dir > /dev/null + pushd $do_dir > /dev/null || return local do_url if [[ $use_ssh == "true" ]]; then @@ -258,7 +450,7 @@ do_install_do() { git clone --recursive --single-branch --branch $do_ref --depth 1 $do_url . || return distro=$OS$VER - install_do_deps_distro="${distro//.}" + install_do_deps_distro="${distro//./}" if [[ $install_do_deps_distro != "" ]]; then local bootstrap_file=$do_dir/build/scripts/bootstrap.sh @@ -267,7 +459,7 @@ do_install_do() { fi mkdir cmake || return - pushd cmake > /dev/null + pushd cmake > /dev/null || return local do_cmake_options=( "-DDO_BUILD_TESTS:BOOL=OFF" @@ -283,8 +475,8 @@ do_install_do() { cmake "${do_cmake_options[@]}" .. || return cmake --build . || return $SUDO cmake --build . --target install || return - popd > /dev/null - popd > /dev/null + popd > /dev/null || return + popd > /dev/null || return if [[ $keep_source_code != "true" ]]; then $SUDO rm -rf $do_dir @@ -311,7 +503,7 @@ do_install_azure_blob_storage_file_upload_utility() { echo -e "Cloning Azure Blob Storage File Upload Uility ...\n\tBranch: $azure_blob_storage_file_upload_utility_ref\n\t Folder: $abs_fuu_dir" mkdir -p $abs_fuu_dir || return - pushd $abs_fuu_dir > /dev/null + pushd $abs_fuu_dir > /dev/null || return git clone --recursive --single-branch --branch $azure_blob_storage_file_upload_utility_ref --depth 1 $azure_storage_cpplite_url . || return echo -e "Installing Azure Blob Storage File Upload Utiltiy dependencies..." @@ -323,7 +515,7 @@ do_install_azure_blob_storage_file_upload_utility() { ./scripts/install-deps.sh -a --skip-azure-iot-sdk-install mkdir cmake || return - pushd cmake > /dev/null + pushd cmake > /dev/null || return local azure_blob_storage_file_upload_utility_cmake_options if [[ $keep_source_code == "true" ]]; then @@ -339,8 +531,8 @@ do_install_azure_blob_storage_file_upload_utility() { cmake --build . || return $SUDO cmake --build . --target install || return - popd > /dev/null - popd > /dev/null + popd > /dev/null || return + popd > /dev/null || return if [[ $keep_source_code != "true" ]]; then $SUDO rm -rf $abs_fuu_dir || return @@ -348,50 +540,178 @@ do_install_azure_blob_storage_file_upload_utility() { } do_install_cmake_from_source() { - local cmake_url + local ret_value + local cmake_src_url local cmake_tar_path local cmake_dir_path + local maj_min_ver= + + if [[ $install_cmake_version != "$supported_cmake_version" ]]; then + warn "Using unsupported cmake version ${install_cmake_version}!" + fi + + echo "Building CMake ${install_cmake_version} from source ..." + + local tarball_name="cmake-${install_cmake_version}" + local tarball_filename="cmake-${install_cmake_version}.tar.gz" + maj_min_ver=$(echo "$install_cmake_version" | sed -E 's#([0-9]+\.[0-9]+)\.[0-9]+#\1#g') # e.g. 3.23.2 => 3.23 + + cmake_src_url="https://cmake.org/files/v${maj_min_ver}/${tarball_filename}" + cmake_tar_path="$work_folder/${tarball_filename}" + cmake_dir_path="$work_folder/${tarball_name}" - if [[ $install_cmake_version == "3.10.2" ]]; then - cmake_url="https://cmake.org/files/v3.10/cmake-3.10.2.tar.gz" - cmake_tar_path="$work_folder/cmake-3.10.2.tar.gz" - cmake_dir_path="$work_folder/cmake-3.10.2/" + mkdir -p "$cmake_dir_path" + ret_value=$? + if [ $ret_value -ne 0 ]; then + error "Failed to make dir '${cmake_dir_path}' with exit code: ${ret_value}" + return $ret_value + fi + + echo "Fetching source tarball '$cmake_src_url' -> '$work_folder' ..." + wget -P "$work_folder" "$cmake_src_url" > "$cmake_dir_path/wget.log" 2>&1 + ret_value=$? + if [ $ret_value -ne 0 ]; then + error "wget of ${cmake_src_url} failed with exit code ${ret_value}" + return $ret_value + fi + + echo "Expanding source tarball '$cmake_tar_path' ..." + tar -xzvf "$cmake_tar_path" -C "$work_folder" > "$cmake_dir_path/tar.log" 2>&1 || return + + pushd "$cmake_dir_path" > /dev/null || return + + echo "Running 'bootstrap' ..." + $SUDO ./bootstrap --verbose --no-qt-gui --prefix=${cmake_prefix} > "${cmake_dir_path}/bootstrap.log" 2>&1 + ret_value=$? + if [ $ret_value -ne 0 ]; then + error "bootstrap --prefix=${cmake_prefix} failed with exit code ${ret_value}" + return $ret_value + fi - wget -P $work_folder ${cmake_url} - tar -zxvf ${cmake_tar_path} -C ${work_folder} + echo "Running 'make' ..." + $SUDO make > "$cmake_dir_path/make.log" 2>&1 || return - pushd $cmake_dir_path > /dev/null + popd > /dev/null || return - ./bootstrap - make > /dev/null - popd > /dev/null - elif [[ $install_cmake_version == "3.19.8" ]]; then - # Later it will be added when the cmake linking error is solved - # Task 38390989: Move to latest CMake at least 3.19+ to get Ubuntu 20.04+ working - error "Currently CMake 3.13+ is not supported. It will be added in June 2022. " + $SUDO ln -sf "${cmake_prefix}/${tarball_name}" "$cmake_dir_symlink" +} + +do_install_cmake_from_installer() { + local arch="$1" + shift + + local ret_value + + if [[ $install_cmake_version != "$supported_cmake_version" ]]; then + warn "Using unsupported cmake version ${install_cmake_version}!" + fi + + local cmake_installer_sh="cmake-${install_cmake_version}-linux-${arch}.sh" + local cmake_installer_url="https://github.com/Kitware/CMake/releases/download/v${install_cmake_version}/${cmake_installer_sh}" + + $SUDO wget -P "$work_folder" "$cmake_installer_url" + ret_value=$? + if [ $ret_value -ne 0 ]; then + error "wget failed with exit code ${ret_value}" + return $ret_value + fi + + local fullpath_cmake_installer_sh="${work_folder}/${cmake_installer_sh}" + $SUDO chmod u+x "${fullpath_cmake_installer_sh}" + $SUDO "${fullpath_cmake_installer_sh}" --include-subdir --skip-license --prefix=${cmake_prefix} + ret_value=$? + if [ $ret_value -ne 0 ]; then + error "${fullpath_cmake_installer_sh} failed with exit code ${ret_value}" + return $ret_value + fi + + $SUDO ln -sf "$cmake_installer_dir" "$cmake_dir_symlink" +} + +do_install_shellcheck() { + local shellcheck_version='' + if [ -x "${work_folder}/deviceupdate-shellcheck" ]; then + shellcheck_version=$("${work_folder}/deviceupdate-shellcheck" --version | grep -i -e '^version:' | awk '{ print $2 }') + fi + + if [[ $shellcheck_version == "$supported_shellcheck_version" ]]; then + echo "${work_folder}/deviceupdate-shellcheck at version ${supported_cmake_version} already exists. Skipping install..." + return 0 + fi + + local arch='' + if [[ $is_arm64 == "true" ]]; then + arch='aarch64' + elif [[ $is_amd64 == "true" ]]; then + arch='x86_64' + fi + + local base_url='https://github.com/koalaman/shellcheck' + local scver="$supported_shellcheck_version" + if [[ $arch == '' ]]; then + echo "Building shellcheck ${scver} from source..." + $SUDO apt install --yes cabal-install || return 1 + cabal update || return 1 + + local tarball_filename="v${scver}.tar.gz" + wget -P "$work_folder" "${base_url}/archive/refs/tags/${tarball_filename}" || return 1 + tar -xzvf "$work_folder/$tarball_filename" -C "$work_folder" || return 1 + + pushd "${work_folder}/shellcheck-${scver}" > /dev/null || return 1 + cabal install + local ret_val=$? + popd || return 1 + + if [[ $ret_val != 0 ]]; then + return $ret_val + fi + + ln -sf "${HOME}/.cabal/bin/shellcheck" "${work_folder}/deviceupdate-shellcheck" || return 1 else - echo "Not a supported cmake version" + echo "Installing shellcheck ${scver} from pre-built binaries..." + local tar_filename="shellcheck-v${scver}.linux.x86_64.tar.xz" + wget -P "$work_folder" "${base_url}/releases/download/v${scver}/${tar_filename}" || return 1 + tar -xvf "$work_folder/$tar_filename" -C "$work_folder" || return 1 + ln -sf "${work_folder}/shellcheck-v0.8.0/shellcheck" "${work_folder}/deviceupdate-shellcheck" || return 1 fi } -determine_distro() { - # shellcheck disable=SC1091 +determine_machine_architecture() { + local arch='' + arch="$(uname -m)" + local ret_val=$? + if [[ $ret_val != 0 ]]; then + error "Failed to get cpu architecture." + return 1 + else + if [[ $arch == aarch64* || $arch == armv8* ]]; then + is_arm64=true + elif [[ $arch == armv7* || $arch == 'arm' ]]; then + is_arm32=true + elif [[ $arch == 'x86_64' || $arch == 'amd64' ]]; then + is_amd64=true + else + error "Machine architecture '$arch' is not supported." + return 1 + fi + fi +} +determine_distro_and_arch() { # Checking distro name and version if [ -r /etc/os-release ]; then # freedesktop.org and systemd OS=$(grep "^ID\s*=\s*" /etc/os-release | sed -e "s/^ID\s*=\s*//") VER=$(grep "^VERSION_ID=" /etc/os-release | sed -e "s/^VERSION_ID=//") - VER=$(sed -e 's/^"//' -e 's/"$//' <<<"$VER") - elif type lsb_release >/dev/null 2>&1; then + VER=$(sed -e 's/^"//' -e 's/"$//' <<< "$VER") + elif type lsb_release > /dev/null 2>&1; then # linuxbase.org OS=$(lsb_release -si) VER=$(lsb_release -sr) elif [ -f /etc/lsb-release ]; then # For some versions of Debian/Ubuntu without lsb_release command - . /etc/lsb-release - OS=$DISTRIB_ID - VER=$DISTRIB_RELEASE + OS=$(grep DISTRIB_ID /etc/lsb-release | awk -F'=' '{ print $2 }') + VER=$(grep DISTRIB_RELEASE /etc/lsb-release | awk -F'=' '{ print $2 }') elif [ -f /etc/debian_version ]; then # Older Debian/Ubuntu/etc. OS=Debian @@ -401,16 +721,19 @@ determine_distro() { OS=$(uname -s) VER=$(uname -r) fi - # Covert OS to lowercase + + # Convert OS to lowercase OS="$(echo "$OS" | tr '[:upper:]' '[:lower:]')" + + determine_machine_architecture || return 1 } do_list_all_deps() { declare -a deps_set=() - deps_set+=(${aduc_packages[@]}) - deps_set+=(${compiler_packages[@]}) - deps_set+=(${static_analysis_packages[@]}) - deps_set+=(${do_packages[@]}) + deps_set+=("${aduc_packages[@]}") + deps_set+=("${compiler_packages[@]}") + deps_set+=("${static_analysis_packages[@]}") + deps_set+=("${do_packages[@]}") echo "Listing the state of dependencies:" dpkg-query -W -f='${binary:Package} ${Version} (${Architecture})\n' "${deps_set[@]}" ret_val=$? @@ -432,12 +755,6 @@ if [[ $1 == "" ]]; then $ret 1 fi -determine_distro - -if [[ $OS != "ubuntu" || $VER != "18.04" ]]; then - install_cmake_from_source=true -fi - # Parse cmd options while [[ $1 != "" ]]; do case $1 in @@ -465,10 +782,37 @@ while [[ $1 != "" ]]; do --install-catch2) install_catch2=true ;; + --install-cmake) + install_cmake=true + ;; + --cmake-prefix) + shift + cmake_prefix=$1 + ;; + --cmake-version) + shift + install_cmake_version=$1 + ;; + --cmake-force-source) + cmake_force_source=true + ;; + --install-shellcheck) + install_shellcheck=true + ;; + --install-githooks) + install_githooks=true + ;; --catch2-ref) shift catch2_ref=$1 ;; + --install-swupdate) + install_swupdate=true + ;; + --swupdate-ref) + shift + swupdate_ref=$1 + ;; --install-do) install_do=true ;; @@ -508,9 +852,16 @@ while [[ $1 != "" ]]; do shift done +# Get OS, VER, machine architecture for use in other parts of the script. +determine_distro_and_arch + # If there is no install action specified, # assume that we want to install all deps. -if [[ $install_all_deps != "true" && $install_aduc_deps != "true" && $install_do != "true" && $install_azure_iot_sdk != "true" && $install_catch2 != "true" ]]; then +if [[ $install_all_deps != "true" && $install_aduc_deps != "true" && \ + $install_do != "true" && $install_azure_iot_sdk != "true" && \ + $install_catch2 != "true" && $install_swupdate != "true" && \ + $install_cmake != "true" && $install_shellcheck != "true" && \ + $install_githooks != "true" ]]; then install_all_deps=true fi @@ -519,7 +870,9 @@ fi if [[ $install_all_deps == "true" ]]; then install_aduc_deps=true install_do=true - install_packages=true + install_cmake=true + install_shellcheck=true + install_githooks=true fi # Set implied options for aduc deps. @@ -549,6 +902,56 @@ if [[ $install_aduc_deps == "true" ]]; then do_install_aduc_packages || $ret fi +# Must be of the form X.Y.Z, where X, Y, and Z are one or more decimal digits. +if [[ $install_cmake_version != "" && ! $install_cmake_version =~ ^[[:digit:]]+.[[:digit:]]+\.[[:digit:]]+ ]]; then + error "Invalid --cmake-version '${install_cmake_version}'. Valid pattern: digit+.digit+.digit+ e.g. '3.23.2'" + $ret 1 +fi + +# First off, install cmake if requested. +if [[ $install_cmake == "true" ]]; then + if [[ $is_amd64 == "false" && $is_arm64 == "false" || $cmake_force_source == "true" ]]; then + if ! do_install_cmake_from_source; then + error "Failed to install cmake from source." + $ret 1 + fi + else + arch='' + if [[ $is_amd64 == "true" ]]; then + arch='x86_64' + elif [[ $is_arm64 == "true" ]]; then + arch='aarch64' + else + error "Machine architecture is not supported for downloading CMake installer." + $ret 1 + fi + cmake_installer_dir="${cmake_prefix}/cmake-${install_cmake_version}-linux-${arch}" + + if [[ -d $cmake_installer_dir && -x "${cmake_dir_symlink}/bin/cmake" ]]; then + echo "${cmake_installer_dir} already exists. Skipping install of cmake..." + else + if ! do_install_cmake_from_installer "$arch"; then + error "Failed to install cmake using installer." + $ret 1 + fi + fi + fi +fi + +# Install git hooks if requested. +if [[ $install_githooks == "true" ]]; then + if ! do_install_githooks; then + warn "Failed to install git hooks." + fi +fi + +# Install shellcheck if requested. +if [[ $install_shellcheck == "true" ]]; then + if ! do_install_shellcheck; then + warn "Failed to install shellcheck." + fi +fi + # Install dependencies from source if [[ $install_packages_only == "false" ]]; then if [[ $install_azure_iot_sdk == "true" ]]; then @@ -559,6 +962,10 @@ if [[ $install_packages_only == "false" ]]; then do_install_catch2 || $ret fi + if [[ $install_swupdate == "true" ]]; then + do_install_swupdate || $ret + fi + if [[ $install_do == "true" ]]; then do_install_do || $ret fi diff --git a/scripts/sh-format.sh b/scripts/sh-format.sh index 0ab3d0473..1ea392f9e 100755 --- a/scripts/sh-format.sh +++ b/scripts/sh-format.sh @@ -8,15 +8,47 @@ OPTIND=1 # Ensure we dont end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi -error() { echo -e "\033[1;31mError:\033[0m $*" >&2; } - validate=false +# use readline -e to resolve symlink +shellcheck_bin=$(readlink -e '/tmp/deviceupdate-shellcheck') +shfmt_bin='/tmp/shfmt' + +is_arm32=false +is_amd64=false + +# Attempts to determine the machine cpu architecture by pattern matching the +# output of uname -m +# Returns 0 on success and sets the following variables as side-effect: +# is_arm32, is_amd64 +determine_machine_architecture() { + local arch='' + arch="$(uname -m)" + local ret_val=$? + if [[ $ret_val != 0 ]]; then + error "Failed to get cpu architecture." + return 1 + else + if [[ $arch == armv7* || $arch == 'arm' ]]; then + is_arm32=true + elif [[ $arch == 'x86_64' || $arch == 'amd64' ]]; then + is_amd64=true + else + error "Machine architecture '$arch' is not supported." + return 1 + fi + fi +} + +warn() { echo -e "\033[1;33mWarning:\033[0m $*" >&2; } + +error() { echo -e "\033[1;31mError:\033[0m $*" >&2; } + print_help() { echo "Usage: sh-format.sh [options...]" echo "-v, --validate Validates shell scripts as well as format." @@ -40,42 +72,96 @@ while [[ $1 != "" ]]; do shift done -if ! [ -x "$(command -v git)" ]; then - error 'git is not installed. Try: apt install git' - $ret 1 +if [[ ! -e $shfmt_bin || ! -x $shfmt_bin ]]; then + # Do not use snap because on ubuntu 20.04 it is a version that has trouble with finding .editorconfig but reported as permissions issue + warn 'shfmt is not installed. Trying to install...' + + if ! determine_machine_architecture; then + $ret 1 + fi + + SHFMT_GIT_BASE_URL="https://github.com/patrickvane/shfmt/releases/download/master" + + # shfmt releases _arm is arm32 as per readelf -h -A and there is no arm64 + arch_suffix='' + if [[ $is_arm32 == "true" ]]; then + arch_suffix='arm' + elif [[ $is_amd64 == "true" ]]; then + arch_suffix='amd64' + else + warn "not arm32 or amd64. Try: wget ${SHFMT_GIT_BASE_URL}/shfmt_linux_{ARCH}" + warn ' chmod 755 shfmt_linux_{ARCH} && ln -s /path/to/shfmt_linux_{ARCH} /somewhere/in/your/PATH/shfmt' + warn ' OR last resort Try: snap install shfmt' + $ret 1 + fi + + if [[ $arch_suffix != "" ]]; then + release_asset="shfmt_linux_${arch_suffix}" + release_asset_url="${SHFMT_GIT_BASE_URL}/${release_asset}" + + wget -P /tmp "$release_asset_url" || $ret 1 + chmod u+rwx,go+rx "/tmp/${release_asset}" || $ret 1 + ln -sf "/tmp/${release_asset}" "$shfmt_bin" || $ret 1 + fi fi -if ! [ -x "$(command -v shfmt)" ]; then - error 'shfmt is not installed. Try: snap install shfmt' +if ! [ -x "$(command -v git)" ]; then + error 'git is not installed. Try: apt install git' $ret 1 fi GITROOT="$(git rev-parse --show-toplevel 2> /dev/null)" - if [ -z "$GITROOT" ]; then error 'Unable to determine git root.' $ret 1 fi -pushd "$GITROOT" > /dev/null -# diff-filter=d will exclude deleted files. +pushd "$GITROOT" > /dev/null || $ret +# diff-filter=d will exclude deleted files. Use --cached to only focus on +# staged files to avoid bypass of pre-commit hook using unstaged working set. IFS=$'\n' -shell_files=$(git diff --diff-filter=d --relative --name-only HEAD -- "*.sh" "*.bash" "*.mksh") +shell_files=$(git diff --diff-filter=d --relative --name-only --cached HEAD -- "*.sh" "*.bash" "*.mksh") +shfmt_failed=false for FILE in $shell_files; do echo "Formatting $FILE" - shfmt -s -w -i 4 -bn -sr "$FILE" + + # Use git cat-file blob to get the staged contents only for a dry-run to test if a format would occur. + # -w cannot be used when piping contents into shfmt. + resolved_shfmt_bin=$(readlink -e $shfmt_bin) + if ! git cat-file blob :"$FILE" 2>&1 | $resolved_shfmt_bin -d -s -i 4 -bn -sr; then + shfmt_failed=true + + # use -w now to actually in-place write the reformatting. + $resolved_shfmt_bin -d -s -w -i 4 -bn -sr "$FILE" + fi done +if [[ $shfmt_failed == "true" ]]; then + error 'shfmt failed or reformatted a file. Fix failures and/or git add the unstaged changes before commit' + $ret 1 +fi + if [[ $validate == "true" ]]; then - if ! [ -x "$(command -v shellcheck)" ]; then - echo 'Error: shellcheck is not installed. Try: apt install shellcheck' >&2 + if [[ ! -x $shellcheck_bin ]]; then + error "'$shellcheck_bin' is not installed or executable. Try: ./scripts/install-deps.sh --install-shellcheck" $ret 1 fi + shellcheck_validation_failed=false for FILE in $shell_files; do echo "Checking $FILE" - shellcheck -s bash "$FILE" + if ! $shellcheck_bin --shell=bash --severity=style "$FILE"; then + shellcheck_validation_failed=true + # continue on to next file + fi done + + if [[ $shellcheck_validation_failed == "true" ]]; then + error "shellcheck failed. Fix the issue(s) and try again." + $ret 1 + fi fi IFS=' ' -popd > /dev/null +popd > /dev/null || $ret + +$ret 0 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c5adfaa42..1963e1010 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,5 +1,9 @@ cmake_minimum_required (VERSION 3.5) +if (ADUC_TRACE_TARGET_DEPS) + set_property (GLOBAL PROPERTY GLOBAL_DEPENDS_DEBUG_MODE 1) +endif () + if (ADUC_WARNINGS_AS_ERRORS) set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror") set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror") @@ -24,7 +28,6 @@ add_subdirectory (adu-shell) add_subdirectory (adu_types) add_subdirectory (adu_workflow) add_subdirectory (communication_abstraction) -add_subdirectory (content_handlers) add_subdirectory (diagnostics_component) if (ADUC_BUILD_DOCUMENTATION) @@ -36,3 +39,4 @@ add_subdirectory (platform_layers) add_subdirectory (utils) add_subdirectory (extensions) add_subdirectory (agent) +add_subdirectory (agent_orchestration) diff --git a/src/adu-shell/scripts/adu-swupdate.sh b/src/adu-shell/scripts/adu-swupdate.sh index e2b4069ab..5ea0ac47b 100644 --- a/src/adu-shell/scripts/adu-swupdate.sh +++ b/src/adu-shell/scripts/adu-swupdate.sh @@ -8,9 +8,9 @@ OPTIND=1 # Ensure we dont end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi print_help() { diff --git a/src/adu-shell/src/main.cpp b/src/adu-shell/src/main.cpp index 43cfab0db..460585f35 100644 --- a/src/adu-shell/src/main.cpp +++ b/src/adu-shell/src/main.cpp @@ -31,7 +31,7 @@ namespace AptGetTasks = Adu::Shell::Tasks::AptGet; #endif #ifdef ADUSHELL_SCRIPT -# include "script_tasks.hpp" +# include "script_tasks.hpp" namespace ScriptTasks = Adu::Shell::Tasks::Script; #endif @@ -222,6 +222,7 @@ void ShowChildProcessLogs(const std::string& output) Log_Info("# %s", token.c_str()); } Log_Info("########## End Child's Logs ##########"); + Log_RequestFlush(); } } diff --git a/src/adu-shell/src/swupdate_tasks.cpp b/src/adu-shell/src/swupdate_tasks.cpp index 470a65e14..64024c701 100644 --- a/src/adu-shell/src/swupdate_tasks.cpp +++ b/src/adu-shell/src/swupdate_tasks.cpp @@ -36,7 +36,7 @@ ADUShellTaskResult Install(const ADUShell_LaunchArguments& launchArgs) ADUShellTaskResult taskResult; // Constructing parameter for child process. - // command << c_installScript << " -l " << _logFolder << " -i '" << _workFolder << "/" << filename << "'"; + // command << SWUpdateCommand << " -l " << _logFolder << " -i '" << _workFolder << "/" << filename << "'"; Log_Info("Installing image. Path: %s, Log folder: %s", launchArgs.targetData, launchArgs.logFile); @@ -65,7 +65,7 @@ ADUShellTaskResult Apply(const ADUShell_LaunchArguments& launchArgs) ADUShellTaskResult taskResult; // Constructing parameter for child process. - // This is equivalent to : command << c_installScript << " -l " << _logFolder << " -a" + // This is equivalent to : command << SWUpdateCommand << " -l " << _logFolder << " -a" std::vector args; @@ -103,7 +103,7 @@ ADUShellTaskResult Cancel(const ADUShell_LaunchArguments& launchArgs) ADUShellTaskResult taskResult; // Constructing parameter for child process. - // command << c_installScript << " -l " << logFolder << " -r" + // command << SWUpdateCommand << " -l " << logFolder << " -r" std::vector args; if (launchArgs.logFile != nullptr) diff --git a/src/adu_types/CMakeLists.txt b/src/adu_types/CMakeLists.txt index 899e647c6..fa7b9e7f9 100644 --- a/src/adu_types/CMakeLists.txt +++ b/src/adu_types/CMakeLists.txt @@ -13,6 +13,6 @@ find_package (Parson REQUIRED) # set_property (TARGET ${target_name} PROPERTY POSITION_INDEPENDENT_CODE ON) -target_include_directories (${target_name} PUBLIC inc) +target_include_directories (${target_name} PUBLIC inc ${ADUC_EXPORT_INCLUDES}) -target_link_libraries (${target_name} PUBLIC aduc::logging aduc::c_utils Parson::parson) +target_link_libraries (${target_name} PUBLIC aduc::c_utils aduc::logging Parson::parson) diff --git a/src/adu_types/inc/aduc/adu_types.h b/src/adu_types/inc/aduc/adu_types.h index c8d30afc4..485bb1dbd 100644 --- a/src/adu_types/inc/aduc/adu_types.h +++ b/src/adu_types/inc/aduc/adu_types.h @@ -15,22 +15,34 @@ #include "aduc/logging.h" EXTERN_C_BEGIN + +typedef enum tagADUC_ExtensionRegistrationType +{ + ExtensionRegistrationType_None, + ExtensionRegistrationType_UpdateContentHandler, + ExtensionRegistrationType_ContentDownloadHandler, + ExtensionRegistrationType_ComponentEnumerator, + ExtensionRegistrationType_DownloadHandler, +} ADUC_ExtensionRegistrationType; + /** * @brief ADU Client launch arguments. */ typedef struct tagADUC_LaunchArguments { - int argc; /**< Size of argv */ char** argv; /**< Command-line arguments */ - ADUC_LOG_SEVERITY logLevel; /**< Log level */ + char* ipcCommand; /**< an inter-process command to be insert into a command queue. */ char* connectionString; /**< Device connection string from command-line. */ - bool iotHubTracingEnabled; /**< Whether to enable logging from IoT Hub SDK */ + char* contentDownloaderFilePath; /**< A full path of a content downloader to be registered. */ + char* extensionFilePath; /**< The path to the extension shared library file. */ + char* extensionId; /**< The extension id for an extension registration type like + downloadHandlerId for download handlers and updateType for content handlers. */ + int argc; /**< Size of argv */ + ADUC_LOG_SEVERITY logLevel; /**< Log level */ + ADUC_ExtensionRegistrationType extensionRegistrationType; /**< The type of extension being registered. */ + bool iotHubTracingEnabled; /**< Whether to enable logging from IoT Hub SDK. */ bool showVersion; /**< Show an agent version */ bool healthCheckOnly; /**< Only check agent health. Doesn't process any data or messages from services. */ - char* contentHandlerFilePath; /**< A full path of an update content handler to be registered */ - char* componentEnumeratorFilePath; /**< A full path of a component enumerator to be registered */ - char* contentDownloaderFilePath; /**< A full path of a content downloader to be registered */ - char* updateType; } ADUC_LaunchArguments; typedef enum tagADUC_ConnType @@ -74,6 +86,18 @@ void ADUC_ConnectionInfo_DeAlloc(ADUC_ConnectionInfo* info); */ const char* ADUC_ConnType_ToString(const ADUC_ConnType connType); +/** + * @brief Struct containing information about the IoT Hub device/model client PnP property update notification. + * + */ +typedef struct tagADUC_PnPComponentClient_PropertyUpdate_Context +{ + _Bool clientInitiated; /** Indicates that the property update notification was caused by a client request. + For example, when the agent call IoTHub_DeviceClient_LL_GetTwinAsync API. + Note: this value should be set to null when calling ClientHandle_SetClientTwinCallback. */ + _Bool forceUpdate; /** In indicates whether the force process the update. */ +} ADUC_PnPComponentClient_PropertyUpdate_Context; + EXTERN_C_END #endif // ADUC_ADU_TYPES_H diff --git a/src/adu_types/inc/aduc/types/adu_core.h b/src/adu_types/inc/aduc/types/adu_core.h index 0566ebd87..0688acc46 100644 --- a/src/adu_types/inc/aduc/types/adu_core.h +++ b/src/adu_types/inc/aduc/types/adu_core.h @@ -65,9 +65,23 @@ typedef void (*IdleCallbackFunc)(ADUC_Token token, const char* workflowId); * @param workflowData Data about what to download. */ typedef ADUC_Result (*DownloadCallbackFunc)( - ADUC_Token token, - const ADUC_WorkCompletionData* workCompletionData, - ADUC_WorkflowDataToken workflowData); + ADUC_Token token, const ADUC_WorkCompletionData* workCompletionData, ADUC_WorkflowDataToken workflowData); + +// +// Backup callback. +// + +/** + * @brief Callback method to do backup. + * + * Must not block! + * + * @param token Opaque token. + * @param workCompletionData Method and value to call when work completes if *Result_InProgress returned. + * @param workflowData Data about what to backup. + */ +typedef ADUC_Result (*BackupCallbackFunc)( + ADUC_Token token, const ADUC_WorkCompletionData* workCompletionData, ADUC_WorkflowDataToken workflowData); // // Install callback. @@ -83,9 +97,7 @@ typedef ADUC_Result (*DownloadCallbackFunc)( * @param workflowData Data about what to install. */ typedef ADUC_Result (*InstallCallbackFunc)( - ADUC_Token token, - const ADUC_WorkCompletionData* workCompletionData, - ADUC_WorkflowDataToken workflowData); + ADUC_Token token, const ADUC_WorkCompletionData* workCompletionData, ADUC_WorkflowDataToken workflowData); // Apply callback. @@ -99,9 +111,23 @@ typedef ADUC_Result (*InstallCallbackFunc)( * @param info Data about what to apply. */ typedef ADUC_Result (*ApplyCallbackFunc)( - ADUC_Token Token, - const ADUC_WorkCompletionData* workCompletionData, - ADUC_WorkflowDataToken workflowData); + ADUC_Token Token, const ADUC_WorkCompletionData* workCompletionData, ADUC_WorkflowDataToken workflowData); + +// +// Restore callback. +// + +/** + * @brief Callback method to do restore. + * + * Must not block! + * + * @param token Opaque token. + * @param workCompletionData Method and value to call when work completes if *Result_InProgress returned. + * @param workflowData Data about what to restore. + */ +typedef ADUC_Result (*RestoreCallbackFunc)( + ADUC_Token token, const ADUC_WorkCompletionData* workCompletionData, ADUC_WorkflowDataToken workflowData); // Cancel callback. @@ -162,8 +188,10 @@ typedef struct tagADUC_UpdateActionCallbacks IdleCallbackFunc IdleCallback; /**< Idle state handler. */ DownloadCallbackFunc DownloadCallback; /**< Download message handler. */ + BackupCallbackFunc BackupCallback; /**< Backup message handler. */ InstallCallbackFunc InstallCallback; /**< Install message handler. */ ApplyCallbackFunc ApplyCallback; /**< Apply message handler. */ + RestoreCallbackFunc RestoreCallback; /**< Restore message handler. */ CancelCallbackFunc CancelCallback; /**< Cancel message handler. */ IsInstalledCallbackFunc IsInstalledCallback; /**< IsInstalled function pointer */ @@ -186,6 +214,7 @@ typedef enum tagADUC_ResultCode // Success codes. ADUC_Result_Success = 1, /**< General success. */ + ADUC_Result_Success_Cache_Miss = 2, /**< General success when cache miss. */ ADUC_Result_Register_Success = 100, /**< Succeeded. */ @@ -197,29 +226,32 @@ typedef enum tagADUC_ResultCode ADUC_Result_Download_Success = 500, /**< Succeeded. */ ADUC_Result_Download_InProgress = 501, /**< Async operation started. CompletionCallback will be called when complete. */ - ADUC_Result_Download_Skipped_FileExists = 502, /**< Download skipped. File already exsits and hash validation passed. */ + ADUC_Result_Download_Skipped_FileExists = 502, /**< Download skipped. File already exists and hash validation passed. */ ADUC_Result_Download_Skipped_UpdateAlreadyInstalled = 503, /**< Download succeeded. Also indicates that the Installed Criteria is met. */ ADUC_Result_Download_Skipped_NoMatchingComponents = 504, /**< Download succeeded. Also indicates that no matchings components for this update. */ + ADUC_Result_Download_Handler_SuccessSkipDownload = 520, /**< Succeeded. DownloadHandler was able to produce the update. Agent must skip downloading. */ + ADUC_Result_Download_Handler_RequiredFullDownload = 521, /**< Not a failure. Agent fallback to downloading the update is required. */ + ADUC_Result_Install_Success = 600, /**< Succeeded. */ ADUC_Result_Install_InProgress = 601, /**< Async operation started. CompletionCallback will be called when complete. */ ADUC_Result_Install_Skipped_UpdateAlreadyInstalled = 603, /**< Install succeeded. Also indicates that the Installed Criteria is met. */ ADUC_Result_Install_Skipped_NoMatchingComponents = 604, /**< Install succeeded. Also indicates that no matchings components for this update. */ - ADUC_Result_Install_RequiredImmediateReboot = 605, /**< Succeeded. An immidiate device reboot is required, to complete the task. */ + ADUC_Result_Install_RequiredImmediateReboot = 605, /**< Succeeded. An immediate device reboot is required, to complete the task. */ ADUC_Result_Install_RequiredReboot = 606, /**< Succeeded. A deferred device reboot is required, to complete the task. */ - ADUC_Result_Install_RequiredImmediateAgentRestart = 607, /**< Succeeded. An immediate agent restart is requied, to complete the task. */ - ADUC_Result_Install_RequiredAgentRestart = 608, /**< Succeeded. A deferred agent restart is requied, to complete the task. */ + ADUC_Result_Install_RequiredImmediateAgentRestart = 607, /**< Succeeded. An immediate agent restart is required, to complete the task. */ + ADUC_Result_Install_RequiredAgentRestart = 608, /**< Succeeded. A deferred agent restart is required, to complete the task. */ ADUC_Result_Apply_Success = 700, /**< Succeeded. */ ADUC_Result_Apply_InProgress = 701, /**< Async operation started. CompletionCallback will be called when complete. */ - ADUC_Result_Apply_RequiredImmediateReboot = 705, /**< Succeeded. An immidiate device reboot is required, to complete the task. */ + ADUC_Result_Apply_RequiredImmediateReboot = 705, /**< Succeeded. An immediate device reboot is required, to complete the task. */ ADUC_Result_Apply_RequiredReboot = 706, /**< Succeeded. A deferred device reboot is required, to complete the task. */ - ADUC_Result_Apply_RequiredImmediateAgentRestart = 707, /**< Succeeded. An immediate agent restart is requied, to complete the task. */ - ADUC_Result_Apply_RequiredAgentRestart = 708, /**< Succeeded. A deferred agent restart is requied, to complete the task. */ + ADUC_Result_Apply_RequiredImmediateAgentRestart = 707, /**< Succeeded. An immediate agent restart is required, to complete the task. */ + ADUC_Result_Apply_RequiredAgentRestart = 708, /**< Succeeded. A deferred agent restart is required, to complete the task. */ ADUC_Result_Cancel_Success = 800, /**< Succeeded. */ ADUC_Result_Cancel_UnableToCancel = 801, /**< Not a failure. Cancel is best effort. */ @@ -227,12 +259,24 @@ typedef enum tagADUC_ResultCode ADUC_Result_IsInstalled_Installed = 900, /**< Succeeded and content is installed. */ ADUC_Result_IsInstalled_NotInstalled = 901, /**< Succeeded and content is not installed */ + ADUC_Result_Backup_Success = 1000, /**< Succeeded. */ + ADUC_Result_Backup_Success_Unsupported = 1001, /**< Succeeded to proceed with the workflow, but the action is not implemented/supported in the content handler. */ + ADUC_Result_Backup_InProgress = 1002, /**< Async operation started. CompletionCallback will be called when complete. */ + + ADUC_Result_Restore_Success = 1100, /**< Succeeded. */ + ADUC_Result_Restore_Success_Unsupported = 1101, /**< Succeeded to proceed with the workflow, but the action is not implemented/supported in the content handler. */ + ADUC_Result_Restore_InProgress = 1102, /**< Async operation started. CompletionCallback will be called when complete. */ + + ADUC_Result_Restore_RequiredImmediateReboot = 1105, /**< Succeeded. An immediate device reboot is required, to complete the task. */ + ADUC_Result_Restore_RequiredReboot = 1106, /**< Succeeded. A deferred device reboot is required, to complete the task. */ + ADUC_Result_Restore_RequiredImmediateAgentRestart = 1107, /**< Succeeded. An immediate agent restart is required, to complete the task. */ + ADUC_Result_Restore_RequiredAgentRestart = 1108, /**< Succeeded. A deferred agent restart is required, to complete the task. */ } ADUC_ResultCode; -#define AducResultCodeIndicatesInProgress(resultCode) \ - ((resultCode) == ADUC_Result_Download_InProgress || \ - (resultCode) == ADUC_Result_Install_InProgress || \ - (resultCode) == ADUC_Result_Apply_InProgress) +#define AducResultCodeIndicatesInProgress(resultCode) \ + ((resultCode) == ADUC_Result_Download_InProgress || (resultCode) == ADUC_Result_Backup_InProgress || \ + (resultCode) == ADUC_Result_Install_InProgress || (resultCode) == ADUC_Result_Apply_InProgress || \ + (resultCode) == ADUC_Result_Restore_InProgress) // clang-format on diff --git a/src/adu_types/inc/aduc/types/update_content.h b/src/adu_types/inc/aduc/types/update_content.h index 83e71d6ed..a15a46296 100644 --- a/src/adu_types/inc/aduc/types/update_content.h +++ b/src/adu_types/inc/aduc/types/update_content.h @@ -114,6 +114,11 @@ EXTERN_C_BEGIN */ #define ADUCITF_FIELDNAME_DEVICEPROPERTIES_INTERFACEID "interfaceId" +/** + * @brief JSON field name for DeviceProperties contractModelId + */ +#define ADUCITF_FIELDNAME_DEVICEPROPERTIES_CONTRACT_MODEL_ID "contractModelId" + /** * @brief JSON field name for DeviceProperties aduc version */ @@ -197,6 +202,21 @@ EXTERN_C_BEGIN */ #define ADUCITF_FIELDNAME_ARGUMENTS "arguments" +/** + * @brief JSON field name for the updateManifest's file entity's relatedFiles + */ +#define ADUCITF_FIELDNAME_RELATEDFILES "relatedFiles" + +/** + * @brief JSON field name for the updateManifest's file entity's downloadHandler + */ +#define ADUCITF_FIELDNAME_DOWNLOADHANDLER "downloadHandler" + +/** + * @brief JSON field name for the updateManifest's file entity's downloadHandler id + */ +#define ADUCITF_FIELDNAME_DOWNLOADHANDLER_ID "id" + // // UpdateAction // @@ -235,8 +255,10 @@ typedef enum tagADUCITF_WorkflowStep ADUCITF_WorkflowStep_Undefined = 0, ///< The undefined worfklow step. ADUCITF_WorkflowStep_ProcessDeployment = 1, ///< Step that reports DeploymentInProgress ACK ADUCITF_WorkflowStep_Download = 2, ///< Step where it starts download operation - ADUCITF_WorkflowStep_Install = 3, ///< Step where it starts install operation - ADUCITF_WorkflowStep_Apply = 4, ///< Step where it starts apply operation + ADUCITF_WorkflowStep_Backup = 3, ///< Step where it starts backup operation + ADUCITF_WorkflowStep_Install = 4, ///< Step where it starts install operation + ADUCITF_WorkflowStep_Apply = 5, ///< Step where it starts apply operation + ADUCITF_WorkflowStep_Restore = 6, ///< Step where it starts restore operation } ADUCITF_WorkflowStep; // @@ -259,6 +281,9 @@ typedef enum tagADUCITF_State ADUCITF_State_InstallSucceeded = 4, ///< The state for when an install operation has completed with success. ADUCITF_State_ApplyStarted = 5, ///< The state for when an apply operation is in progress. ADUCITF_State_DeploymentInProgress = 6, ///< The state for reporting the acknowledgement of ProcessDeployment update action. + ADUCITF_State_BackupStarted = 7, ///< The state for when a backup operation is in progress. + ADUCITF_State_BackupSucceeded = 8, ///< The state for when a backup operation has completed with success. + ADUCITF_State_RestoreStarted = 9, ///< The state for when an restore operation is in progress. ADUCITF_State_Failed = 255, ///< The state for when a deployment has failed and for reporting the failure. } ADUCITF_State; @@ -274,6 +299,30 @@ typedef struct tagADUC_UpdateId char* Version; /**< Version for the update*/ } ADUC_UpdateId; +/** + * @brief Describes a name value pair property. + */ +typedef struct tagProperty +{ + char* Name; /**< Name for the property. */ + char* Value; /**< Value for the property. */ +} ADUC_Property; + +/** + * @brief Describes a related file. + */ +typedef struct tagRelatedFile +{ + char* FileId; /**< Id for the related file. */ + char* DownloadUri; /**< The URI of the related file to download. */ + ADUC_Hash* Hash; /**< Array of ADUC_Hashes containing the hash options for the file. */ + size_t HashCount; /**< Total number of hashes in the array of hashes. */ + char* FileName; /**< The file name of the related file. */ + size_t SizeInBytes; /**< The file size. */ + ADUC_Property* Properties; /**< The property bag for related file. */ + size_t PropertiesCount; /**< Count of properties in the property bag. */ +} ADUC_RelatedFile; + /** * @brief Describes a specific file to download. */ @@ -286,6 +335,9 @@ typedef struct tagADUC_FileEntity char* TargetFilename; /**< File name to store content in DownloadUri to. */ char* Arguments; //**< Arguments associate with this file. */ size_t SizeInBytes; /**< File size. */ + ADUC_RelatedFile* RelatedFiles; /**< The related files for this update payload. */ + size_t RelatedFileCount; /**< The count of related files. */ + char* DownloadHandlerId; /**< The identifier for the download handler extensibility point. */ } ADUC_FileEntity; /** @@ -301,16 +353,6 @@ typedef struct tagADUC_FileUrl // ADUC_UpdateId Helper Functions // -/** - * @brief Allocates and sets the UpdateId fields - * @param provider the provider for the UpdateId - * @param name the name for the UpdateId - * @param version the version for the UpdateId - * - * @returns An UpdateId on success, NULL on failure - */ -ADUC_UpdateId* ADUC_UpdateId_AllocAndInit(const char* provider, const char* name, const char* version); - /** * @brief Checks if the UpdateId is valid * @param updateId updateId to check diff --git a/src/adu_types/inc/aduc/types/workflow.h b/src/adu_types/inc/aduc/types/workflow.h index f830fadb0..905374ff0 100644 --- a/src/adu_types/inc/aduc/types/workflow.h +++ b/src/adu_types/inc/aduc/types/workflow.h @@ -16,6 +16,7 @@ #include "aduc/types/download.h" #include "aduc/types/hash.h" #include "aduc/types/update_content.h" +#include "aduc/types/workflow.h" #include "parson.h" @@ -58,68 +59,18 @@ typedef enum tagADUC_SystemRebootState */ typedef enum tagADUC_WorkflowCancellationType { - ADUC_WorkflowCancellationType_None = 0, /**< No cancellation. */ - ADUC_WorkflowCancellationType_Normal = 1, /**< A normal cancel due to a Cancel update action from the cloud. */ - ADUC_WorkflowCancellationType_Replacement = 2, /**< A cancel due to a process deployment update action from the cloud for a workflow with a different workflow id . */ - ADUC_WorkflowCancellationType_Retry = 3, /**< A cancel due to a process deployment update action from the cloud for the same workflow id but with a new retry timestamp token. */ + ADUC_WorkflowCancellationType_None = 0, /**< No cancellation. */ + ADUC_WorkflowCancellationType_Normal = 1, /**< A normal cancel due to a Cancel update action from the cloud. */ + ADUC_WorkflowCancellationType_Replacement = + 2, /**< A cancel due to a process deployment update action from the cloud for a workflow with a different workflow id . */ + ADUC_WorkflowCancellationType_Retry = + 3, /**< A cancel due to a process deployment update action from the cloud for the same workflow id but with a new retry timestamp token. */ ADUC_WorkflowCancellationType_ComponentChanged = 4, /**< A cancel due to a components changed event. */ } ADUC_WorkflowCancellationType; -/** - * @brief Function signature for callback to send download progress to. - */ -typedef void (*ADUC_Core_DownloadFunction)( - const char* workflowId, - const char* fileId, - ADUC_DownloadProgressState state, - uint64_t bytesTransferred, - uint64_t bytesTotal); - // Forward decl. struct tagADUC_WorkflowData; -typedef ADUC_Result (*ADUC_Core_Download_Function)( - const ADUC_FileEntity* entity, - const char* workflowId, - const char* workFolder, - ADUC_DownloadProgressCallback downloadProgressCallback); - -typedef ADUC_Result (*ADUC_Set_Workflow_Result_Function)( - const char* workflowId, ADUC_Result result, _Bool reportToCloud, _Bool persistLocally); - -typedef void (*ADUC_WorkflowData_Free_Function)(struct tagADUC_WorkflowData* workflowData); - -typedef void (*HandleUpdateActionFunc)(struct tagADUC_WorkflowData* workflowData); -typedef void (*SetUpdateStateWithResultFunc)(struct tagADUC_WorkflowData* workflowData, ADUCITF_State updateState, ADUC_Result result); -typedef void (*WorkCompletionCallbackFunc)(const void* workCompletionToken, ADUC_Result result, _Bool isAsync); -typedef int (*RebootSystemFunc)(); -typedef int (*RestartAgentFunc)(); - -typedef void* ADUC_CLIENT_HANDLE_TYPE; -typedef void (*IOTHUB_CLIENT_REPORTED_STATE_CALLBACK_TYPE)(int, void*); -typedef enum ADUC_IOTHUB_CLIENT_RESULT_TAG { - ADUC_IOTHUB_CLIENT_OK, - ADUC_IOTHUB_CLIENT_INVALID_ARG, - ADUC_IOTHUB_CLIENT_ERROR, - ADUC_IOTHUB_CLIENT_INVALID_SIZE, - ADUC_IOTHUB_CLIENT_INDEFINITE_TIME -} IOTHUB_CLIENT_RESULT_TYPE; -typedef IOTHUB_CLIENT_RESULT_TYPE (ClientHandleSendReportType)(ADUC_CLIENT_HANDLE_TYPE, const unsigned char *, size_t, IOTHUB_CLIENT_REPORTED_STATE_CALLBACK_TYPE, void *); -typedef ClientHandleSendReportType* ClientHandleSendReportFunc; - -#ifdef ADUC_BUILD_UNIT_TESTS -typedef struct tagADUC_TestOverride_Hooks -{ - void* ContentHandler_TestOverride; - HandleUpdateActionFunc HandleUpdateActionFunc_TestOverride; - SetUpdateStateWithResultFunc SetUpdateStateWithResultFunc_TestOverride; - WorkCompletionCallbackFunc WorkCompletionCallbackFunc_TestOverride; - RebootSystemFunc RebootSystemFunc_TestOverride; - RestartAgentFunc RestartAgentFunc_TestOverride; - void* ClientHandle_SendReportedStateFunc_TestOverride; -} ADUC_TestOverride_Hooks; -#endif // #ifdef ADUC_BUILD_UNIT_TESTS - /** * @brief A callback function for reporting state, and optionally result to service. * @@ -154,7 +105,8 @@ typedef struct tagADUC_WorkflowData // Workflow states // ADUC_Result Result; /**< Current workflow result data. */ - ADUCITF_State LastReportedState; /**< Last state set for the workflow and may have been reported as per agent orchestration. */ + ADUCITF_State + LastReportedState; /**< Last state set for the workflow and may have been reported as per agent orchestration. */ char* LastCompletedWorkflowId; /**< Last workflow id for deployment that completed successfully. */ ADUC_UpdateActionCallbacks UpdateActionCallbacks; /**< Upper-level registration data; function pointers, etc. */ @@ -171,7 +123,8 @@ typedef struct tagADUC_WorkflowData ADUC_DownloadProgressCallback DownloadProgressCallback; /**< Callback for download progress. */ - ADUC_ReportStateAndResultAsyncCallback ReportStateAndResultAsyncCallback; /**< Callback for reporting workflow state and result. */ + ADUC_ReportStateAndResultAsyncCallback + ReportStateAndResultAsyncCallback; /**< Callback for reporting workflow state and result. */ /** * @brief Results object @@ -233,12 +186,6 @@ typedef struct tagADUC_WorkflowData */ JSON_Array* Results; - char* LastGoalStateJson; /**< The goal state data sent from DU Service to DU Agent. This data is needed when re-processing latest update on the device */ - -#ifdef ADUC_BUILD_UNIT_TESTS - ADUC_TestOverride_Hooks* TestOverrides; /**< Test hook overrides. This will be NULL when not testing. */ -#endif - } ADUC_WorkflowData; #endif // ADUC_TYPES_WORKFLOW_H diff --git a/src/adu_types/src/adu_types.c b/src/adu_types/src/adu_types.c index 5a857c8b0..a5f65bc2a 100644 --- a/src/adu_types/src/adu_types.c +++ b/src/adu_types/src/adu_types.c @@ -73,7 +73,7 @@ _Bool ADUC_IsValidUpdateId(const ADUC_UpdateId* updateId) /** * @brief Free the UpdateId and its content. - * @param updateid a pointer to an updateId struct to be freed + * @param updateId a pointer to an updateId struct to be freed */ void ADUC_UpdateId_UninitAndFree(ADUC_UpdateId* updateId) { @@ -83,68 +83,17 @@ void ADUC_UpdateId_UninitAndFree(ADUC_UpdateId* updateId) } free(updateId->Provider); - free(updateId->Name); - free(updateId->Version); - free(updateId); -} - -/** - * @brief Allocates and sets the UpdateId fields - * @details Caller should free the allocated ADUC_UpdateId* using ADUC_UpdateId_UninitAndFree() - * @param provider the provider for the UpdateId - * @param name the name for the UpdateId - * @param version the version for the UpdateId - * - * @returns An UpdateId on success, NULL on failure - */ -ADUC_UpdateId* ADUC_UpdateId_AllocAndInit(const char* provider, const char* name, const char* version) -{ - _Bool success = false; - - ADUC_UpdateId* updateId = (ADUC_UpdateId*)calloc(1, sizeof(ADUC_UpdateId)); - - if (updateId == NULL) - { - Log_Error("ADUC_UpdateId_AllocAndInit called with a NULL updateId handle"); - goto done; - } - - if (provider == NULL || name == NULL || version == NULL) - { - Log_Error( - "Invalid call to ADUC_UpdateId_AllocAndInit with provider %s name %s version %s", provider, name, version); - goto done; - } + updateId->Provider = NULL; - if (mallocAndStrcpy_s(&(updateId->Provider), provider) != 0) - { - goto done; - } - - if (mallocAndStrcpy_s(&(updateId->Name), name) != 0) - { - goto done; - } - - if (mallocAndStrcpy_s(&(updateId->Version), version) != 0) - { - goto done; - } - - success = true; - -done: + free(updateId->Name); + updateId->Name = NULL; - if (!success) - { - ADUC_UpdateId_UninitAndFree(updateId); - updateId = NULL; - } + free(updateId->Version); + updateId->Version = NULL; - return updateId; + free(updateId); } - /** * @brief Convert UpdateState to string representation. * @@ -163,10 +112,16 @@ const char* ADUCITF_StateToString(ADUCITF_State updateState) return "DownloadStarted"; case ADUCITF_State_DownloadSucceeded: return "DownloadSucceeded"; + case ADUCITF_State_BackupStarted: + return "BackupStarted"; + case ADUCITF_State_BackupSucceeded: + return "BackupSucceeded"; case ADUCITF_State_InstallStarted: return "InstallStarted"; case ADUCITF_State_InstallSucceeded: return "InstallSucceeded"; + case ADUCITF_State_RestoreStarted: + return "RestoreStarted"; case ADUCITF_State_ApplyStarted: return "ApplyStarted"; case ADUCITF_State_DeploymentInProgress: diff --git a/src/adu_workflow/CMakeLists.txt b/src/adu_workflow/CMakeLists.txt index 6e99dea59..8de379171 100644 --- a/src/adu_workflow/CMakeLists.txt +++ b/src/adu_workflow/CMakeLists.txt @@ -4,32 +4,32 @@ include (agentRules) compileasc99 () -add_library ( - ${PROJECT_NAME} STATIC - src/agent_workflow.c) +add_library (${PROJECT_NAME} STATIC "") set_property (TARGET ${PROJECT_NAME} PROPERTY POSITION_INDEPENDENT_CODE ON) add_library (aduc::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) +target_sources (${PROJECT_NAME} PRIVATE src/agent_workflow.c) + target_include_directories (${PROJECT_NAME} PUBLIC inc ${ADUC_EXPORT_INCLUDES}) -target_link_digital_twin_client (${PROJECT_NAME} PUBLIC) find_package (Parson REQUIRED) target_link_libraries ( ${PROJECT_NAME} PUBLIC aduc::adu_types - PRIVATE + PRIVATE aduc::adu_core_export_helpers + aduc::agent_orchestration + aduc::c_utils + aduc::download_handler_factory + aduc::download_handler_plugin aduc::logging aduc::parser_utils + aduc::system_utils aduc::workflow_data_utils aduc::workflow_utils - Parson::parson -zdef) -target_compile_definitions ( - ${PROJECT_NAME} - PRIVATE ADUC_CONF_FILE_PATH="${ADUC_CONF_FILE_PATH}" - ADUC_BUILD_UNIT_TESTS="${ADUC_BUILD_UNIT_TESTS}") +target_compile_definitions (${PROJECT_NAME} PRIVATE ADUC_CONF_FILE_PATH="${ADUC_CONF_FILE_PATH}") diff --git a/src/adu_workflow/inc/aduc/agent_workflow.h b/src/adu_workflow/inc/aduc/agent_workflow.h index ca96dd5d8..6e3e5ddce 100644 --- a/src/adu_workflow/inc/aduc/agent_workflow.h +++ b/src/adu_workflow/inc/aduc/agent_workflow.h @@ -15,9 +15,8 @@ EXTERN_C_BEGIN void ADUC_Workflow_DoWork(ADUC_WorkflowData* workflowData); -void ADUC_Workflow_HandlePropertyUpdate(ADUC_WorkflowData* currentWorkflowData, const unsigned char* propertyUpdateValue, bool forceDeferral); - -void ADUC_Workflow_HandleComponentChanged(ADUC_WorkflowData* workflowData); +void ADUC_Workflow_HandlePropertyUpdate( + ADUC_WorkflowData* currentWorkflowData, const unsigned char* propertyUpdateValue, bool forceUpdate); void ADUC_Workflow_HandleUpdateAction(ADUC_WorkflowData* workflowData); @@ -25,8 +24,6 @@ void ADUC_Workflow_TransitionWorkflow(ADUC_WorkflowData* workflowData); void ADUC_Workflow_HandleStartupWorkflowData(ADUC_WorkflowData* currentWorkflowData); -const char* ADUC_Workflow_CancellationTypeToString(ADUC_WorkflowCancellationType cancellationType); - // // Device Update Action data type and methods. // @@ -44,23 +41,29 @@ void ADUC_Workflow_MethodCall_ProcessDeployment_Complete(ADUC_MethodCall_Data* m ADUC_Result ADUC_Workflow_MethodCall_Download(ADUC_MethodCall_Data* methodCallData); void ADUC_Workflow_MethodCall_Download_Complete(ADUC_MethodCall_Data* methodCallData, ADUC_Result result); +ADUC_Result ADUC_Workflow_MethodCall_Backup(ADUC_MethodCall_Data* methodCallData); +void ADUC_Workflow_MethodCall_Backup_Complete(ADUC_MethodCall_Data* methodCallData, ADUC_Result result); + ADUC_Result ADUC_Workflow_MethodCall_Install(ADUC_MethodCall_Data* methodCallData); void ADUC_Workflow_MethodCall_Install_Complete(ADUC_MethodCall_Data* methodCallData, ADUC_Result result); ADUC_Result ADUC_Workflow_MethodCall_Apply(ADUC_MethodCall_Data* methodCallData); void ADUC_Workflow_MethodCall_Apply_Complete(ADUC_MethodCall_Data* methodCallData, ADUC_Result result); +ADUC_Result ADUC_Workflow_MethodCall_Restore(ADUC_MethodCall_Data* methodCallData); +void ADUC_Workflow_MethodCall_Restore_Complete(ADUC_MethodCall_Data* methodCallData, ADUC_Result result); + void ADUC_Workflow_MethodCall_Cancel(const ADUC_WorkflowData* workflowData); ADUC_Result ADUC_Workflow_MethodCall_IsInstalled(const ADUC_WorkflowData* workflowData); - // // State transition // void ADUC_Workflow_SetUpdateState(ADUC_WorkflowData* workflowData, ADUCITF_State updateState); -void ADUC_Workflow_SetUpdateStateWithResult(ADUC_WorkflowData* workflowData, ADUCITF_State updateState, ADUC_Result result); +void ADUC_Workflow_SetUpdateStateWithResult( + ADUC_WorkflowData* workflowData, ADUCITF_State updateState, ADUC_Result result); void ADUC_Workflow_SetInstalledUpdateIdAndGoToIdle(ADUC_WorkflowData* workflowData, const char* updateId); void ADUC_Workflow_DefaultDownloadProgressCallback( diff --git a/src/adu_workflow/src/agent_workflow.c b/src/adu_workflow/src/agent_workflow.c index 0fbf7cbcb..3b441da64 100644 --- a/src/adu_workflow/src/agent_workflow.c +++ b/src/adu_workflow/src/agent_workflow.c @@ -18,8 +18,12 @@ #include +#include "aduc/adu_core_export_helpers.h" // ADUC_MethodCall_RestartAgent #include "aduc/agent_orchestration.h" +#include "aduc/download_handler_factory.h" // ADUC_DownloadHandlerFactory_LoadDownloadHandler +#include "aduc/download_handler_plugin.h" // ADUC_DownloadHandlerPlugin_OnUpdateWorkflowCompleted #include "aduc/logging.h" +#include "aduc/parser_utils.h" // ADUC_FileEntity_Uninit #include "aduc/result.h" #include "aduc/string_c_utils.h" #include "aduc/system_utils.h" @@ -29,6 +33,9 @@ #include +// fwd decl +void ADUC_Workflow_WorkCompletionCallback(const void* workCompletionToken, ADUC_Result result, _Bool isAsync); + // This lock is used for critical sections where main and worker thread could read/write to ADUC_workflowData // It is used only at the top-level coarse granularity operations: // * (main thread) ADUC_Workflow_HandlePropertyUpdate @@ -46,10 +53,7 @@ static inline void s_workflow_unlock(void) pthread_mutex_unlock(&s_workflow_mutex); } -// fwd decl -void ADUC_Workflow_WorkCompletionCallback(const void* workCompletionToken, ADUC_Result result, _Bool isAsync); - -const char* ADUC_Workflow_CancellationTypeToString(ADUC_WorkflowCancellationType cancellationType) +static const char* ADUC_Workflow_CancellationTypeToString(ADUC_WorkflowCancellationType cancellationType) { switch (cancellationType) { @@ -82,10 +86,14 @@ static const char* ADUCITF_WorkflowStepToString(ADUCITF_WorkflowStep workflowSte return "ProcessDeployment"; case ADUCITF_WorkflowStep_Download: return "Download"; + case ADUCITF_WorkflowStep_Backup: + return "Backup"; case ADUCITF_WorkflowStep_Install: return "Install"; case ADUCITF_WorkflowStep_Apply: return "Apply"; + case ADUCITF_WorkflowStep_Restore: + return "Restore"; case ADUCITF_WorkflowStep_Undefined: return "Undefined"; } @@ -129,12 +137,19 @@ typedef struct tagADUC_WorkflowHandlerMapEntry const ADUC_Workflow_OperationCompleteFunc OperationCompleteFunc; - const ADUCITF_State NextState; /**< State to transition to on successful operation */ + const ADUCITF_State NextStateOnSuccess; /**< State to transition to on successful operation */ - /**< The next workflow step input to transition workflow after transitioning to above NextState when current + /**< The next workflow step input to transition workflow after transitioning to above NextStateOnSuccess when current * workflow step is above WorkflowStep. Using ADUCITF_WorkflowStep_Undefined means it ends the workflow. */ - const ADUCITF_WorkflowStep AutoTransitionWorkflowStep; + const ADUCITF_WorkflowStep AutoTransitionWorkflowStepOnSuccess; + + const ADUCITF_State NextStateOnFailure; /**< State to transition to on failed operation */ + + /**< The next workflow step input to transition workflow after transitioning to above NextStateOnFailure when current + * workflow step is above WorkflowStep. Using ADUCITF_WorkflowStep_Undefined means it ends the workflow. + */ + const ADUCITF_WorkflowStep AutoTransitionWorkflowStepOnFailure; } ADUC_WorkflowHandlerMapEntry; // clang-format off @@ -156,32 +171,63 @@ typedef struct tagADUC_WorkflowHandlerMapEntry */ const ADUC_WorkflowHandlerMapEntry workflowHandlerMap[] = { { ADUCITF_WorkflowStep_ProcessDeployment, - /* calls operation */ ADUC_Workflow_MethodCall_ProcessDeployment, - /* and on completion calls */ ADUC_Workflow_MethodCall_ProcessDeployment_Complete, - /* then on success, transitions to state */ ADUCITF_State_DeploymentInProgress, - /* and then auto-transitions to workflow step */ ADUCITF_WorkflowStep_Download, + /* calls operation */ ADUC_Workflow_MethodCall_ProcessDeployment, + /* and on completion calls */ ADUC_Workflow_MethodCall_ProcessDeployment_Complete, + /* on success, transitions to state */ ADUCITF_State_DeploymentInProgress, + /* on success auto-transitions to workflow step */ ADUCITF_WorkflowStep_Download, + /* on failure, transitions to state */ ADUCITF_State_Failed, + /* on failure auto-transitions to workflow step */ ADUCITF_WorkflowStep_Undefined, }, { ADUCITF_WorkflowStep_Download, - /* calls operation */ ADUC_Workflow_MethodCall_Download, - /* and on completion calls */ ADUC_Workflow_MethodCall_Download_Complete, - /* then on success, transitions to state */ ADUCITF_State_DownloadSucceeded, - /* and then auto-transitions to workflow step */ ADUCITF_WorkflowStep_Install, + /* calls operation */ ADUC_Workflow_MethodCall_Download, + /* and on completion calls */ ADUC_Workflow_MethodCall_Download_Complete, + /* on success, transitions to state */ ADUCITF_State_DownloadSucceeded, + /* on success auto-transitions to workflow step */ ADUCITF_WorkflowStep_Backup, + /* on failure, transitions to state */ ADUCITF_State_Failed, + /* on failure auto-transitions to workflow step */ ADUCITF_WorkflowStep_Undefined, + }, + + { ADUCITF_WorkflowStep_Backup, + /* calls operation */ ADUC_Workflow_MethodCall_Backup, + /* and on completion calls */ ADUC_Workflow_MethodCall_Backup_Complete, + /* on success, transitions to state */ ADUCITF_State_BackupSucceeded, + /* on success auto-transitions to workflow step */ ADUCITF_WorkflowStep_Install, + /* Note: The default behavior of backup is that if Backup fails, + the workflow will end and report failure immediately. + To opt out of this design, in the content handler, the owner of the content handler + will need to persist the result of ADUC_Workflow_MethodCall_Backup and return + ADUC_Result_Backup_Success to let the workflow continue. */ + /* on failure, transitions to state */ ADUCITF_State_Failed, + /* on failure auto-transitions to workflow step */ ADUCITF_WorkflowStep_Undefined, }, { ADUCITF_WorkflowStep_Install, - /* calls operation */ ADUC_Workflow_MethodCall_Install, - /* and on completion calls */ ADUC_Workflow_MethodCall_Install_Complete, - /* then on success, transitions to state */ ADUCITF_State_InstallSucceeded, - /* and then auto-transitions to workflow step */ ADUCITF_WorkflowStep_Apply, + /* calls operation */ ADUC_Workflow_MethodCall_Install, + /* and on completion calls */ ADUC_Workflow_MethodCall_Install_Complete, + /* on success, transitions to state */ ADUCITF_State_InstallSucceeded, + /* on success auto-transitions to workflow step */ ADUCITF_WorkflowStep_Apply, + /* on failure, transitions to state */ ADUCITF_State_Failed, + /* on failure auto-transitions to workflow step */ ADUCITF_WorkflowStep_Restore, }, // Note: There's no "ApplySucceeded" state. On success, we should return to Idle state. { ADUCITF_WorkflowStep_Apply, - /* calls operation */ ADUC_Workflow_MethodCall_Apply, - /* and on completion calls */ ADUC_Workflow_MethodCall_Apply_Complete, - /* then on success, transition to state */ ADUCITF_State_Idle, - /* and then auto-transitions to workflow step */ ADUCITF_WorkflowStep_Undefined, // Undefined means end of workflow + /* calls operation */ ADUC_Workflow_MethodCall_Apply, + /* and on completion calls */ ADUC_Workflow_MethodCall_Apply_Complete, + /* on success, transition to state */ ADUCITF_State_Idle, + /* on success auto-transitions to workflow step */ ADUCITF_WorkflowStep_Undefined, // Undefined means end of workflow + /* on failure, transitions to state */ ADUCITF_State_Failed, + /* on failure auto-transitions to workflow step */ ADUCITF_WorkflowStep_Restore, + }, + + { ADUCITF_WorkflowStep_Restore, + /* calls operation */ ADUC_Workflow_MethodCall_Restore, + /* and on completion calls */ ADUC_Workflow_MethodCall_Restore_Complete, + /* on success, transition to state */ ADUCITF_State_Idle, + /* on success auto-transitions to workflow step */ ADUCITF_WorkflowStep_Undefined, // Undefined means end of workflow + /* on failure, transitions to state */ ADUCITF_State_Failed, + /* on failure auto-transitions to workflow step */ ADUCITF_WorkflowStep_Undefined, }, }; @@ -251,15 +297,6 @@ void ADUC_Workflow_HandleStartupWorkflowData(ADUC_WorkflowData* currentWorkflowD } else { - ADUC_Result isInstalledResult = ADUC_Workflow_MethodCall_IsInstalled(currentWorkflowData); - if (isInstalledResult.ResultCode == ADUC_Result_IsInstalled_Installed) - { - char* updateId = workflow_get_expected_update_id_string(currentWorkflowData->WorkflowHandle); - ADUC_Workflow_SetInstalledUpdateIdAndGoToIdle(currentWorkflowData, updateId); - free(updateId); - goto done; - } - // The default result for Idle state. // This will reset twin status code to 200 to indicate that we're successful (so far). const ADUC_Result result = { .ResultCode = ADUC_Result_Idle_Success }; @@ -276,12 +313,21 @@ void ADUC_Workflow_HandleStartupWorkflowData(ADUC_WorkflowData* currentWorkflowD ADUC_WorkflowData_SetCurrentAction(desiredAction, currentWorkflowData); - SetUpdateStateWithResultFunc setUpdateStateWithResultFunc = - ADUC_WorkflowData_GetSetUpdateStateWithResultFunc(currentWorkflowData); - (*setUpdateStateWithResultFunc)(currentWorkflowData, ADUCITF_State_Idle, result); + ADUC_Workflow_SetUpdateStateWithResult(currentWorkflowData, ADUCITF_State_Idle, result); goto done; } + else if (desiredAction == ADUCITF_UpdateAction_ProcessDeployment) + { + ADUC_Result isInstalledResult = ADUC_Workflow_MethodCall_IsInstalled(currentWorkflowData); + if (isInstalledResult.ResultCode == ADUC_Result_IsInstalled_Installed) + { + char* updateId = workflow_get_expected_update_id_string(currentWorkflowData->WorkflowHandle); + ADUC_Workflow_SetInstalledUpdateIdAndGoToIdle(currentWorkflowData, updateId); + free(updateId); + goto done; + } + } Log_Info("There's a pending '%s' action", ADUCITF_UpdateActionToString(desiredAction)); } @@ -291,8 +337,7 @@ void ADUC_Workflow_HandleStartupWorkflowData(ADUC_WorkflowData* currentWorkflowD // In this case, we will set last reportedState to 'idle', so that we can continue. ADUC_WorkflowData_SetLastReportedState(ADUCITF_State_Idle, currentWorkflowData); - HandleUpdateActionFunc handleUpdateActionFunc = ADUC_WorkflowData_GetHandleUpdateActionFunc(currentWorkflowData); - (*handleUpdateActionFunc)(currentWorkflowData); + ADUC_Workflow_HandleUpdateAction(currentWorkflowData); done: @@ -305,40 +350,16 @@ void ADUC_Workflow_HandleStartupWorkflowData(ADUC_WorkflowData* currentWorkflowD * * @param[in,out] currentWorkflowData The current ADUC_WorkflowData object. * @param[in] propertyUpdateValue The updated property value. - */ -void ADUC_Workflow_HandleComponentChanged(ADUC_WorkflowData* workflowData) -{ - if (workflowData == NULL) - { - Log_Info("Nothing to do due to no workflow data object."); - return; - } - - // Process the latest goal state, if successfully cached. - if (workflowData->LastGoalStateJson != NULL) - { - ADUC_Workflow_HandlePropertyUpdate( - workflowData, (const unsigned char*)workflowData->LastGoalStateJson, true /* forceDeferral */); - } - else - { - Log_Error( - "Component changes is detected, but the update data cache is not available. An update must be trigger by DU service."); - } -} - -/** - * @brief Handles updates to a 1 or more PnP Properties in the ADU Core interface. - * - * @param[in,out] currentWorkflowData The current ADUC_WorkflowData object. - * @param[in] propertyUpdateValue The updated property value. - * @param[in] forceDeferral Ensures that specifed @p propertyUpdateValue will be processed by force deferral if there is ongoing workflow processing. + * @param[in] forceUpdate Ensures that specifed @p propertyUpdateValue will be processed by force deferral if there is ongoing workflow processing. */ void ADUC_Workflow_HandlePropertyUpdate( - ADUC_WorkflowData* currentWorkflowData, const unsigned char* propertyUpdateValue, bool forceDeferral) + ADUC_WorkflowData* currentWorkflowData, const unsigned char* propertyUpdateValue, bool forceUpdate) { ADUC_WorkflowHandle nextWorkflow; - ADUC_Result result = workflow_init((const char*)propertyUpdateValue, true, &nextWorkflow); + + ADUC_Result result = workflow_init((const char*)propertyUpdateValue, true /* shouldValidate */, &nextWorkflow); + + workflow_set_force_update(nextWorkflow, forceUpdate); if (IsAducResultCodeFailure(result.ResultCode)) { @@ -358,8 +379,6 @@ void ADUC_Workflow_HandlePropertyUpdate( // s_workflow_lock(); - HandleUpdateActionFunc handleUpdateActionFunc = ADUC_WorkflowData_GetHandleUpdateActionFunc(currentWorkflowData); - if (currentWorkflowData->WorkflowHandle != NULL) { if (nextUpdateAction == ADUCITF_UpdateAction_Cancel) @@ -372,7 +391,7 @@ void ADUC_Workflow_HandlePropertyUpdate( currentWorkflowData->WorkflowHandle, ADUC_WorkflowCancellationType_Normal); // call into handle update action for cancellation logic to invoke ADUC_Workflow_MethodCall_Cancel - (*handleUpdateActionFunc)(currentWorkflowData); + ADUC_Workflow_HandleUpdateAction(currentWorkflowData); goto done; } @@ -387,7 +406,7 @@ void ADUC_Workflow_HandlePropertyUpdate( } else if (nextUpdateAction == ADUCITF_UpdateAction_ProcessDeployment) { - if (!forceDeferral && workflow_id_compare(currentWorkflowData->WorkflowHandle, nextWorkflow) == 0) + if (!forceUpdate && workflow_id_compare(currentWorkflowData->WorkflowHandle, nextWorkflow) == 0) { // Possible retry of the current workflow. const char* currentRetryToken = workflow_peek_retryTimestamp(currentWorkflowData->WorkflowHandle); @@ -406,7 +425,7 @@ void ADUC_Workflow_HandlePropertyUpdate( workflow_update_retry_deployment(currentWorkflowData->WorkflowHandle, newRetryToken); // call into handle update action for cancellation logic to invoke ADUC_Workflow_MethodCall_Cancel - (*handleUpdateActionFunc)(currentWorkflowData); + ADUC_Workflow_HandleUpdateAction(currentWorkflowData); goto done; } else @@ -442,16 +461,14 @@ void ADUC_Workflow_HandlePropertyUpdate( nextWorkflow = NULL; // call into handle update action for cancellation logic to invoke ADUC_Workflow_MethodCall_Cancel - (*handleUpdateActionFunc)(currentWorkflowData); + ADUC_Workflow_HandleUpdateAction(currentWorkflowData); goto done; } workflow_transfer_data( currentWorkflowData->WorkflowHandle /* wfTarget */, nextWorkflow /* wfSource */); - ADUC_WorkflowData_SaveLastGoalStateJson(currentWorkflowData, (const char*)propertyUpdateValue); - - (*handleUpdateActionFunc)(currentWorkflowData); + ADUC_Workflow_HandleUpdateAction(currentWorkflowData); goto done; } @@ -469,8 +486,6 @@ void ADUC_Workflow_HandlePropertyUpdate( workflow_free(currentWorkflowData->WorkflowHandle); currentWorkflowData->WorkflowHandle = nextWorkflow; - ADUC_WorkflowData_SaveLastGoalStateJson(currentWorkflowData, (const char*)propertyUpdateValue); - nextWorkflow = NULL; workflow_set_cancellation_type( @@ -490,7 +505,7 @@ void ADUC_Workflow_HandlePropertyUpdate( } else { - (*handleUpdateActionFunc)(currentWorkflowData); + ADUC_Workflow_HandleUpdateAction(currentWorkflowData); } done: @@ -539,11 +554,11 @@ void ADUC_Workflow_HandleUpdateAction(ADUC_WorkflowData* workflowData) if (workflow_get_operation_in_progress(workflowData->WorkflowHandle)) { Log_Info( - "Canceling request for in-progress operation. desiredAction: %s, cancelationType: %s", + "Canceling request for in-progress operation. desiredAction: %s, cancellationType: %s", ADUCITF_UpdateActionToString(desiredAction), ADUC_Workflow_CancellationTypeToString(cancellationType)); - // This sets a marker that cancelation has been requested. + // This sets a marker that cancellation has been requested. workflow_set_operation_cancel_requested(workflowData->WorkflowHandle, true); // Call upper-layer to notify of cancel @@ -571,7 +586,8 @@ void ADUC_Workflow_HandleUpdateAction(ADUC_WorkflowData* workflowData) } // Ignore duplicate deployment that can be caused by token expiry connection refresh after about 40 minutes. - if (workflow_isequal_id(workflowData->WorkflowHandle, workflowData->LastCompletedWorkflowId)) + if (workflow_isequal_id(workflowData->WorkflowHandle, workflowData->LastCompletedWorkflowId) + && !workflow_get_force_update(workflowData->WorkflowHandle)) { Log_Debug("Ignoring duplicate deployment %s, action %d", workflowData->LastCompletedWorkflowId, desiredAction); goto done; @@ -617,15 +633,10 @@ void ADUC_Workflow_HandleUpdateAction(ADUC_WorkflowData* workflowData) * It must be in a lock before calling this. * * @param workflowData The global context workflow data structure. + * @param onSuccess Indicate whether it is a transition on success or on failure. */ -void ADUC_Workflow_AutoTransitionWorkflow(ADUC_WorkflowData* workflowData) +void ADUC_Workflow_AutoTransitionWorkflow(ADUC_WorkflowData* workflowData, _Bool onSuccess) { - if (ADUC_WorkflowData_GetLastReportedState(workflowData) == ADUCITF_State_Failed) - { - Log_Debug("Skipping transition for Failed state."); - return; - } - // // If the workflow's not complete, then auto-transition to the next step/phase of the workflow. // For example, Download just completed, so it should auto-transition with workflow step input of WorkflowStep_Install, @@ -641,19 +652,42 @@ void ADUC_Workflow_AutoTransitionWorkflow(ADUC_WorkflowData* workflowData) return; } - if (AgentOrchestration_IsWorkflowComplete(postCompleteEntry->AutoTransitionWorkflowStep)) + if (!onSuccess) { - Log_Info("Workflow is Complete."); + if (AgentOrchestration_IsWorkflowComplete(postCompleteEntry->AutoTransitionWorkflowStepOnFailure)) + { + Log_Info("Workflow is Complete."); + } + else + { + workflow_set_current_workflowstep( + workflowData->WorkflowHandle, postCompleteEntry->AutoTransitionWorkflowStepOnFailure); + + Log_Info( + "workflow is not completed. AutoTransition to step: %s", + ADUCITF_WorkflowStepToString(postCompleteEntry->AutoTransitionWorkflowStepOnFailure)); + + ADUC_Workflow_TransitionWorkflow(workflowData); + } } + else { - workflow_set_current_workflowstep(workflowData->WorkflowHandle, postCompleteEntry->AutoTransitionWorkflowStep); + if (AgentOrchestration_IsWorkflowComplete(postCompleteEntry->AutoTransitionWorkflowStepOnSuccess)) + { + Log_Info("Workflow is Complete."); + } + else + { + workflow_set_current_workflowstep( + workflowData->WorkflowHandle, postCompleteEntry->AutoTransitionWorkflowStepOnSuccess); - Log_Info( - "workflow is not completed. AutoTransition to step: %s", - ADUCITF_WorkflowStepToString(postCompleteEntry->AutoTransitionWorkflowStep)); + Log_Info( + "workflow is not completed. AutoTransition to step: %s", + ADUCITF_WorkflowStepToString(postCompleteEntry->AutoTransitionWorkflowStepOnSuccess)); - ADUC_Workflow_TransitionWorkflow(workflowData); + ADUC_Workflow_TransitionWorkflow(workflowData); + } } } @@ -688,16 +722,7 @@ void ADUC_Workflow_TransitionWorkflow(ADUC_WorkflowData* workflowData) // workCompletionData is sent to the upper-layer which will pass the WorkCompletionToken back // when it makes the async work complete call. - WorkCompletionCallbackFunc workCompletionCallbackFunc = ADUC_Workflow_WorkCompletionCallback; - -#ifdef ADUC_BUILD_UNIT_TESTS - if (workflowData->TestOverrides && workflowData->TestOverrides->WorkCompletionCallbackFunc_TestOverride) - { - workCompletionCallbackFunc = workflowData->TestOverrides->WorkCompletionCallbackFunc_TestOverride; - } -#endif - - methodCallData->WorkCompletionData.WorkCompletionCallback = workCompletionCallbackFunc; + methodCallData->WorkCompletionData.WorkCompletionCallback = ADUC_Workflow_WorkCompletionCallback; methodCallData->WorkCompletionData.WorkCompletionToken = methodCallData; // Call into the upper-layer method to perform operation. @@ -714,7 +739,7 @@ void ADUC_Workflow_TransitionWorkflow(ADUC_WorkflowData* workflowData) if (!AducResultCodeIndicatesInProgress(result.ResultCode) || IsAducResultCodeFailure(result.ResultCode)) { Log_Debug("The synchronous operation is complete."); - (*workCompletionCallbackFunc)(methodCallData, result, false /* isAsync */); + ADUC_Workflow_WorkCompletionCallback(methodCallData, result, false /* isAsync */); } done: @@ -777,14 +802,14 @@ void ADUC_Workflow_WorkCompletionCallback(const void* workCompletionToken, ADUC_ { // Operation succeeded -- go to next state. - const ADUCITF_State nextUpdateState = entry->NextState; + const ADUCITF_State nextUpdateStateOnSuccess = entry->NextStateOnSuccess; Log_Info( "WorkCompletionCallback: %s succeeded. Going to state %s", ADUCITF_WorkflowStepToString(entry->WorkflowStep), - ADUCITF_StateToString(nextUpdateState)); + ADUCITF_StateToString(nextUpdateStateOnSuccess)); - ADUC_Workflow_SetUpdateState(workflowData, nextUpdateState); + ADUC_Workflow_SetUpdateState(workflowData, nextUpdateStateOnSuccess); // Transitioning to idle (or failed) state frees and nulls-out the WorkflowHandle as a side-effect of // setting the update state. @@ -796,7 +821,7 @@ void ADUC_Workflow_WorkCompletionCallback(const void* workCompletionToken, ADUC_ // // We are now ready to transition to the next step of the workflow. // - ADUC_Workflow_AutoTransitionWorkflow(workflowData); + ADUC_Workflow_AutoTransitionWorkflow(workflowData, true); goto done; } } @@ -823,7 +848,29 @@ void ADUC_Workflow_WorkCompletionCallback(const void* workCompletionToken, ADUC_ if (cancellationType == ADUC_WorkflowCancellationType_Replacement) { - // Reset workflow state to process deployment and transfer the deferred workflow to current. + // Cleanup the download sandbox for the current workflowId + // since it will not be transitioning to Idle state (where + // sandbox cleanup is normally done) + + char* workflowId = ADUC_WorkflowData_GetWorkflowId(workflowData); // see workflow_free_string below + char* workFolder = ADUC_WorkflowData_GetWorkFolder(workflowData); // see workflow_free_string below + + if (workflowId != NULL && workFolder != NULL) + { + Log_Info("Cleanup sandbox before replacement workflow"); + + const ADUC_UpdateActionCallbacks* updateActionCallbacks = + &(workflowData->UpdateActionCallbacks); + + updateActionCallbacks->SandboxDestroyCallback( + updateActionCallbacks->PlatformLayerHandle, workflowId, workFolder); + } + + workflow_free_string(workflowId); + workflow_free_string(workFolder); + + // Reset workflow state to process deployment and transfer + // the deferred workflow to current. workflow_update_for_replacement(workflowData->WorkflowHandle); } else @@ -862,22 +909,20 @@ void ADUC_Workflow_WorkCompletionCallback(const void* workCompletionToken, ADUC_ else { // Operation failed. - // - // Report back the result and set state to "Failed". - // It's expected that the service will call us again with a "Cancel" action, - // to indicate that it's received the operation result and state, at which time - // we'll return back to idle state. - Log_Error( - "%s failed. error %d, %d (0x%X) - Expecting service to send Cancel action.", - ADUCITF_WorkflowStepToString(entry->WorkflowStep), - result.ResultCode, - result.ExtendedResultCode, - result.ExtendedResultCode); + const ADUCITF_State nextUpdateStateOnFailure = entry->NextStateOnFailure; - ADUC_Workflow_SetUpdateStateWithResult(workflowData, ADUCITF_State_Failed, result); + Log_Info( + "WorkCompletionCallback: %s failed. Going to state %s", + ADUCITF_WorkflowStepToString(entry->WorkflowStep), + ADUCITF_StateToString(nextUpdateStateOnFailure)); + // Reset so that a Retry/Replacement avoids cancel and instead properly starts processing. workflow_set_operation_in_progress(workflowData->WorkflowHandle, false); + + ADUC_Workflow_SetUpdateState(workflowData, nextUpdateStateOnFailure); + + ADUC_Workflow_AutoTransitionWorkflow(workflowData, false); } } @@ -1020,6 +1065,42 @@ static void ADUC_Workflow_SetUpdateStateHelper( Log_RequestFlush(); } +/** + * @brief For each update payload that has a DownloadHandlerId, load the handler and call OnUpdateWorkflowCompleted. + * + * @param workflowHandle The workflow handle. + * @details This function will not fail but if a download handler's OnUpdateWorkflowCompleted fails, side effects include logging the error result codes and saving the extended result code that can be reported along with a successful workflow deployment. + */ +static void CallDownloadHandlerOnUpdateWorkflowCompleted(const ADUC_WorkflowHandle workflowHandle) +{ + size_t payloadCount = workflow_get_update_files_count(workflowHandle); + for (size_t i = 0; i < payloadCount; ++i) + { + ADUC_Result result = {}; + ADUC_FileEntity* fileEntity = NULL; + if (!workflow_get_update_file(workflowHandle, i, &fileEntity) || IsNullOrEmpty(fileEntity->DownloadHandlerId)) + { + continue; + } + + // NOTE: do not free the handle as it is owned by the DownloadHandlerFactory. + DownloadHandlerHandle* handle = ADUC_DownloadHandlerFactory_LoadDownloadHandler(fileEntity->DownloadHandlerId); + if (handle != NULL) + { + result = ADUC_DownloadHandlerPlugin_OnUpdateWorkflowCompleted(handle, workflowHandle); + if (IsAducResultCodeFailure(result.ResultCode)) + { + Log_Warn( + "OnupdateWorkflowCompleted, result 0x%08x, erc 0x%08x", + result.ResultCode, + result.ExtendedResultCode); + + workflow_set_success_erc(workflowHandle, result.ExtendedResultCode); + } + } + } +} + /** * @brief Set a new update state. * @@ -1068,6 +1149,8 @@ void ADUC_Workflow_SetInstalledUpdateIdAndGoToIdle(ADUC_WorkflowData* workflowDa Log_Error("Failed to set last completed workflow id. Going to idle state."); } + CallDownloadHandlerOnUpdateWorkflowCompleted(workflowData->WorkflowHandle); + ADUC_Workflow_MethodCall_Idle(workflowData); workflowData->SystemRebootState = ADUC_SystemRebootState_None; @@ -1169,7 +1252,6 @@ ADUC_Result ADUC_Workflow_MethodCall_Download(ADUC_MethodCall_Data* methodCallDa ADUC_Result result = { ADUC_Result_Download_Success }; char* workFolder = workflow_get_workfolder(workflowHandle); - char* workflowId = workflow_get_id(workflowHandle); Log_Info("Workflow step: Download"); @@ -1187,7 +1269,7 @@ ADUC_Result ADUC_Workflow_MethodCall_Download(ADUC_MethodCall_Data* methodCallDa // Note: It's okay for SandboxCreate to return NULL for the work folder. // NULL likely indicates an OS without a file system. result = updateActionCallbacks->SandboxCreateCallback( - updateActionCallbacks->PlatformLayerHandle, workflowId, workFolder); + updateActionCallbacks->PlatformLayerHandle, workflow_peek_id(workflowData->WorkflowHandle), workFolder); if (IsAducResultCodeFailure(result.ResultCode)) { @@ -1206,7 +1288,6 @@ ADUC_Result ADUC_Workflow_MethodCall_Download(ADUC_MethodCall_Data* methodCallDa } done: - workflow_free_string(workflowId); workflow_free_string(workFolder); return result; @@ -1233,7 +1314,7 @@ ADUC_Result ADUC_Workflow_MethodCall_Install(ADUC_MethodCall_Data* methodCallDat Log_Info("Workflow step: Install"); ADUCITF_State lastReportedState = ADUC_WorkflowData_GetLastReportedState(workflowData); - if (lastReportedState != ADUCITF_State_DownloadSucceeded) + if (lastReportedState != ADUCITF_State_BackupSucceeded) { Log_Error("Install Workflow step called in unexpected state: %s!", ADUCITF_StateToString(lastReportedState)); result.ResultCode = ADUC_Result_Failure; @@ -1254,16 +1335,14 @@ ADUC_Result ADUC_Workflow_MethodCall_Install(ADUC_MethodCall_Data* methodCallDat void ADUC_Workflow_MethodCall_Install_Complete(ADUC_MethodCall_Data* methodCallData, ADUC_Result result) { - if (workflow_is_immediate_reboot_requested(methodCallData->WorkflowData->WorkflowHandle) || - workflow_is_reboot_requested(methodCallData->WorkflowData->WorkflowHandle)) + if (workflow_is_immediate_reboot_requested(methodCallData->WorkflowData->WorkflowHandle) + || workflow_is_reboot_requested(methodCallData->WorkflowData->WorkflowHandle)) { // If 'install' indicated a reboot required result from apply, go ahead and reboot. Log_Info("Install indicated success with RebootRequired - rebooting system now"); methodCallData->WorkflowData->SystemRebootState = ADUC_SystemRebootState_Required; - RebootSystemFunc rebootFn = ADUC_WorkflowData_GetRebootSystemFunc(methodCallData->WorkflowData); - - int success = (*rebootFn)(); + int success = ADUC_MethodCall_RebootSystem(); if (success == 0) { methodCallData->WorkflowData->SystemRebootState = ADUC_SystemRebootState_InProgress; @@ -1275,16 +1354,14 @@ void ADUC_Workflow_MethodCall_Install_Complete(ADUC_MethodCall_Data* methodCallD } } else if ( - workflow_is_immediate_agent_restart_requested(methodCallData->WorkflowData->WorkflowHandle) || - workflow_is_agent_restart_requested(methodCallData->WorkflowData->WorkflowHandle)) + workflow_is_immediate_agent_restart_requested(methodCallData->WorkflowData->WorkflowHandle) + || workflow_is_agent_restart_requested(methodCallData->WorkflowData->WorkflowHandle)) { // If 'install' indicated a restart is required, go ahead and restart the agent. Log_Info("Install indicated success with AgentRestartRequired - restarting the agent now"); methodCallData->WorkflowData->SystemRebootState = ADUC_SystemRebootState_Required; - RestartAgentFunc restartAgentFn = ADUC_WorkflowData_GetRestartAgentFunc(methodCallData->WorkflowData); - - int success = (*restartAgentFn)(); + int success = ADUC_MethodCall_RestartAgent(); if (success == 0) { methodCallData->WorkflowData->AgentRestartState = ADUC_AgentRestartState_InProgress; @@ -1297,6 +1374,46 @@ void ADUC_Workflow_MethodCall_Install_Complete(ADUC_MethodCall_Data* methodCallD } } +/** + * @brief Called to do backup. + * + * @param[in] methodCallData - the method call data. + * @return Result code. + */ +ADUC_Result ADUC_Workflow_MethodCall_Backup(ADUC_MethodCall_Data* methodCallData) +{ + ADUC_WorkflowData* workflowData = methodCallData->WorkflowData; + const ADUC_UpdateActionCallbacks* updateActionCallbacks = &(workflowData->UpdateActionCallbacks); + ADUC_Result result = {}; + + Log_Info("Workflow step: backup"); + + ADUCITF_State lastReportedState = ADUC_WorkflowData_GetLastReportedState(workflowData); + if (lastReportedState != ADUCITF_State_DownloadSucceeded) + { + Log_Error("Backup Workflow step called in unexpected state: %s!", ADUCITF_StateToString(lastReportedState)); + result.ResultCode = ADUC_Result_Failure; + result.ExtendedResultCode = ADUC_ERC_UPPERLEVEL_WORKFLOW_UPDATE_ACTION_UNEXPECTED_STATE; + goto done; + } + + ADUC_Workflow_SetUpdateState(workflowData, ADUCITF_State_BackupStarted); + + Log_Info("Calling BackupCallback"); + + result = updateActionCallbacks->BackupCallback( + updateActionCallbacks->PlatformLayerHandle, &(methodCallData->WorkCompletionData), workflowData); + +done: + return result; +} + +void ADUC_Workflow_MethodCall_Backup_Complete(ADUC_MethodCall_Data* methodCallData, ADUC_Result result) +{ + UNREFERENCED_PARAMETER(methodCallData); + UNREFERENCED_PARAMETER(result); +} + /** * @brief Called to do apply. * @@ -1333,16 +1450,14 @@ ADUC_Result ADUC_Workflow_MethodCall_Apply(ADUC_MethodCall_Data* methodCallData) void ADUC_Workflow_MethodCall_Apply_Complete(ADUC_MethodCall_Data* methodCallData, ADUC_Result result) { - if (workflow_is_immediate_reboot_requested(methodCallData->WorkflowData->WorkflowHandle) || - workflow_is_reboot_requested(methodCallData->WorkflowData->WorkflowHandle)) + if (workflow_is_immediate_reboot_requested(methodCallData->WorkflowData->WorkflowHandle) + || workflow_is_reboot_requested(methodCallData->WorkflowData->WorkflowHandle)) { // If apply indicated a reboot required result from apply, go ahead and reboot. Log_Info("Apply indicated success with RebootRequired - rebooting system now"); methodCallData->WorkflowData->SystemRebootState = ADUC_SystemRebootState_Required; - RebootSystemFunc rebootFn = ADUC_WorkflowData_GetRebootSystemFunc(methodCallData->WorkflowData); - - int success = (*rebootFn)(); + int success = ADUC_MethodCall_RebootSystem(); if (success == 0) { methodCallData->WorkflowData->SystemRebootState = ADUC_SystemRebootState_InProgress; @@ -1354,16 +1469,14 @@ void ADUC_Workflow_MethodCall_Apply_Complete(ADUC_MethodCall_Data* methodCallDat } } else if ( - workflow_is_immediate_agent_restart_requested(methodCallData->WorkflowData->WorkflowHandle) || - workflow_is_agent_restart_requested(methodCallData->WorkflowData->WorkflowHandle)) + workflow_is_immediate_agent_restart_requested(methodCallData->WorkflowData->WorkflowHandle) + || workflow_is_agent_restart_requested(methodCallData->WorkflowData->WorkflowHandle)) { // If apply indicated a restart is required, go ahead and restart the agent. Log_Info("Apply indicated success with AgentRestartRequired - restarting the agent now"); methodCallData->WorkflowData->SystemRebootState = ADUC_SystemRebootState_Required; - RestartAgentFunc restartAgentFn = ADUC_WorkflowData_GetRestartAgentFunc(methodCallData->WorkflowData); - - int success = (*restartAgentFn)(); + int success = ADUC_MethodCall_RestartAgent(); if (success == 0) { methodCallData->WorkflowData->AgentRestartState = ADUC_AgentRestartState_InProgress; @@ -1381,6 +1494,90 @@ void ADUC_Workflow_MethodCall_Apply_Complete(ADUC_MethodCall_Data* methodCallDat } } +/** + * @brief Called to do restore. + * + * @param[in] methodCallData - the method call data. + * @return Result code. + */ +ADUC_Result ADUC_Workflow_MethodCall_Restore(ADUC_MethodCall_Data* methodCallData) +{ + ADUC_WorkflowData* workflowData = methodCallData->WorkflowData; + const ADUC_UpdateActionCallbacks* updateActionCallbacks = &(workflowData->UpdateActionCallbacks); + ADUC_Result result = {}; + + Log_Info("Workflow step: Restore"); + + ADUCITF_State lastReportedState = ADUC_WorkflowData_GetLastReportedState(workflowData); + if (lastReportedState != ADUCITF_State_Failed) + { + Log_Error("Apply Workflow step called in unexpected state: %s!", ADUCITF_StateToString(lastReportedState)); + result.ResultCode = ADUC_Result_Failure; + result.ExtendedResultCode = ADUC_ERC_NOTPERMITTED; + goto done; + } + + workflow_set_current_workflowstep(workflowData->WorkflowHandle, ADUCITF_WorkflowStep_Restore); + + ADUC_Workflow_SetUpdateState(workflowData, ADUCITF_State_RestoreStarted); + + Log_Info("Calling RestoreCallback"); + + result = updateActionCallbacks->RestoreCallback( + updateActionCallbacks->PlatformLayerHandle, &(methodCallData->WorkCompletionData), workflowData); + +done: + return result; +} + +void ADUC_Workflow_MethodCall_Restore_Complete(ADUC_MethodCall_Data* methodCallData, ADUC_Result result) +{ + if (result.ResultCode == ADUC_Result_Restore_RequiredReboot + || result.ResultCode == ADUC_Result_Restore_RequiredImmediateReboot) + { + // If restore indicated a reboot required result from restore, go ahead and reboot. + Log_Info("Restore indicated success with RebootRequired - rebooting system now"); + methodCallData->WorkflowData->SystemRebootState = ADUC_SystemRebootState_Required; + + int success = ADUC_MethodCall_RebootSystem(); + if (success == 0) + { + methodCallData->WorkflowData->SystemRebootState = ADUC_SystemRebootState_InProgress; + } + else + { + Log_Error("Reboot attempt failed."); + workflow_set_operation_in_progress(methodCallData->WorkflowData->WorkflowHandle, false); + } + } + else if ( + result.ResultCode == ADUC_Result_Restore_RequiredAgentRestart + || result.ResultCode == ADUC_Result_Restore_RequiredImmediateAgentRestart) + { + // If restore indicated a restart is required, go ahead and restart the agent. + Log_Info("Restore indicated success with AgentRestartRequired - restarting the agent now"); + methodCallData->WorkflowData->SystemRebootState = ADUC_SystemRebootState_Required; + + int success = ADUC_MethodCall_RestartAgent(); + if (success == 0) + { + methodCallData->WorkflowData->AgentRestartState = ADUC_AgentRestartState_InProgress; + } + else + { + Log_Error("Agent restart attempt failed."); + workflow_set_operation_in_progress(methodCallData->WorkflowData->WorkflowHandle, false); + } + } + else if ( + result.ResultCode == ADUC_Result_Restore_Success + || result.ResultCode == ADUC_Result_Restore_Success_Unsupported) + { + // An restore action completed successfully. Continue to the next step. + workflow_set_operation_in_progress(methodCallData->WorkflowData->WorkflowHandle, false); + } +} + /** * @brief Called to request platform-layer operation to cancel. * diff --git a/src/agent/CMakeLists.txt b/src/agent/CMakeLists.txt index a663e8882..ff66816c7 100644 --- a/src/agent/CMakeLists.txt +++ b/src/agent/CMakeLists.txt @@ -4,9 +4,11 @@ project (azure_iot_aduc_agent) set (target_name AducIotAgent) -add_subdirectory (pnp_helper) +add_subdirectory (adu_core_export_helpers) add_subdirectory (adu_core_interface) add_subdirectory (device_info_interface) +add_subdirectory (pnp_helper) +add_subdirectory (command_helper) include (agentRules) @@ -54,8 +56,11 @@ endif () target_compile_definitions ( ${target_name} - PRIVATE ADUC_VERSION="${ADUC_VERSION}" ADUC_PLATFORM_LAYER="${ADUC_PLATFORM_LAYER}" - ADUC_CONTENT_HANDLERS="${ADUC_CONTENT_HANDLERS}") + PRIVATE ADUC_VERSION="${ADUC_VERSION}" + ADUC_PLATFORM_LAYER="${ADUC_PLATFORM_LAYER}" + ADUC_CONTENT_HANDLERS="${ADUC_CONTENT_HANDLERS}" + SUPPORTED_UPDATE_MANIFEST_VERSION_MIN=${SUPPORTED_UPDATE_MANIFEST_VERSION_MIN} + SUPPORTED_UPDATE_MANIFEST_VERSION_MAX=${SUPPORTED_UPDATE_MANIFEST_VERSION_MAX}) # NOTE: the call to find_package for azure_c_shared_utility # must come before umqtt since their config.cmake files expect the aziotsharedutil target to already have been defined. @@ -70,16 +75,21 @@ target_link_libraries ( ${target_name} PRIVATE aziotsharedutil IotHubClient::iothub_client - iothub_client_mqtt_transport umqtt + aduc::adu_core_export_helpers aduc::adu_core_interface + aduc::agent_orchestration aduc::agent_workflow aduc::c_utils + aduc::command_helper aduc::communication_abstraction aduc::config_utils + aduc::d2c_messaging aduc::device_info_interface aduc::eis_utils aduc::extension_manager + aduc::extension_utils + aduc::https_proxy_utils aduc::logging aduc::permission_utils aduc::pnp_helper @@ -87,6 +97,28 @@ target_link_libraries ( diagnostics_component::diagnostics_interface diagnostics_component::diagnostics_devicename) +if (ADUC_IOT_HUB_PROTOCOL STREQUAL "MQTT") + target_compile_definitions (${target_name} PRIVATE ADUC_ALLOW_MQTT=1) + target_link_libraries (${target_name} PRIVATE iothub_client_mqtt_transport) + +elseif (ADUC_IOT_HUB_PROTOCOL STREQUAL "MQTT_over_WebSockets") + target_compile_definitions (${target_name} PRIVATE ADUC_ALLOW_MQTT_OVER_WEBSOCKETS=1) + target_link_libraries (${target_name} PRIVATE iothub_client_mqtt_ws_transport) + +elseif (ADUC_IOT_HUB_PROTOCOL STREQUAL "IotHub_Protocol_from_Config") + target_compile_definitions (${target_name} PRIVATE ADUC_GET_IOTHUB_PROTOCOL_FROM_CONFIG=1) + target_compile_definitions (${target_name} PRIVATE ADUC_ALLOW_MQTT=1) + target_compile_definitions (${target_name} PRIVATE ADUC_ALLOW_MQTT_OVER_WEBSOCKETS=1) + target_link_libraries (${target_name} PRIVATE iothub_client_mqtt_transport) + target_link_libraries (${target_name} PRIVATE iothub_client_mqtt_ws_transport) + +else () + message ( + FATAL_ERROR, + "Invalid value '${ADUC_IOT_HUB_PROTOCOL}' for ADUC_IOT_HUB_PROTOCOL. Valid values: MQTT | MQTT_over_WebSockets | IotHub_Protocol_from_Config" + ) +endif () + get_filename_component ( ADUC_INSTALLEDCRITERIA_FILE_PATH "${ADUC_DATA_FOLDER}/${ADUC_INSTALLEDCRITERIA_FILE}" @@ -100,6 +132,7 @@ target_compile_definitions ( ADUC_CONF_FOLDER="${ADUC_CONF_FOLDER}" ADUC_DATA_FOLDER="${ADUC_DATA_FOLDER}" ADUC_DOWNLOADS_FOLDER="${ADUC_DOWNLOADS_FOLDER}" + ADUC_COMMANDS_FIFO_NAME="${ADUC_COMMANDS_FIFO_NAME}" ADUC_FILE_GROUP="${ADUC_FILE_GROUP}" ADUC_FILE_USER="${ADUC_FILE_USER}" ADUC_INSTALLEDCRITERIA_FILE_PATH="${ADUC_INSTALLEDCRITERIA_FILE_PATH}" diff --git a/src/agent/adu_core_export_helpers/CMakeLists.txt b/src/agent/adu_core_export_helpers/CMakeLists.txt new file mode 100644 index 000000000..eeda05b27 --- /dev/null +++ b/src/agent/adu_core_export_helpers/CMakeLists.txt @@ -0,0 +1,21 @@ +set (target_name adu_core_export_helpers) + +include (agentRules) +compileasc99 () + +find_package (Parson REQUIRED) + +add_library (${target_name} STATIC "") +add_library (aduc::${target_name} ALIAS ${target_name}) + +# Turn -fPIC on, in order to use this library in a shared library. +set_property (TARGET ${target_name} PROPERTY POSITION_INDEPENDENT_CODE ON) + +target_include_directories (${target_name} PUBLIC inc ${ADUC_EXPORT_INCLUDES}) + +target_sources (${target_name} PRIVATE src/adu_core_export_helpers.c) + +target_link_libraries ( + ${target_name} + PUBLIC aduc::adu_types aduc::c_utils + PRIVATE aduc::logging aduc::platform_layer Parson::parson) diff --git a/src/agent/adu_core_interface/inc/aduc/adu_core_export_helpers.h b/src/agent/adu_core_export_helpers/inc/aduc/adu_core_export_helpers.h similarity index 97% rename from src/agent/adu_core_interface/inc/aduc/adu_core_export_helpers.h rename to src/agent/adu_core_export_helpers/inc/aduc/adu_core_export_helpers.h index a54707fe1..ff421e3ec 100644 --- a/src/agent/adu_core_interface/inc/aduc/adu_core_export_helpers.h +++ b/src/agent/adu_core_export_helpers/inc/aduc/adu_core_export_helpers.h @@ -9,11 +9,10 @@ #define ADUC_ADU_CORE_EXPORT_HELPERS_H #include "aduc/adu_core_exports.h" -#include "aduc/agent_workflow.h" -#include "aduc/types/workflow.h" #include "aduc/c_utils.h" #include "aduc/types/hash.h" // for ADUC_Hash #include "aduc/types/update_content.h" // for ADUC_FileEntity +#include "aduc/types/workflow.h" #include @@ -39,7 +38,6 @@ ADUC_MethodCall_Register(ADUC_UpdateActionCallbacks* updateActionCallbacks, unsi */ void ADUC_MethodCall_Unregister(const ADUC_UpdateActionCallbacks* updateActionCallbacks); - // // Reboot system // diff --git a/src/agent/adu_core_interface/src/adu_core_export_helpers.c b/src/agent/adu_core_export_helpers/src/adu_core_export_helpers.c similarity index 89% rename from src/agent/adu_core_interface/src/adu_core_export_helpers.c rename to src/agent/adu_core_export_helpers/src/adu_core_export_helpers.c index 1e01e2c63..642bf8189 100644 --- a/src/agent/adu_core_interface/src/adu_core_export_helpers.c +++ b/src/agent/adu_core_export_helpers/src/adu_core_export_helpers.c @@ -6,23 +6,12 @@ * Licensed under the MIT License. */ #include "aduc/adu_core_export_helpers.h" -#include "aduc/adu_core_interface.h" -#include "aduc/c_utils.h" -#include "aduc/extension_manager.h" -#include "aduc/hash_utils.h" #include "aduc/logging.h" -#include "aduc/parser_utils.h" -#include "aduc/string_c_utils.h" -#include "aduc/agent_workflow.h" -#include "aduc/workflow_data_utils.h" -#include "aduc/workflow_utils.h" - +#include #include #include #include - #include // for waitpid -#include // // ADUC_UpdateActionCallbacks helpers. @@ -38,14 +27,17 @@ static _Bool ADUC_UpdateActionCallbacks_VerifyData(const ADUC_UpdateActionCallba { // Note: Okay for updateActionCallbacks->PlatformLayerHandle to be NULL. + // clang-format off if (updateActionCallbacks->IdleCallback == NULL || updateActionCallbacks->DownloadCallback == NULL + || updateActionCallbacks->BackupCallback == NULL || updateActionCallbacks->InstallCallback == NULL || updateActionCallbacks->ApplyCallback == NULL + || updateActionCallbacks->RestoreCallback == NULL || updateActionCallbacks->SandboxCreateCallback == NULL - || updateActionCallbacks->SandboxDestroyCallback == NULL - || updateActionCallbacks->DoWorkCallback == NULL + || updateActionCallbacks->SandboxDestroyCallback == NULL || updateActionCallbacks->DoWorkCallback == NULL || updateActionCallbacks->IsInstalledCallback == NULL) + // clang-format on { Log_Error("Invalid ADUC_UpdateActionCallbacks object"); return false; diff --git a/src/agent/adu_core_interface/CMakeLists.txt b/src/agent/adu_core_interface/CMakeLists.txt index 214f43bc1..a48bc7d71 100644 --- a/src/agent/adu_core_interface/CMakeLists.txt +++ b/src/agent/adu_core_interface/CMakeLists.txt @@ -1,46 +1,42 @@ project (adu_core_interface) include (agentRules) - compileasc99 () -add_library ( - ${PROJECT_NAME} STATIC - src/adu_core_interface.c - src/adu_core_json.c - src/adu_core_export_helpers.c - src/agent_orchestration.c - src/device_properties.c - src/startup_msg_helper.c) - -set_property (TARGET ${PROJECT_NAME} PROPERTY POSITION_INDEPENDENT_CODE ON) +find_package (azure_c_shared_utility REQUIRED) +find_package (IotHubClient REQUIRED) +find_package (Parson REQUIRED) +add_library (${PROJECT_NAME} STATIC "") add_library (aduc::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) +set_property (TARGET ${PROJECT_NAME} PROPERTY POSITION_INDEPENDENT_CODE ON) + target_include_directories (${PROJECT_NAME} PUBLIC inc ${ADUC_EXPORT_INCLUDES}) -target_link_digital_twin_client (${PROJECT_NAME} PUBLIC) -find_package (Parson REQUIRED) +target_sources (${PROJECT_NAME} PRIVATE src/adu_core_interface.c src/device_properties.c + src/startup_msg_helper.c) target_link_libraries ( ${PROJECT_NAME} PUBLIC aduc::adu_types - aduc::agent_workflow aduc::c_utils aduc::communication_abstraction - aduc::extension_manager - Parson::parson - PRIVATE aduc::config_utils + IotHubClient::iothub_client + PRIVATE aduc::adu_core_export_helpers + aduc::agent_orchestration + aduc::agent_workflow + aduc::config_utils + aduc::d2c_messaging + aduc::extension_manager aduc::hash_utils - aduc::parson_json_utils - aduc::jws_utils aduc::logging aduc::parser_utils - aduc::platform_layer aduc::pnp_helper - aduc::system_utils aduc::workflow_data_utils - aduc::workflow_utils) + aduc::workflow_utils + aziotsharedutil + Parson::parson) target_compile_definitions ( ${PROJECT_NAME} @@ -48,16 +44,12 @@ target_compile_definitions ( ADUC_CONF_FILE_PATH="${ADUC_CONF_FILE_PATH}" ADUC_DEVICEPROPERTIES_MODEL="${ADUC_DEVICEPROPERTIES_MODEL}" ADUC_VERSION="${ADUC_VERSION}" - ADUC_BUILDER_IDENTIFIER="${ADUC_BUILDER_IDENTIFIER}" - ADUC_BUILD_UNIT_TESTS="${ADUC_BUILD_UNIT_TESTS}") - -if (NOT ADUC_PLATFORM_LAYER STREQUAL "simulator") - if (ADUC_BUILD_UNIT_TESTS) - find_package (umock_c REQUIRED CONFIG) - target_link_libraries (${PROJECT_NAME} PRIVATE umock_c) - add_subdirectory (tests) - endif () + ADUC_BUILDER_IDENTIFIER="${ADUC_BUILDER_IDENTIFIER}") +if (NOT + ADUC_PLATFORM_LAYER + STREQUAL + "simulator") find_package (deliveryoptimization_sdk CONFIG REQUIRED) target_link_libraries (${PROJECT_NAME} PRIVATE Microsoft::deliveryoptimization) else () diff --git a/src/agent/adu_core_interface/inc/aduc/adu_core_interface.h b/src/agent/adu_core_interface/inc/aduc/adu_core_interface.h index 8d0ab42c7..39feb90cc 100644 --- a/src/agent/adu_core_interface/inc/aduc/adu_core_interface.h +++ b/src/agent/adu_core_interface/inc/aduc/adu_core_interface.h @@ -8,7 +8,6 @@ #ifndef ADUC_ADU_CORE_INTERFACE_H #define ADUC_ADU_CORE_INTERFACE_H -#include // ADUCITF_State #include #include #include // ADUC_Result @@ -64,10 +63,15 @@ void AzureDeviceUpdateCoreInterface_DoWork(void* componentContext); void AzureDeviceUpdateCoreInterface_Destroy(void** componentContext); /** - * @brief A callback for an 'deviceUpdate' component's property update events. + * @brief A callback for a 'deviceUpdate' component's property update events. */ void AzureDeviceUpdateCoreInterface_PropertyUpdateCallback( - ADUC_ClientHandle clientHandle, const char* propertyName, JSON_Value* propertyValue, int version, void* context); + ADUC_ClientHandle clientHandle, + const char* propertyName, + JSON_Value* propertyValue, + int version, + ADUC_PnPComponentClient_PropertyUpdate_Context* sourceContext, + void* context); // // Reporting @@ -88,6 +92,21 @@ _Bool AzureDeviceUpdateCoreInterface_ReportStateAndResultAsync( const ADUC_Result* result, const char* installedUpdateId); +/** + * @brief Get the Reporting Json Value object + * + * @param workflowData The workflow data. + * @param updateState The workflow state machine state. + * @param result The pointer to the result. If NULL, then the result will be retrieved from the opaque handle object in the workflow data. + * @param installedUpdateId The installed Update ID string. + * @return JSON_Value* The resultant json value object. + */ +JSON_Value* GetReportingJsonValue( + ADUC_WorkflowData* workflowData, + ADUCITF_State updateState, + const ADUC_Result* result, + const char* installedUpdateId); + EXTERN_C_END #endif // ADUC_ADU_CORE_INTERFACE_H diff --git a/src/agent/adu_core_interface/inc/aduc/adu_core_json.h b/src/agent/adu_core_interface/inc/aduc/adu_core_json.h deleted file mode 100644 index 5513cb0e6..000000000 --- a/src/agent/adu_core_interface/inc/aduc/adu_core_json.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @file adu_core_json.h - * @brief Defines types and methods required to parse JSON from the urn:azureiot:AzureDeviceUpdateCore:1 interface. - * - * Note that the types in this file must be kept in sync with the AzureDeviceUpdateCore interface. - * - * @copyright Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ -#ifndef ADUC_ADU_CORE_JSON_H -#define ADUC_ADU_CORE_JSON_H - -#include "parson.h" -#include -#include -#include -#include - -EXTERN_C_BEGIN -// -// Methods -// - -struct json_value_t; -typedef struct json_value_t JSON_Value; - -struct tagADUC_FileEntity; -struct tagADUC_UpdateId; - -_Bool ADUC_Json_ValidateManifest(const JSON_Value* updateActionJson); - -JSON_Value* ADUC_Json_GetRoot(const char* updateActionJsonString); - -_Bool ADUC_Json_GetUpdateAction(const JSON_Value* updateActionJson, unsigned* updateAction); - -_Bool ADUC_Json_GetInstalledCriteria(const JSON_Value* updateActionJson, char** installedCriteria); - -_Bool ADUC_Json_GetUpdateType(const JSON_Value* updateActionJson, char** updateTypeStr); - -_Bool ADUC_Json_GetFiles( - const JSON_Value* updateActionJson, unsigned int* fileCount, struct tagADUC_FileEntity** files); - -EXTERN_C_END - -#endif // ADUC_ADU_CORE_JSON_H diff --git a/src/agent/adu_core_interface/src/adu_core_interface.c b/src/agent/adu_core_interface/src/adu_core_interface.c index 1019a2530..fc57fa6c1 100644 --- a/src/agent/adu_core_interface/src/adu_core_interface.c +++ b/src/agent/adu_core_interface/src/adu_core_interface.c @@ -12,6 +12,8 @@ #include "aduc/agent_workflow.h" #include "aduc/c_utils.h" #include "aduc/client_handle_helper.h" +#include "aduc/config_utils.h" +#include "aduc/d2c_messaging.h" #include "aduc/hash_utils.h" #include "aduc/logging.h" #include "aduc/string_c_utils.h" @@ -44,17 +46,16 @@ static const char g_aduPnPComponentServicePropertyName[] = "service"; */ ADUC_ClientHandle g_iotHubClientHandleForADUComponent; -void ClientReportedStateCallback(int statusCode, void* context) +/** + * @brief This function is called when the message is no longer being process. + * + * @param context The ADUC_D2C_Message object + * @param status The message status. + */ +static void OnUpdateResultD2CMessageCompleted(void* context, ADUC_D2C_Message_Status status) { UNREFERENCED_PARAMETER(context); - - if (statusCode < 200 || statusCode >= 300) - { - Log_Error( - "Failed to report ADU agent's state, error: %d, %s", - statusCode, - MU_ENUM_TO_STRING(IOTHUB_CLIENT_RESULT, statusCode)); - } + Log_Debug("Send message completed (status:%d)", status); } /** @@ -86,7 +87,6 @@ _Bool ADUC_WorkflowData_Init(ADUC_WorkflowData* workflowData, int argc, char** a workflowData->ReportStateAndResultAsyncCallback = AzureDeviceUpdateCoreInterface_ReportStateAndResultAsync; workflowData->LastCompletedWorkflowId = NULL; - workflowData->LastGoalStateJson = NULL; workflow_set_cancellation_type(workflowData->WorkflowHandle, ADUC_WorkflowCancellationType_None); @@ -114,32 +114,9 @@ void ADUC_WorkflowData_Uninit(ADUC_WorkflowData* workflowData) } workflow_free_string(workflowData->LastCompletedWorkflowId); - free(workflowData->LastGoalStateJson); memset(workflowData, 0, sizeof(*workflowData)); } -/** - * @brief Gets the client handle send report function. - * - * @param workflowData The workflow data. - * @return ClientHandleSnedReportFunc The function for sending the client report. - */ -static ClientHandleSendReportFunc -ADUC_WorkflowData_GetClientHandleSendReportFunc(const ADUC_WorkflowData* workflowData) -{ - ClientHandleSendReportFunc fn = (ClientHandleSendReportFunc)ClientHandle_SendReportedState; - -#ifdef ADUC_BUILD_UNIT_TESTS - ADUC_TestOverride_Hooks* hooks = workflowData->TestOverrides; - if (hooks && hooks->ClientHandle_SendReportedStateFunc_TestOverride) - { - fn = (ClientHandleSendReportFunc)(hooks->ClientHandle_SendReportedStateFunc_TestOverride); - } -#endif - - return fn; -} - /** * @brief Reports the client json via PnP so it ends up in the reported section of the twin. * @@ -157,7 +134,6 @@ static _Bool ReportClientJsonProperty(const char* json_value, ADUC_WorkflowData* return false; } - IOTHUB_CLIENT_RESULT iothubClientResult; STRING_HANDLE jsonToSend = PnP_CreateReportedProperty(g_aduPnPComponentName, g_aduPnPComponentAgentPropertyName, json_value); @@ -167,28 +143,16 @@ static _Bool ReportClientJsonProperty(const char* json_value, ADUC_WorkflowData* goto done; } - const char* jsonToSendStr = STRING_c_str(jsonToSend); - size_t jsonToSendStrLen = strlen(jsonToSendStr); - - Log_Debug("Reporting agent state:\n%s", jsonToSendStr); - - ClientHandleSendReportFunc clientHandle_SendReportedState_Func = - ADUC_WorkflowData_GetClientHandleSendReportFunc(workflowData); - - iothubClientResult = (IOTHUB_CLIENT_RESULT)clientHandle_SendReportedState_Func( - g_iotHubClientHandleForADUComponent, - (const unsigned char*)jsonToSendStr, - jsonToSendStrLen, - ClientReportedStateCallback, - NULL); - - if (iothubClientResult != IOTHUB_CLIENT_OK) + if (!ADUC_D2C_Message_SendAsync( + ADUC_D2C_Message_Type_Device_Update_Result, + &g_iotHubClientHandleForADUComponent, + STRING_c_str(jsonToSend), + NULL /* responseCallback */, + OnUpdateResultD2CMessageCompleted, + NULL /* statusChangedCallback */, + NULL /* userData */)) { - Log_Error( - "Unable to report state, %s, error: %d, %s", - json_value, - iothubClientResult, - MU_ENUM_TO_STRING(IOTHUB_CLIENT_RESULT, iothubClientResult)); + Log_Error("Unable to send update result."); goto done; } @@ -218,8 +182,6 @@ _Bool ReportStartupMsg(ADUC_WorkflowData* workflowData) _Bool success = false; char* jsonString = NULL; - char* manufacturer = NULL; - char* model = NULL; JSON_Value* startupMsgValue = json_value_init_object(); @@ -235,13 +197,22 @@ _Bool ReportStartupMsg(ADUC_WorkflowData* workflowData) goto done; } - if (!StartupMsg_AddDeviceProperties(startupMsgObj)) + ADUC_ConfigInfo config = {}; + + if (!ADUC_ConfigInfo_Init(&config, ADUC_CONF_FILE_PATH)) + { + goto done; + } + + const ADUC_AgentInfo* agent = ADUC_ConfigInfo_GetAgent(&config, 0); + + if (!StartupMsg_AddDeviceProperties(startupMsgObj, agent)) { Log_Error("Could not add Device Properties to the startup message"); goto done; } - if (!StartupMsg_AddCompatPropertyNames(startupMsgObj)) + if (!StartupMsg_AddCompatPropertyNames(startupMsgObj, &config)) { Log_Error("Could not add compatPropertyNames to the startup message"); goto done; @@ -259,11 +230,10 @@ _Bool ReportStartupMsg(ADUC_WorkflowData* workflowData) success = true; done: - free(model); - free(manufacturer); json_value_free(startupMsgValue); json_free_serialized_string(jsonString); + ADUC_ConfigInfo_UnInit(&config); return success; } @@ -341,7 +311,11 @@ void AzureDeviceUpdateCoreInterface_Destroy(void** componentContext) } void OrchestratorUpdateCallback( - ADUC_ClientHandle clientHandle, JSON_Value* propertyValue, int propertyVersion, void* context) + ADUC_ClientHandle clientHandle, + JSON_Value* propertyValue, + int propertyVersion, + ADUC_PnPComponentClient_PropertyUpdate_Context* sourceContext, + void* context) { ADUC_WorkflowData* workflowData = (ADUC_WorkflowData*)context; STRING_HANDLE jsonToSend = NULL; @@ -369,7 +343,7 @@ void OrchestratorUpdateCallback( Log_Debug("Update Action info string (%s), property version (%d)", ackString, propertyVersion); - ADUC_Workflow_HandlePropertyUpdate(workflowData, (const unsigned char*)jsonString, false /* forceDeferral */); + ADUC_Workflow_HandlePropertyUpdate(workflowData, (const unsigned char*)jsonString, sourceContext->forceUpdate); free(jsonString); jsonString = ackString; @@ -388,17 +362,16 @@ void OrchestratorUpdateCallback( goto done; } - const char* jsonToSendStr = STRING_c_str(jsonToSend); - size_t jsonToSendStrLen = strlen(jsonToSendStr); - IOTHUB_CLIENT_RESULT iothubClientResult = ClientHandle_SendReportedState( - clientHandle, (const unsigned char*)jsonToSendStr, jsonToSendStrLen, NULL, NULL); - - if (iothubClientResult != IOTHUB_CLIENT_OK) + if (!ADUC_D2C_Message_SendAsync( + ADUC_D2C_Message_Type_Device_Update_ACK, + &g_iotHubClientHandleForADUComponent, + STRING_c_str(jsonToSend), + NULL /* responseCallback */, + OnUpdateResultD2CMessageCompleted, + NULL /* statusChangedCallback */, + NULL /* userData */)) { - Log_Error( - "Unable to send acknowledgement of property to IoT Hub for component=%s, error=%d", - g_aduPnPComponentName, - iothubClientResult); + Log_Error("Unable to send update result."); goto done; } @@ -417,14 +390,20 @@ void OrchestratorUpdateCallback( * @param propertyName The name of the property that changed. * @param propertyValue The new property value. * @param version Property version. + * @param sourceContext An information about the source of the property update notificaion. * @param context An ADUC_WorkflowData object. */ void AzureDeviceUpdateCoreInterface_PropertyUpdateCallback( - ADUC_ClientHandle clientHandle, const char* propertyName, JSON_Value* propertyValue, int version, void* context) + ADUC_ClientHandle clientHandle, + const char* propertyName, + JSON_Value* propertyValue, + int version, + ADUC_PnPComponentClient_PropertyUpdate_Context* sourceContext, + void* context) { if (strcmp(propertyName, g_aduPnPComponentServicePropertyName) == 0) { - OrchestratorUpdateCallback(clientHandle, propertyValue, version, context); + OrchestratorUpdateCallback(clientHandle, propertyValue, version, sourceContext, context); } else { @@ -543,6 +522,7 @@ JSON_Value* GetReportingJsonValue( // ADUC_Result rootResult; ADUC_WorkflowHandle handle = workflowData->WorkflowHandle; + ADUC_Result_t successErc = workflow_get_success_erc(handle); if (result != NULL) { @@ -553,6 +533,13 @@ JSON_Value* GetReportingJsonValue( rootResult = workflow_get_result(handle); } + // Allow reporting of extended result code of soft-failing mechanisms, such as download handler, that have a + // fallback mechanism (e.g. full content download) that can ultimately become an overall success. + if (IsAducResultCodeSuccess(rootResult.ResultCode) && successErc != 0) + { + rootResult.ExtendedResultCode = successErc; + } + JSON_Value* rootValue = json_value_init_object(); JSON_Object* rootObject = json_value_get_object(rootValue); int stepsCount = workflow_get_children_count(handle); @@ -627,13 +614,12 @@ JSON_Value* GetReportingJsonValue( // // Workflow // - char* workflowId = workflow_get_id(handle); - if (!IsNullOrEmpty(workflowId)) + if (!IsNullOrEmpty(workflow_peek_id(handle))) { _Bool success = set_workflow_properties( workflowValue, ADUC_WorkflowData_GetCurrentAction(workflowData), - workflowId, + workflow_peek_id(handle), workflow_peek_retryTimestamp(handle)); if (!success) diff --git a/src/agent/adu_core_interface/src/adu_core_json.c b/src/agent/adu_core_interface/src/adu_core_json.c deleted file mode 100644 index a9cfbd660..000000000 --- a/src/agent/adu_core_interface/src/adu_core_json.c +++ /dev/null @@ -1,304 +0,0 @@ -/** - * @file adu_core_json.c - * @brief Defines types and methods required to parse JSON from the urn:azureiot:AzureDeviceUpdateCore:1 interface. - * - * Note that the types in this file must be kept in sync with the AzureDeviceUpdateCore interface. - * - * @copyright Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ -#include -#include - -#include "aduc/adu_core_export_helpers.h" -#include "aduc/adu_core_interface.h" -#include "aduc/c_utils.h" -#include "aduc/hash_utils.h" -#include "aduc/logging.h" -#include "aduc/parser_utils.h" -#include "aduc/types/workflow.h" -#include "aduc/agent_workflow.h" -#include -#include - -_Bool ADUC_JSON_GetUpdateManifestStringField( - const JSON_Value* updateActionJson, const char* jsonFieldName, char** value); - -/** - * @brief Do (some) UpdateAction JSON validation and return root object. - * @param updateActionJsonString JSON string containing information about the UpdateAction. - * @return JSON_Value* NULL on error. - */ -JSON_Value* ADUC_Json_GetRoot(const char* updateActionJsonString) -{ - _Bool succeeded = false; - - JSON_Value* updateActionJson = json_parse_string(updateActionJsonString); - if (updateActionJson == NULL) - { - Log_Error("Invalid json root. JSON: %s", updateActionJsonString); - goto done; - } - - if (json_value_get_type(updateActionJson) != JSONObject) - { - Log_Error("Invalid json root type. JSON: %s", updateActionJsonString); - goto done; - } - - succeeded = true; - -done: - if (!succeeded) - { - json_value_free(updateActionJson); - updateActionJson = NULL; - } - - return updateActionJson; -} - -/** - * @brief Helper function that extracts the signature, validates it, and then stores the hash from the body of the signature in the hashValue - * @param updateActionJson JSON Value created from an updateActionJsonString - * @returns false on failure to validate and true when valid - */ -_Bool ADUC_Json_ValidateManifestSignature(const JSON_Value* updateActionJson) -{ - _Bool success = false; - - if (updateActionJson == NULL) - { - Log_Error("updateActionJson passed to ADUC_Json_ValidateManifestSignature is NULL"); - goto done; - } - - const char* manifestSignature = - ADUC_JSON_GetStringFieldPtr(updateActionJson, ADUCITF_FIELDNAME_UPDATEMANIFESTSIGNATURE); - if (manifestSignature == NULL) - { - Log_Error("Invalid manifest. Does not contain a signature"); - goto done; - } - - JWSResult result = VerifyJWSWithSJWK(manifestSignature); - if (result != JWSResult_Success) - { - Log_Error("Manifest signature validation failed with result: %u", result); - goto done; - } - - success = true; - -done: - return success; -} - -/** - * @brief Helper function for checking the hash of the updatemanifest is equal to the - * hash held within the signature - * @param updateActionJson JSON value created form an updateActionJsonString and contains - * both the updateManifest and the updateManifestSignature - * @returns true on success and false on failure - */ -_Bool ADUC_Json_ValidateManifestHash(const JSON_Value* updateActionJson) -{ - _Bool success = false; - - JSON_Value* signatureValue = NULL; - char* jwtPayload = NULL; - - if (updateActionJson == NULL) - { - Log_Error("updateActionJson passed to ADUC_Json_ValidateManifestHash is NULL"); - goto done; - } - - const char* updateManifestStr = ADUC_JSON_GetStringFieldPtr(updateActionJson, ADUCITF_FIELDNAME_UPDATEMANIFEST); - - if (updateManifestStr == NULL) - { - Log_Error("No updateManifest field in updateActionJson "); - goto done; - } - - const char* updateManifestb64Signature = - ADUC_JSON_GetStringFieldPtr(updateActionJson, ADUCITF_FIELDNAME_UPDATEMANIFESTSIGNATURE); - if (updateManifestb64Signature == NULL) - { - Log_Error("No updateManifestSignature within the updateActionJson"); - goto done; - } - - if (!GetPayloadFromJWT(updateManifestb64Signature, &jwtPayload)) - { - Log_Error("Retrieving the payload from the manifest failed."); - goto done; - } - - signatureValue = ADUC_Json_GetRoot(jwtPayload); - if (signatureValue == NULL) - { - Log_Error("updateManifestSignature contains an invalid body"); - goto done; - } - - const char* b64SignatureManifestHash = ADUC_JSON_GetStringFieldPtr(signatureValue, ADUCITF_JWT_FIELDNAME_HASH); - if (b64SignatureManifestHash == NULL) - { - Log_Error("updateManifestSignature does not contain a hash value. Cannot validate the manifest!"); - goto done; - } - - success = ADUC_HashUtils_IsValidBufferHash( - (const uint8_t*)updateManifestStr, strlen(updateManifestStr), b64SignatureManifestHash, SHA256); - -done: - json_value_free(signatureValue); - free(jwtPayload); - - return success; -} - -/** - * @brief Do validation of the manifest within the UpdateActionJson . - * @details Validates the base64 encoded updateManifestSignature and compares the hash of the updateManifest and the expected hash contained within the updateManifestSignature - * @param updateActionJsonString JSON Value that should contain the updateManifest and updateManifestSignature - * @return True if a valid manifest and false if not - */ -_Bool ADUC_Json_ValidateManifest(const JSON_Value* updateActionJson) -{ - _Bool succeeded = false; - - if (!ADUC_Json_ValidateManifestSignature(updateActionJson)) - { - // Handle failed signature case - Log_Error("ADUC_Json_ValidateManifestSignature failed"); - goto done; - } - - if (!ADUC_Json_ValidateManifestHash(updateActionJson)) - { - // Handle failed hash case - Log_Error("ADUC_Json_ValidateManifestHash failed"); - goto done; - } - - succeeded = true; -done: - return succeeded; -} - -/** - * @brief Parse the update action JSON for the UpdateAction value. - * - * Sample JSON: - * - * { - * "action": 0, - * ... - * } - * - * - * @param updateActionJson UpdateAction Json to parse - * @param updateAction Found value on success. - * @return Bool Success state. - */ -_Bool ADUC_Json_GetUpdateAction(const JSON_Value* updateActionJson, unsigned* updateAction) -{ - _Bool succeeded = false; - - *updateAction = 0; - - JSON_Object* root_object = json_value_get_object(updateActionJson); - if (root_object == NULL) - { - goto done; - } - - // parson library will return 0 if get_number fails, so first check that the type is correct. - if (!json_object_dothas_value_of_type(root_object, ADUCITF_FIELDNAME_WORKFLOW_DOT_ACTION, JSONNumber)) - { - Log_Error("Invalid json - '%s' missing or incorrect", ADUCITF_FIELDNAME_WORKFLOW_DOT_ACTION); - goto done; - } - - *updateAction = (unsigned)json_object_dotget_number(root_object, ADUCITF_FIELDNAME_WORKFLOW_DOT_ACTION); - - succeeded = true; - -done: - return succeeded; -} - -/** - * @brief Parse the update action JSON for installedCriteria value. - * - * Sample JSON: - * { - * "installedCriteria": "1.0.0.0" - * } - * - * @param updateActionJson UpdateAction JSON to parse. - * @param installedCriteria The returned installed criteria string. Caller must call free(). - * @return _Bool True if call was successful. - */ -_Bool ADUC_Json_GetInstalledCriteria(const JSON_Value* updateActionJson, char** installedCriteria) -{ - return ADUC_JSON_GetUpdateManifestStringField( - updateActionJson, ADUCITF_FIELDNAME_INSTALLEDCRITERIA, installedCriteria); -} - -/** - * @brief Retrieves the updateType from @p updateActionJson's updateManifest which is stored in updateTypeStr - * @param updateActionJson UpdateAction JSON to parse - * @param updateTypeStr string to store the updateType in - * @returns True on success, False on failure - */ -_Bool ADUC_Json_GetUpdateType(const JSON_Value* updateActionJson, char** updateTypeStr) -{ - return ADUC_JSON_GetUpdateManifestStringField(updateActionJson, ADUCITF_FIELDNAME_UPDATETYPE, updateTypeStr); -} - -/** - * @brief Gets a string field from the updateManifest JSON within the update action JSON - * - * @param updateActionJson The update Action JSON - * @param jsonFieldName The name of the updateManifest JSON field to get - * @param value The buffer to fill with the value from the JSON field. Caller must call free() - * - * @return _Bool true if call succeeded. False otherwise - */ -_Bool ADUC_JSON_GetUpdateManifestStringField( - const JSON_Value* updateActionJson, const char* jsonFieldName, char** value) -{ - _Bool success = false; - - *value = NULL; - - JSON_Value* updateManifestValue = ADUC_JSON_GetUpdateManifestRoot(updateActionJson); - - if (updateManifestValue == NULL) - { - goto done; - } - - if (!ADUC_JSON_GetStringField(updateManifestValue, jsonFieldName, value)) - { - goto done; - } - - success = true; - -done: - - if (!success) - { - free(*value); - *value = NULL; - } - - json_value_free(updateManifestValue); - - return success; -} diff --git a/src/agent/adu_core_interface/src/device_properties.c b/src/agent/adu_core_interface/src/device_properties.c index 44903c079..691480b7a 100644 --- a/src/agent/adu_core_interface/src/device_properties.c +++ b/src/agent/adu_core_interface/src/device_properties.c @@ -17,9 +17,9 @@ #include /* - * @brief The ADU interface id associated with the modelId for agent-orchestrated updates. + * @brief The ADU contract model id associated with the modelId for agent-orchestrated updates. */ -#define ADUC_DEVICEPROPERTIES_INTERFACEID "dtmi:azure:iot:deviceUpdate;1" +#define ADUC_DEVICEPROPERTIES_DEVICEUPDATE_CONTRACT_MODEL_ID "dtmi:azure:iot:deviceUpdateContractModel;2" /* @brief The adu client builder and version. * Consisting of BUILDER; component and ADUC_VERSION @@ -30,9 +30,10 @@ /** * @brief Adds the aduc_manufacturer and aduc_model to the @p devicePropsObj * @param devicePropsObj the JSON_Object the manufacturer and model will be added to + * @param agent the ADUC_AgentInfo that contains the agent info * @returns true on successful addition and false on failure */ -_Bool DeviceProperties_AddManufacturerAndModel(JSON_Object* devicePropsObj) +_Bool DeviceProperties_AddManufacturerAndModel(JSON_Object* devicePropsObj, const ADUC_AgentInfo* agent) { bool success = false; bool configExisted = false; @@ -40,28 +41,23 @@ _Bool DeviceProperties_AddManufacturerAndModel(JSON_Object* devicePropsObj) char* manufacturer = NULL; char* model = NULL; - ADUC_ConfigInfo config = {}; - - if (ADUC_ConfigInfo_Init(&config, ADUC_CONF_FILE_PATH)) + if (agent != NULL && agent->manufacturer != NULL && agent->model != NULL) { - const ADUC_AgentInfo* agent = ADUC_ConfigInfo_GetAgent(&config, 0); - if (agent != NULL && agent->manufacturer != NULL && agent->model != NULL) + configExisted = true; + if (mallocAndStrcpy_s(&manufacturer, agent->manufacturer) != 0) { - configExisted = true; - if (mallocAndStrcpy_s(&manufacturer, agent->manufacturer) != 0) - { - goto done; - } - if (mallocAndStrcpy_s(&model, agent->model) != 0) - { - goto done; - } + goto done; + } + if (mallocAndStrcpy_s(&model, agent->model) != 0) + { + goto done; } } if (!configExisted) { // If file doesn't exist, or value wasn't specified, use build default. + Log_Info("Config file doesn't exist, use build default for manufacturer and model.'"); if (mallocAndStrcpy_s(&manufacturer, ADUC_DEVICEPROPERTIES_MANUFACTURER) != 0) { goto done; @@ -99,24 +95,59 @@ _Bool DeviceProperties_AddManufacturerAndModel(JSON_Object* devicePropsObj) { Log_Error("Failed to get manufacturer and model device properties"); } - ADUC_ConfigInfo_UnInit(&config); return success; } /** - * @brief Adds the interfaceId property into the @p devicePropsObj - * @param devicePropsObj the JSON_Object the interfaceId will be added to + * @brief Clears the interfaceId property from the @p devicePropsObj. + * This function add or update the value of 'interfaceId' property in @p devicePropsObj to null. + * When client reports any property with null value to the IoTHub device (or, module) twin, the property will + * be deleted from the twin. + * + * @param devicePropsObj the JSON_Object the interfaceId will be set to 'null'. + * @returns true on successful addition and false on failure + */ +_Bool DeviceProperties_ClearInterfaceId(JSON_Object* devicePropsObj) +{ + bool success = false; + + JSON_Status jsonStatus = json_object_set_null( + devicePropsObj, + ADUCITF_FIELDNAME_DEVICEPROPERTIES_INTERFACEID); + + if (jsonStatus != JSONSuccess) + { + Log_Error( + "Could not set JSON field '%s' to null", + ADUCITF_FIELDNAME_DEVICEPROPERTIES_INTERFACEID); + goto done; + } + + success = true; +done: + return success; +} + +/** + * @brief Adds the contractModelId property into the @p devicePropsObj + * @param devicePropsObj the JSON_Object the contractModelId will be added to * @returns true on successful addition and false on failure */ -_Bool DeviceProperties_AddInterfaceId(JSON_Object* devicePropsObj) +_Bool DeviceProperties_AddContractModelId(JSON_Object* devicePropsObj) { bool success = false; - JSON_Status jsonStatus = json_object_set_string(devicePropsObj, ADUCITF_FIELDNAME_DEVICEPROPERTIES_INTERFACEID, ADUC_DEVICEPROPERTIES_INTERFACEID); + JSON_Status jsonStatus = json_object_set_string( + devicePropsObj, + ADUCITF_FIELDNAME_DEVICEPROPERTIES_CONTRACT_MODEL_ID, + ADUC_DEVICEPROPERTIES_DEVICEUPDATE_CONTRACT_MODEL_ID); if (jsonStatus != JSONSuccess) { - Log_Error("Could not serialize JSON field: %s value: %s", ADUCITF_FIELDNAME_DEVICEPROPERTIES_INTERFACEID, ADUC_DEVICEPROPERTIES_INTERFACEID); + Log_Error( + "Could not serialize JSON field: %s value: %s", + ADUCITF_FIELDNAME_DEVICEPROPERTIES_CONTRACT_MODEL_ID, + ADUC_DEVICEPROPERTIES_DEVICEUPDATE_CONTRACT_MODEL_ID); goto done; } @@ -174,3 +205,56 @@ _Bool DeviceProperties_AddVersions(JSON_Object* devicePropsObj) free(do_version); return success; } + +/** + * @brief Adds the customized additional device properties to the @p devicePropsObj + * @param devicePropsObj the JSON_Object the additional device properties will be added to + * @param agent the ADUC_AgentInfo that contains the agent info + * @returns true on successful addition and false on failure + */ +_Bool DeviceProperties_AddAdditionalProperties(JSON_Object* devicePropsObj, const ADUC_AgentInfo* agent) +{ + bool success = false; + + // Additional Device Properties is not a mandatory field. If agent is NULL, skip this function + if (agent != NULL) + { + JSON_Object* additional_properties = agent->additionalDeviceProperties; + if (additional_properties == NULL) + { + // No additional properties is set in the configuration file + success = true; + goto done; + } + + size_t propertiesCount = json_object_get_count(additional_properties); + for (size_t i = 0; i < propertiesCount; i++) + { + const char* name = json_object_get_name(additional_properties, i); + const char* val = json_value_get_string(json_object_get_value_at(additional_properties, i)); + + if ((name == NULL) || (val == NULL)) + { + Log_Error( + "Error retrieving the additional device properties name and/or value at element at index=%zu", i); + goto done; + } + + JSON_Status jsonStatus = json_object_set_string(devicePropsObj, name, val); + if (jsonStatus != JSONSuccess) + { + Log_Error("Could not serialize JSON field: %s value: %s", name, val); + goto done; + } + } + } + + success = true; + +done: + if (!success) + { + Log_Error("Failed to get customized additional device properties"); + } + return success; +} diff --git a/src/agent/adu_core_interface/src/device_properties.h b/src/agent/adu_core_interface/src/device_properties.h index c1fc909c7..b5911d4b1 100644 --- a/src/agent/adu_core_interface/src/device_properties.h +++ b/src/agent/adu_core_interface/src/device_properties.h @@ -14,16 +14,33 @@ /** * @brief Adds the aduc_manufacturer and aduc_model to the @p devicePropsObj * @param devicePropsObj the JSON_Object the manufacturer and model will be added to + * @param agent the ADUC_AgentInfo that contains the agent info * @returns true on successful addition and false on failure */ -_Bool DeviceProperties_AddManufacturerAndModel(JSON_Object* devicePropsObj); +_Bool DeviceProperties_AddManufacturerAndModel(JSON_Object* devicePropsObj, const ADUC_AgentInfo* agent); /** - * @brief Adds the interfaceId property into the @p devicePropsObj - * @param devicePropsObj the JSON_Object the interfaceId will be added to + * @brief Adds the customized additional device properties to the @p devicePropsObj + * @param devicePropsObj the JSON_Object the additional device properties will be added to + * @param agent the ADUC_AgentInfo that contains the agent info * @returns true on successful addition and false on failure */ -_Bool DeviceProperties_AddInterfaceId(JSON_Object* devicePropsObj); +_Bool DeviceProperties_AddAdditionalProperties(JSON_Object* devicePropsObj, const ADUC_AgentInfo* agent); + +/** + * @brief Clears the interfaceId property from the @p devicePropsObj + * @param devicePropsObj the JSON_Object the interfaceId will be set to 'null' to remove any + * existing value set by DU Agents older than version 1.0. + * @returns true on successful addition and false on failure + */ +_Bool DeviceProperties_ClearInterfaceId(JSON_Object* devicePropsObj); + +/** + * @brief Adds the contractModelId property into the @p devicePropsObj + * @param devicePropsObj the JSON_Object the contractModelId will be added to + * @returns true on successful addition and false on failure + */ +_Bool DeviceProperties_AddContractModelId(JSON_Object* devicePropsObj); /** * @brief Adds the version properties into the @p devicePropsObj diff --git a/src/agent/adu_core_interface/src/startup_msg_helper.c b/src/agent/adu_core_interface/src/startup_msg_helper.c index 606061694..1dca35975 100644 --- a/src/agent/adu_core_interface/src/startup_msg_helper.c +++ b/src/agent/adu_core_interface/src/startup_msg_helper.c @@ -6,13 +6,12 @@ * Licensed under the MIT License. */ -#include "device_properties.h" #include "startup_msg_helper.h" +#include "device_properties.h" -#include -#include #include #include +#include // ADUCITF_FIELDNAME_DEVICEPROPERTIES, etc. #include /** @@ -23,9 +22,10 @@ /** * @brief Adds the deviceProperties to the @p startupObj * @param startupObj the JSON Object which will have the device properties added to it + * @param agent the ADUC_AgentInfo that contains the agent info * @returns true on successful addition, false on failure */ -_Bool StartupMsg_AddDeviceProperties(JSON_Object* startupObj) +_Bool StartupMsg_AddDeviceProperties(JSON_Object* startupObj, const ADUC_AgentInfo* agent) { if (startupObj == NULL) { @@ -43,12 +43,22 @@ _Bool StartupMsg_AddDeviceProperties(JSON_Object* startupObj) goto done; } - if (!DeviceProperties_AddManufacturerAndModel(devicePropsObj)) + if (!DeviceProperties_AddManufacturerAndModel(devicePropsObj, agent)) + { + goto done; + } + + if (!DeviceProperties_AddAdditionalProperties(devicePropsObj, agent)) + { + goto done; + } + + if (!DeviceProperties_ClearInterfaceId(devicePropsObj)) { goto done; } - if (!DeviceProperties_AddInterfaceId(devicePropsObj)) + if (!DeviceProperties_AddContractModelId(devicePropsObj)) { goto done; } @@ -76,16 +86,16 @@ _Bool StartupMsg_AddDeviceProperties(JSON_Object* startupObj) Log_Error("Adding deviceProperties properties failed."); json_value_free(devicePropsValue); } - return success; } /** * @brief Adds the compatPropertyNames to the @p startupObj * @param startupObj the JSON Object which will have the compatPropertyNames from config added to it + * @param config the ADUC_ConfigInfo that contains the config info * @returns true on successful addition, false on failure */ -_Bool StartupMsg_AddCompatPropertyNames(JSON_Object* startupObj) +_Bool StartupMsg_AddCompatPropertyNames(JSON_Object* startupObj, ADUC_ConfigInfo* config) { if (startupObj == NULL) { @@ -94,19 +104,10 @@ _Bool StartupMsg_AddCompatPropertyNames(JSON_Object* startupObj) _Bool success = false; - ADUC_ConfigInfo config = {}; - - if (!ADUC_ConfigInfo_Init(&config, ADUC_CONF_FILE_PATH)) - { - Log_Warn("Could not initialize config at: %s", ADUC_CONF_FILE_PATH); - } - JSON_Status jsonStatus = json_object_set_string( startupObj, ADUCITF_FIELDNAME_COMPAT_PROPERTY_NAMES, - IsNullOrEmpty(config.compatPropertyNames) - ? DEFAULT_COMPAT_PROPERTY_NAMES_VALUE - : config.compatPropertyNames); + IsNullOrEmpty(config->compatPropertyNames) ? DEFAULT_COMPAT_PROPERTY_NAMES_VALUE : config->compatPropertyNames); if (jsonStatus != JSONSuccess) { @@ -118,7 +119,5 @@ _Bool StartupMsg_AddCompatPropertyNames(JSON_Object* startupObj) done: - ADUC_ConfigInfo_UnInit(&config); - return success; } diff --git a/src/agent/adu_core_interface/src/startup_msg_helper.h b/src/agent/adu_core_interface/src/startup_msg_helper.h index 784abf8aa..e740212af 100644 --- a/src/agent/adu_core_interface/src/startup_msg_helper.h +++ b/src/agent/adu_core_interface/src/startup_msg_helper.h @@ -9,6 +9,7 @@ #ifndef STARTUP_MSG_HELPER_H #define STARTUP_MSG_HELPER_H #include +#include #include #include @@ -17,16 +18,18 @@ EXTERN_C_BEGIN /** * @brief Adds the deviceProperties to the @p startupObj * @param startupObj the JSON Object which will have the device properties added to it + * @param agent the ADUC_AgentInfo that contains the agent info * @returns true on successful addition, false on failure */ -_Bool StartupMsg_AddDeviceProperties(JSON_Object* startupObj); +_Bool StartupMsg_AddDeviceProperties(JSON_Object* startupObj, const ADUC_AgentInfo* agent); /** * @brief Adds the compatPropertyNames to the @p startupObj * @param startupObj the JSON Object which will have the compatPropertyNames from config added to it + * @param config the ADUC_ConfigInfo that contains the config info * @returns true on successful addition, false on failure */ -_Bool StartupMsg_AddCompatPropertyNames(JSON_Object* startupObj); +_Bool StartupMsg_AddCompatPropertyNames(JSON_Object* startupObj, ADUC_ConfigInfo* config); EXTERN_C_END #endif // STARTUP_MSG_HELPER_H diff --git a/src/agent/adu_core_interface/tests/CMakeLists.txt b/src/agent/adu_core_interface/tests/CMakeLists.txt deleted file mode 100644 index c321e82fc..000000000 --- a/src/agent/adu_core_interface/tests/CMakeLists.txt +++ /dev/null @@ -1,65 +0,0 @@ -cmake_minimum_required (VERSION 3.5) - -project (adu_core_interface_unit_tests) - -include (agentRules) -include (aduc_helpers) - -compileasc99 () -disablertti () - -set ( - sources - main.cpp - adu_core_export_helpers_ut.cpp - adu_core_interface_ut.cpp - adu_core_json_ut.cpp - result_ut.cpp - startup_workflowdata_ut.cpp - workflow_reboot_ut.cpp - workflow_replacement_ut.cpp - workflow_test_utils.cpp - workflow_ut.cpp) - -find_package (Catch2 REQUIRED) -find_package (umock_c REQUIRED CONFIG) -find_package (azure_c_shared_utility REQUIRED) -find_package (IotHubClient REQUIRED) -find_package (umqtt REQUIRED) - -add_executable (${PROJECT_NAME} ${sources}) - -target_include_directories (${PROJECT_NAME} PRIVATE ${ADUC_EXPORT_INCLUDES}) - -target_compile_definitions ( - ${PROJECT_NAME} PRIVATE ADUC_VERSION="${ADUC_VERSION}" - ADUC_BUILDER_IDENTIFIER="${ADUC_BUILDER_IDENTIFIER}" - ADUC_TEST_DATA_FOLDER="${ADUC_TEST_DATA_FOLDER}" - ADUC_BUILD_UNIT_TESTS="${ADUC_BUILD_UNIT_TESTS}") - -target_link_libraries ( - ${PROJECT_NAME} - PRIVATE aduc::adu_core_interface - aduc::c_utils aduc::adu_types - aduc::content_handlers - aduc::string_utils - aduc::parser_utils - aduc::workflow_data_utils - aduc::workflow_utils - Catch2::Catch2 - aziotsharedutil - IotHubClient::iothub_client - iothub_client_mqtt_transport - umqtt - umock_c) - -# Ensure that ctest discovers catch2 tests. -# Use catch_discover_tests() rather than add_test() -# See https://github.com/catchorg/Catch2/blob/master/contrib/Catch.cmake -include (CTest) -include (Catch) -catch_discover_tests (${PROJECT_NAME}) - -if (ENABLE_ADU_TELEMETRY_REPORTING) - target_compile_definitions (${PROJECT_NAME} PRIVATE ENABLE_ADU_TELEMETRY_REPORTING) -endif () diff --git a/src/agent/adu_core_interface/tests/adu_core_export_helpers_ut.cpp b/src/agent/adu_core_interface/tests/adu_core_export_helpers_ut.cpp deleted file mode 100644 index c2f70921a..000000000 --- a/src/agent/adu_core_interface/tests/adu_core_export_helpers_ut.cpp +++ /dev/null @@ -1,183 +0,0 @@ -/** - * @file adu_core_export_helpers_ut.cpp - * @brief Unit tests for adu_core_export_helpers.h - * - * @copyright Copyright (c) Microsoft Corp. - * Licensed under the MIT License. - */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "aduc/adu_core_export_helpers.h" -#include "aduc/adu_core_interface.h" -#include "aduc/agent_workflow.h" -#include "aduc/c_utils.h" -#include "aduc/client_handle.h" -#include "aduc/content_handler.hpp" -#include "aduc/result.h" -#include "aduc/workflow_data_utils.h" -#include "aduc/workflow_internal.h" -#include "aduc/workflow_utils.h" - -extern ADUC_ClientHandle g_iotHubClientHandleForADUComponent; - -//NOLINTNEXTLINE(performance-unnecessary-value-param) -static ADUC_Workflow* get_workflow_from_test_data(const std::string path_under_testdata_folder) -{ - auto wf = (ADUC_Workflow*)malloc(sizeof(ADUC_Workflow)); // NOLINT - REQUIRE(wf != nullptr); - - ADUC_Result result = workflow_init_from_file( - (std::string{ ADUC_TEST_DATA_FOLDER } + "/" + path_under_testdata_folder).c_str(), - true, - (void**)&wf); // NOLINT - - REQUIRE(IsAducResultCodeSuccess(result.ResultCode)); - REQUIRE(wf != nullptr); - return wf; -} - -class AduCoreExportHelpersTestCaseFixture -{ -public: - AduCoreExportHelpersTestCaseFixture() - { - const ADUC_Result result{ ADUC_MethodCall_Register(&m_updateActionCallbacks, 0 /*argc*/, nullptr /*argv*/) }; - REQUIRE(IsAducResultCodeSuccess(result.ResultCode)); - REQUIRE(result.ExtendedResultCode == 0); - - m_wf = get_workflow_from_test_data("adu_core_export_helpers/updateManifest.json"); - m_workflowData.WorkflowHandle = m_wf; - m_workflowData.UpdateActionCallbacks = m_updateActionCallbacks; - m_workflowData.ReportStateAndResultAsyncCallback = AzureDeviceUpdateCoreInterface_ReportStateAndResultAsync; - - m_previousDeviceHandle = g_iotHubClientHandleForADUComponent; - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - g_iotHubClientHandleForADUComponent = reinterpret_cast(42); - } - - ~AduCoreExportHelpersTestCaseFixture() - { - if (m_workflowData.WorkflowHandle != nullptr) - { - workflow_free(m_workflowData.WorkflowHandle); - m_workflowData.WorkflowHandle = nullptr; - } - - ADUC_MethodCall_Unregister(&m_updateActionCallbacks); - - CHECK(g_iotHubClientHandleForADUComponent != nullptr); - g_iotHubClientHandleForADUComponent = m_previousDeviceHandle; - } - - AduCoreExportHelpersTestCaseFixture(const AduCoreExportHelpersTestCaseFixture&) = delete; - AduCoreExportHelpersTestCaseFixture& operator=(const AduCoreExportHelpersTestCaseFixture&) = delete; - AduCoreExportHelpersTestCaseFixture(AduCoreExportHelpersTestCaseFixture&&) = delete; - AduCoreExportHelpersTestCaseFixture& operator=(AduCoreExportHelpersTestCaseFixture&&) = delete; - - ADUC_WorkflowData* GetWorkflowData() - { - return &m_workflowData; - } - -private: - ADUC_ClientHandle m_previousDeviceHandle; - - ADUC_UpdateActionCallbacks m_updateActionCallbacks{}; - ADUC_WorkflowData m_workflowData{}; - ADUC_Workflow* m_wf = nullptr; -}; - -TEST_CASE("ADUC_MethodCall_Register and Unregister: Valid") -{ - ADUC_UpdateActionCallbacks updateActionCallbacks{}; - const ADUC_Result result{ ADUC_MethodCall_Register(&updateActionCallbacks, 0 /*argc*/, nullptr /*argv*/) }; - REQUIRE(IsAducResultCodeSuccess(result.ResultCode)); - REQUIRE(result.ExtendedResultCode == 0); - - ADUC_MethodCall_Unregister(&updateActionCallbacks); -} - -TEST_CASE("ADUC_Workflow_MethodCall_Idle - Calls SandboxDestroyCallback") -{ - AduCoreExportHelpersTestCaseFixture fixture; - ADUC_WorkflowData* workflowData = fixture.GetWorkflowData(); - - auto Mock_IdleCallback = [](ADUC_Token token, const char* workflowId) - { - CHECK(std::string("test-workflow-id") == workflowId); - }; - - auto Mock_SandboxDestroyCallback = [](ADUC_Token token, const char* workflowId, const char* workFolder) - { - CHECK(std::string("test-workflow-id") == workflowId); - }; - - workflowData->UpdateActionCallbacks.SandboxDestroyCallback = Mock_SandboxDestroyCallback; - workflowData->UpdateActionCallbacks.IdleCallback = Mock_IdleCallback; - - ADUC_Workflow_MethodCall_Idle(workflowData); - - // ADUC_Workflow_MethodCall_Idle frees and Nulls-out WorkflowHandle - CHECK(workflowData->WorkflowHandle == nullptr); -} - -TEST_CASE("ADUC_Workflow_MethodCall_Download - Fail if not DeploymentInProgress state") -{ - AduCoreExportHelpersTestCaseFixture fixture; - ADUC_WorkflowData* workflowData = fixture.GetWorkflowData(); - - ADUC_WorkflowData_SetLastReportedState(ADUCITF_State_Idle, workflowData); // something other than DeploymentInProgress - - ADUC_MethodCall_Data methodCallData{}; - methodCallData.WorkflowData = workflowData; - ADUC_Result result = ADUC_Workflow_MethodCall_Download(&methodCallData); - CHECK(result.ResultCode == ADUC_Result_Failure); - CHECK(result.ExtendedResultCode == ADUC_ERC_UPPERLEVEL_WORKFLOW_UPDATE_ACTION_UNEXPECTED_STATE); -} - -// NOLINTNEXTLINE(readability-non-const-parameter) -static ADUC_Result Mock_SandboxCreateCallback(ADUC_Token token, const char* workflowId, char* workFolder) -{ - UNREFERENCED_PARAMETER(token); - UNREFERENCED_PARAMETER(workflowId); - UNREFERENCED_PARAMETER(workFolder); - - ADUC_Result result{ADUC_Result_Success}; - return result; -} - -static ADUC_Result Mock_DownloadCallback(ADUC_Token token, const ADUC_WorkCompletionData* workCompletionData, ADUC_WorkflowDataToken workflowData) -{ - UNREFERENCED_PARAMETER(token); - UNREFERENCED_PARAMETER(workCompletionData); - UNREFERENCED_PARAMETER(workflowData); - - ADUC_Result result{ADUC_Result_Success}; - return result; -} - -TEST_CASE("ADUC_Workflow_MethodCall_Download - Calls SandboxCreate and DownloadCallback") -{ - AduCoreExportHelpersTestCaseFixture fixture; - ADUC_WorkflowData* workflowData = fixture.GetWorkflowData(); - - workflowData->UpdateActionCallbacks.SandboxCreateCallback = Mock_SandboxCreateCallback; - workflowData->UpdateActionCallbacks.DownloadCallback = Mock_DownloadCallback; - - ADUC_WorkflowData_SetLastReportedState(ADUCITF_State_DeploymentInProgress, workflowData); // Must be DeploymentInProgress to succeed. - - ADUC_MethodCall_Data methodCallData{}; - methodCallData.WorkflowData = workflowData; - ADUC_Result result = ADUC_Workflow_MethodCall_Download(&methodCallData); - - CHECK(result.ResultCode == ADUC_Result_Success); -} diff --git a/src/agent/adu_core_interface/tests/adu_core_interface_ut.cpp b/src/agent/adu_core_interface/tests/adu_core_interface_ut.cpp deleted file mode 100644 index d8ab00742..000000000 --- a/src/agent/adu_core_interface/tests/adu_core_interface_ut.cpp +++ /dev/null @@ -1,426 +0,0 @@ -/** - * @file adu_core_interface_ut.cpp - * @brief Unit tests for adu_core_interface.h - * - * @copyright Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ -#include - -#include -#include -#include - -#include "aduc/adu_core_export_helpers.h" -#include "aduc/adu_core_exports.h" -#include "aduc/adu_core_interface.h" -#include "aduc/agent_workflow.h" -#include "aduc/hash_utils.h" -#include "aduc/workflow_utils.h" - -#include "aduc/client_handle_helper.h" - -// -// Test Helpers -// - -static void mockIdleCallback(ADUC_Token /*token*/, const char* /*workflowId*/) -{ -} - -static void mockSandboxDestroyCallback(ADUC_Token /*token*/, const char* /*workflowId*/, const char* /*workFolder**/) -{ -} - -static ADUC_Result mockSandboxCreateCallback(ADUC_Token /*token*/, const char* /*workflowId*/, char* /*workFolder*/) -{ - return ADUC_Result{ ADUC_Result_Success }; -} - -static ADUC_Result mockDownloadCallback( - ADUC_Token /*token*/, const ADUC_WorkCompletionData* /*workCompletionData*/, ADUC_WorkflowDataToken /*info*/) -{ - return ADUC_Result{ ADUC_Result_Success }; -} - -static ADUC_Result mockInstallCallback( - ADUC_Token /*token*/, const ADUC_WorkCompletionData* /*workCompletionData*/, ADUC_WorkflowDataToken /*info*/) -{ - return ADUC_Result{ ADUC_Result_Success }; -} - -static ADUC_Result mockApplyCallback( - ADUC_Token /*token*/, const ADUC_WorkCompletionData* /*workCompletionData*/, ADUC_WorkflowDataToken /*info*/) -{ - return ADUC_Result{ ADUC_Result_Success }; -} - -static std::string escaped(const std::string& input) -{ - std::string output; - output.reserve(input.size()); - for (const char c : input) - { - // more cases can be added here if we can to escape certain characters - switch (c) - { - case '{': - output += "\\{"; - break; - case '}': - output += "\\}"; - break; - case '+': - output += "\\+"; - break; - case '\\': - output += "\\\\"; - break; - default: - output += c; - break; - } - } - return output; -} - -static ADUC_Result mockIsInstalledCallback(ADUC_Token /*token*/, ADUC_WorkflowDataToken /*workflowDataconst*/) -{ - return ADUC_Result{ ADUC_Result_IsInstalled_Installed }; -} - -class ADUC_UT_ReportPropertyAsyncValues -{ -public: - void - set(ADUC_ClientHandle deviceHandleIn, - const unsigned char* reportedStateIn, - size_t reportedStateLenIn, - IOTHUB_CLIENT_REPORTED_STATE_CALLBACK reportedStateCallbackIn, - void* userContextCallbackIn) - { - deviceHandle = deviceHandleIn; - - // we interpret the octets to be bytes of a string as it is a json string (utf-8 or ascii is fine). - // copy the bytes into the std::string wholesale which is bitwise correct. - std::string reportedState; - reportedState.clear(); - reportedState.reserve(reportedStateLenIn); - - // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) - std::for_each(reportedStateIn, reportedStateIn + reportedStateLenIn, [&](unsigned char c) { - reportedState.push_back(c); - }); - - reportedStates.push_back(reportedState); - - reportedStateCallback = reportedStateCallbackIn; - userContextCallback = userContextCallbackIn; - } - - std::vector reportedStates; - ADUC_ClientHandle deviceHandle{ nullptr }; - IOTHUB_CLIENT_REPORTED_STATE_CALLBACK reportedStateCallback{ nullptr }; - const void* userContextCallback{ nullptr }; -} g_SendReportedStateValues; - -static IOTHUB_CLIENT_RESULT mockClientHandle_SendReportedState( - ADUC_ClientHandle deviceHandle, - const unsigned char* reportedState, - size_t reportedStateLen, - IOTHUB_CLIENT_REPORTED_STATE_CALLBACK reportedStateCallback, - void* userContextCallback) -{ - g_SendReportedStateValues.set( - deviceHandle, reportedState, reportedStateLen, reportedStateCallback, userContextCallback); - - return IOTHUB_CLIENT_OK; -} - -class TestCaseFixture -{ -public: - TestCaseFixture() - { - m_previousDeviceHandle = g_iotHubClientHandleForADUComponent; - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - g_iotHubClientHandleForADUComponent = reinterpret_cast(42); - } - - ~TestCaseFixture() - { - g_iotHubClientHandleForADUComponent = m_previousDeviceHandle; - } - - TestCaseFixture(const TestCaseFixture&) = delete; - TestCaseFixture& operator=(const TestCaseFixture&) = delete; - TestCaseFixture(TestCaseFixture&&) = delete; - TestCaseFixture& operator=(TestCaseFixture&&) = delete; - -private: - ADUC_ClientHandle m_previousDeviceHandle; -}; - -// -// Test Cases -// - -TEST_CASE_METHOD(TestCaseFixture, "AzureDeviceUpdateCoreInterface_Create") -{ - void* context = nullptr; - - CHECK(AzureDeviceUpdateCoreInterface_Create(&context, 0 /*argc*/, nullptr /*argv*/)); - CHECK(context != nullptr); - - AzureDeviceUpdateCoreInterface_Destroy(&context); -} - -const char* action_bundle_deployment = - R"( { )" - R"( "updateManifest": "{\"manifestVersion\":\"2.0\",\"updateId\":{\"provider\":\"Contoso\",\"name\":\"VacuumBundleUpdate\",\"version\":\"1.0\"},\"updateType\":\"microsoft/bundle:1\",\"installedCriteria\":\"1.0\",\"files\":{\"00000\":{\"fileName\":\"contoso-motor-1.0-updatemanifest.json\",\"sizeInBytes\":1396,\"hashes\":{\"sha256\":\"E2o94XQss/K8niR1pW6OdaIS/y3tInwhEKMn/6Rw1Gw=\"}}},\"createdDateTime\":\"2021-06-07T07:25:59.0781905Z\"}", )" - R"( "updateManifestSignature": "eyJhbGciOiJSUzI1NiIsInNqd2siOiJleUpoYkdjaU9pSlNVekkxTmlJc0ltdHBaQ0k2SWtGRVZTNHlNREEzTURJdVVpSjkuZXlKcmRIa2lPaUpTVTBFaUxDSnVJam9pY2toV1FrVkdTMUl4ZG5Ob1p5dEJhRWxuTDFORVVVOHplRFJyYWpORFZWUTNaa2R1U21oQmJYVkVhSFpJWm1velowaDZhVEJVTWtsQmNVTXhlREpDUTFka1QyODFkamgwZFcxeFVtb3ZibGx3WnprM2FtcFFRMHQxWTJSUE5tMHpOMlJqVDIxaE5EWm9OMDh3YTBod2Qwd3pibFZJUjBWeVNqVkVRUzloY0ZsdWQwVmxjMlY0VkdwVU9GTndMeXRpVkhGWFJXMTZaMFF6TjNCbVpFdGhjV3AwU0V4SFZtbFpkMVpJVUhwMFFtRmlkM2RxYUVGMmVubFNXUzk1T1U5bWJYcEVabGh0Y2xreGNtOHZLekpvUlhGRmVXdDFhbmRSUlZscmFHcEtZU3RDTkRjMkt6QnRkVWQ1VjBrMVpVbDJMMjlzZERKU1pWaDRUV0k1VFd4c1dFNTViMUF6WVU1TFNVcHBZbHBOY3pkMVMyTnBkMnQ1YVZWSllWbGpUV3B6T1drdlVrVjVLMnhOT1haSlduRnlabkJEVlZoMU0zUnVNVXRuWXpKUmN5OVVaRGgwVGxSRFIxWTJkM1JXWVhGcFNYQlVaRlEwVW5KRFpFMXZUelZUVG1WbVprUjVZekpzUXpkMU9EVXJiMjFVYTJOcVVHcHRObVpoY0dSSmVVWXljV1Z0ZGxOQ1JHWkNOMk5oYWpWRVNVa3lOVmQzTlVWS1kyRjJabmxRTlRSdGNVNVJVVE5IWTAxUllqSmtaMmhwWTJ4d2FsbHZLelF6V21kWlEyUkhkR0ZhWkRKRlpreGFkMGd6VVdjeWNrUnNabXN2YVdFd0x6RjVjV2xyTDFoYU1XNXpXbFJwTUVKak5VTndUMDFGY1daT1NrWlJhek5DVjI5Qk1EVnlRMW9pTENKbElqb2lRVkZCUWlJc0ltRnNaeUk2SWxKVE1qVTJJaXdpYTJsa0lqb2lRVVJWTGpJd01EY3dNaTVTTGxNaWZRLmlTVGdBRUJYc2Q3QUFOa1FNa2FHLUZBVjZRT0dVRXV4dUhnMllmU3VXaHRZWHFicE0takk1UlZMS2VzU0xDZWhLLWxSQzl4Ni1fTGV5eE5oMURPRmMtRmE2b0NFR3dVajh6aU9GX0FUNnM2RU9tY2txUHJ4dXZDV3R5WWtrRFJGNzRkdGFLMWpOQTdTZFhyWnp2V0NzTXFPVU1OejBnQ29WUjBDczEyNTRrRk1SbVJQVmZFY2pnVDdqNGxDcHlEdVdncjlTZW5TZXFnS0xZeGphYUcwc1JoOWNkaTJkS3J3Z2FOYXFBYkhtQ3JyaHhTUENUQnpXTUV4WnJMWXp1ZEVvZnlZSGlWVlJoU0pwajBPUTE4ZWN1NERQWFYxVGN0MXkzazdMTGlvN244aXpLdXEybTNUeEY5dlBkcWI5TlA2U2M5LW15YXB0cGJGcEhlRmtVTC1GNXl0bF9VQkZLcHdOOUNMNHdwNnlaLWpkWE5hZ3JtVV9xTDFDeVh3MW9tTkNnVG1KRjNHZDNseXFLSEhEZXJEcy1NUnBtS2p3U3dwWkNRSkdEUmNSb3ZXeUwxMnZqdzNMQkpNaG1VeHNFZEJhWlA1d0dkc2ZEOGxkS1lGVkZFY1owb3JNTnJVa1NNQWw2cEl4dGVmRVhpeTVscW1pUHpxX0xKMWVSSXJxWTBfIn0.eyJzaGEyNTYiOiI3alo1YWpFN2Z5SWpzcTlBbWlKNmlaQlNxYUw1bkUxNXZkL0puVWgwNFhZPSJ9.EK5zcNiEgO2rHh_ichQWlDIvkIsPXrPMQK-0D5WK8ZnOR5oJdwhwhdpgBaB-tE-6QxQB1PKurbC2BtiGL8HI1DgQtL8Fq_2ASRfzgNtrtpp6rBiLRynJuWCy7drgM6g8WoSh8Utdxsx5lnGgAVAU67ijK0ITd0E70R7vWJRmY8YxxDh-Sh8BNz68pvU-YJQwKtVy64lD5zA0--BL432F-uZWTc6n-BduQdSB4J7Eu6zGlT75s8Ehd-SIylsstu4wdypU0tcwIH-MaSKcH5mgEmokaHncJrb4zKnZwxYQUeDMoFjF39P9hDmheHywY1gwYziXjUcnMn8_T00oMeycQ7PDCTJHIYB3PGbtM9KiA3RQH-08ofqiCVgOLeqbUHTP03Z0Cx3e02LzTgP8_Lerr4okAUPksT2IGvvsiMtj04asdrLSlv-AvFud-9U0a2mJEWcosI04Q5NAbqhZ5ZBzCkkowLGofS04SnfS-VssBfmbH5ue5SWb-AxBv1inZWUj", )" - R"( "workflow": { )" - R"( "id": "action_bundle", )" - R"( "action": 3 )" - R"( }, )" - R"( "fileUrls": { )" - R"( "00000": "file:///tmp/tests/testfiles/contoso-motor-1.0-updatemanifest.json", )" - R"( "00001": "file:///tmp/tests/testfiles/contoso-motor-1.0-installer", )" - R"( "gw001": "file:///tmp/tests/testfiles/behind-gateway-info.json" )" - R"( } )" - R"( } )"; - -const char* action_bundle_cancel = R"( { )" - R"( "updateManifest": "", )" - R"( "updateManifestSignature": "", )" - R"( "workflow": { )" - R"( "id": "action_bundle", )" - R"( "action": 255 )" - R"( } )" - R"( } )"; - -TEST_CASE_METHOD(TestCaseFixture, "AzureDeviceUpdateCoreInterface_Connected") -{ - g_SendReportedStateValues.reportedStates.clear(); - - // Init workflow. - ADUC_WorkflowData workflowData{}; - ADUC_WorkflowHandle bundle = nullptr; - ADUC_Result result = workflow_init(action_bundle_deployment, false, &bundle); - CHECK(bundle != nullptr); - - workflowData.WorkflowHandle = bundle; - CHECK(result.ResultCode != 0); - - ADUC_TestOverride_Hooks testHooks = {}; - testHooks.ClientHandle_SendReportedStateFunc_TestOverride = (void*)mockClientHandle_SendReportedState; // NOLINT - workflowData.TestOverrides = &testHooks; - - // Typically Register would initialize the IdleCallback. - workflowData.UpdateActionCallbacks.IdleCallback = mockIdleCallback; - // Typically Register would initialize the DownloadCallback. - workflowData.UpdateActionCallbacks.DownloadCallback = mockDownloadCallback; - // Typically Register would initialize the InstallCallback. - workflowData.UpdateActionCallbacks.InstallCallback = mockInstallCallback; - // Typically Register would initialize the ApplyCallback. - workflowData.UpdateActionCallbacks.ApplyCallback = mockApplyCallback; - // Typically Register would initialize the IsInstalledCallback. - workflowData.UpdateActionCallbacks.IsInstalledCallback = mockIsInstalledCallback; - // Typically Register would initialize the SandboxDestroyCallback. - workflowData.UpdateActionCallbacks.SandboxDestroyCallback = mockSandboxDestroyCallback; - // Typically Register would initialize the SandboxCreateCallback. - workflowData.UpdateActionCallbacks.SandboxCreateCallback = mockSandboxCreateCallback; - - AzureDeviceUpdateCoreInterface_Connected(&workflowData); - - int lastReportedState = workflowData.LastReportedState; - - // The expected reported state when Agent orchestration of all workflow steps is Idle. - CHECK(lastReportedState == ADUCITF_State_Idle); - - // NOTE: If AzureDeviceUpdateInterface_Connected() is called when the workflowData->WorkflowHandle is already set, - // the AzureDeviceUpdateCoreInterface_Connected function will not call ADUC_Workflow_HandleStartupWorkflowData(). - // Hence, at this point, there's no guarantees that workflowData.StartupIdleCallSent is 'true'. - // - // CHECK(workflowData.StartupIdleCallSent); - // - CHECK(!workflow_get_operation_in_progress(workflowData.WorkflowHandle)); - CHECK(!workflow_get_operation_cancel_requested(workflowData.WorkflowHandle)); -} - -TEST_CASE_METHOD(TestCaseFixture, "AzureDeviceUpdateCoreInterface_ReportStateAndResultAsync") -{ - SECTION("DeploymentInProgress") - { - g_SendReportedStateValues.reportedStates.clear(); - - ADUC_Result result{ ADUC_Result_DeploymentInProgress_Success, 0 }; - // Init workflow. - ADUC_WorkflowData workflowData{}; - workflowData.CurrentAction = ADUCITF_UpdateAction_ProcessDeployment; - - ADUC_WorkflowHandle bundle = nullptr; - result = workflow_init(action_bundle_cancel, false, &bundle); - workflowData.WorkflowHandle = bundle; - CHECK(result.ResultCode != 0); - - ADUC_TestOverride_Hooks testHooks = {}; - testHooks.ClientHandle_SendReportedStateFunc_TestOverride = (void*)mockClientHandle_SendReportedState; // NOLINT - workflowData.TestOverrides = &testHooks; - - const ADUCITF_State updateState = ADUCITF_State_DeploymentInProgress; - REQUIRE(AzureDeviceUpdateCoreInterface_ReportStateAndResultAsync( - &workflowData, updateState, &result, nullptr /* installedUpdateId */)); - - CHECK(g_SendReportedStateValues.deviceHandle != nullptr); - std::stringstream strm; - - // clang-format off - strm << R"({)" - << R"("deviceUpdate":{)" - << R"("__t":"c",)" - << R"("agent":{)" - << R"("lastInstallResult":{)" - << R"("stepResults":null,)" - << R"("resultCode":)" << static_cast(result.ResultCode) << R"(,)" - << R"("extendedResultCode":0,)" - << R"("resultDetails":"")" - << R"(},)" - << R"("state":)" << static_cast(updateState) << R"(,)" - << R"("workflow":{)" - << R"("action":)" << static_cast(workflowData.CurrentAction) << R"(,)" - << R"("id":"action_bundle")" - << R"(})" - << R"(})" - << R"(})" - << R"(})"; - // clang-format on - CHECK(g_SendReportedStateValues.reportedStates[0] == strm.str()); - CHECK(g_SendReportedStateValues.reportedStateCallback != nullptr); - CHECK(g_SendReportedStateValues.userContextCallback == nullptr); - } - - SECTION("Failed") - { - g_SendReportedStateValues.reportedStates.clear(); - - ADUC_Result result; - - const ADUCITF_State updateState = ADUCITF_State_Failed; - - // Init workflow. - ADUC_WorkflowData workflowData{}; - workflowData.CurrentAction = ADUCITF_UpdateAction_ProcessDeployment; - - ADUC_WorkflowHandle bundle = nullptr; - result = workflow_init(action_bundle_cancel, false, &bundle); - workflowData.WorkflowHandle = bundle; - CHECK(result.ResultCode != 0); - - ADUC_TestOverride_Hooks testHooks = {}; - testHooks.ClientHandle_SendReportedStateFunc_TestOverride = (void*)mockClientHandle_SendReportedState; // NOLINT - workflowData.TestOverrides = &testHooks; - - result = { ADUC_Result_Failure, ADUC_ERC_NOTPERMITTED }; - REQUIRE(AzureDeviceUpdateCoreInterface_ReportStateAndResultAsync( - &workflowData, updateState, &result, nullptr /* installedUpdateId */)); - - CHECK(g_SendReportedStateValues.deviceHandle != nullptr); - std::stringstream strm; - - // clang-format off - strm << R"({)" - << R"("deviceUpdate":{)" - << R"("__t":"c",)" - << R"("agent":{)" - << R"("lastInstallResult":{)" - << R"("resultCode":)" << static_cast(ADUC_Result_Failure) << R"(,)" - << R"("extendedResultCode":)"<< ADUC_ERC_NOTPERMITTED << R"(,)" - << R"("resultDetails":"")" - << R"(},)" - << R"("state":)" << static_cast(updateState) << R"(,)" - << R"("workflow":{)" - << R"("action":)" << static_cast(workflowData.CurrentAction) << R"(,)" - << R"("id":"action_bundle")" - << R"(})" - << R"(})" - << R"(})" - << R"(})"; - // clang-format on - CHECK(g_SendReportedStateValues.reportedStates[0] == strm.str()); - CHECK(g_SendReportedStateValues.reportedStateCallback != nullptr); - CHECK(g_SendReportedStateValues.userContextCallback == nullptr); - } -} - -TEST_CASE_METHOD(TestCaseFixture, "AzureDeviceUpdateCoreInterface_ReportContentIdAndIdleAsync") -{ - g_SendReportedStateValues.reportedStates.clear(); - - ADUC_Result result; - - const std::string provider{ "Microsoft" }; - const std::string name{ "adu" }; - const std::string version{ "1.2.3.4" }; - - std::stringstream installedUpdateIdStr; - - // clang-format off - installedUpdateIdStr - << R"({)" - << R"(\"provider\":\")" << provider << R"(\",)" - << R"(\"name\":\")"<< name << R"(\",)" - << R"(\"version\":\")" << version << R"(\")" - << R"(})"; - // clang-format on - - ADUC_UpdateId* updateId = ADUC_UpdateId_AllocAndInit(provider.c_str(), name.c_str(), version.c_str()); - CHECK(updateId != nullptr); - - // Init workflow since workflow needs a valid workflow handle to get the workflow id. - ADUC_WorkflowData workflowData{}; - workflowData.CurrentAction = ADUCITF_UpdateAction_ProcessDeployment; - - ADUC_WorkflowHandle bundle = nullptr; - result = workflow_init(action_bundle_deployment, false, &bundle); - workflowData.WorkflowHandle = bundle; - CHECK(result.ResultCode != 0); - - ADUC_TestOverride_Hooks testHooks = {}; - testHooks.ClientHandle_SendReportedStateFunc_TestOverride = (void*)mockClientHandle_SendReportedState; // NOLINT - workflowData.TestOverrides = &testHooks; - - ADUC_Result idleResult = { .ResultCode = ADUC_Result_Apply_Success, .ExtendedResultCode = 0 }; - // Report Idle state and Update Id to service. - REQUIRE(AzureDeviceUpdateCoreInterface_ReportStateAndResultAsync((ADUC_WorkflowDataToken)&workflowData, ADUCITF_State_Idle, &idleResult, installedUpdateIdStr.str().c_str())); - - CHECK(g_SendReportedStateValues.deviceHandle != nullptr); - - std::stringstream strm; - // clang-format off - strm << R"({)" - << R"("deviceUpdate":{)" - << R"("__t":"c",)" - << R"("agent":{)" - << R"("lastInstallResult":{)" - << R"("resultCode":700,)" - << R"("extendedResultCode":0,)" - << R"("resultDetails":"")" - << R"(},)" - << R"("state":)" << static_cast(ADUCITF_State_Idle) << R"(,)" - << R"("workflow":{)" - << R"("action":)" << static_cast(workflowData.CurrentAction) << R"(,)" - << R"("id":"action_bundle")" - << R"(})" - << R"(,)" - << R"("installedUpdateId":"{\\\"provider\\\":\\\"Microsoft\\\",\\\"name\\\":\\\"adu\\\",\\\"version\\\":\\\"1.2.3.4\\\"}")" - << R"(})" - << R"(})" - << R"(})"; - // clang-format on - CHECK(g_SendReportedStateValues.reportedStates[0] == strm.str()); - CHECK(g_SendReportedStateValues.reportedStateCallback != nullptr); - CHECK(g_SendReportedStateValues.userContextCallback == nullptr); - - ADUC_UpdateId_UninitAndFree(updateId); -} diff --git a/src/agent/adu_core_interface/tests/adu_core_json_ut.cpp b/src/agent/adu_core_interface/tests/adu_core_json_ut.cpp deleted file mode 100644 index cfba5e5cb..000000000 --- a/src/agent/adu_core_interface/tests/adu_core_json_ut.cpp +++ /dev/null @@ -1,634 +0,0 @@ -/** - * @file adu_core_json_ut.cpp - * @brief Unit tests for adu_core_json.h - * - * @copyright Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ -#include -using Catch::Matchers::Equals; - -#include -#include -#include -#include - -#include "aduc/adu_core_export_helpers.h" // ADUC_FileEntityArray_Free -#include "aduc/adu_core_exports.h" // ADUC_FileEntity -#include "aduc/adu_core_json.h" -#include "aduc/hash_utils.h" -#include "aduc/parser_utils.h" -#include - -using ADUC::StringUtils::cstr_wrapper; - -TEST_CASE("ADUCITF_StateToString: Valid") -{ - // clang-format off - const std::map files{ - { ADUCITF_State_Idle, "Idle" }, - { ADUCITF_State_DownloadStarted, "DownloadStarted" }, - { ADUCITF_State_DownloadSucceeded, "DownloadSucceeded" }, - { ADUCITF_State_InstallStarted, "InstallStarted" }, - { ADUCITF_State_InstallSucceeded, "InstallSucceeded" }, - { ADUCITF_State_ApplyStarted, "ApplyStarted" }, - { ADUCITF_State_Failed, "Failed" } - }; - // clang-format on - for (const auto& val : files) - { - CHECK(ADUCITF_StateToString(val.first) == val.second); - } -} - -TEST_CASE("ADUCITF_StateToString: Invalid") -{ - CHECK_THAT(ADUCITF_StateToString((ADUCITF_State)(65535)), Equals("")); -} - -TEST_CASE("ADUC_Json_GetRoot: Valid") -{ - CHECK(ADUC_Json_GetRoot(R"({ })") != nullptr); - - CHECK(ADUC_Json_GetRoot(R"({ "a": "42" })") != nullptr); -} - -TEST_CASE("ADUC_Json_GetRoot: Invalid") -{ - SECTION("Invalid JSON returns nullptr") - { - CHECK(ADUC_Json_GetRoot(R"(Not JSON)") == nullptr); - } - - SECTION("Invalid root object returns nullptr") - { - CHECK(ADUC_Json_GetRoot(R"(["a"])") == nullptr); - } -} - -/* - * Example Manifest with Signature: - { - "updateManifest": "{ - "manifestVersion":"1.0", - "updateId":{ - "provider":"AduTest", - "name":"ADU content team", - "version":"2020.611.534.16" - }, - "updateType":null, - "installedCriteria":null, - "files":{ - "00000":{ - "fileName":"setup.exe", - "sizeInBytes":76, - "hashes":{ - "sha256":"IhIIxBJpLfazQOk/PVi6SzR7BM0jf4HDqw+6gdZ3vp8=" - } - } - }, - "createdDateTime":"2020-06-12T00:38:13.9350278" - }", - "fileUrls":{ - "00000": "" - } - "updateManifestSignature": "ey...mw" -} -*/ - -TEST_CASE("ADUC_Json_GetRootAndValidateManifest: Valid Signature Value") -{ - std::stringstream manifest_json_strm; - // clang-format off - manifest_json_strm - << R"({)" - << R"("updateManifest":)" - << R"("{)" - << R"(\"manifestVersion\":\"2.0\",)" - << R"(\"updateId\":{)" - << R"(\"provider\":\"adu\",)" - << R"(\"name\":\"test\",)" - << R"(\"version\":\"2.0.0.0\")" - << R"(},)" - << R"(\"updateType\":\"SWUpdate\",)" - << R"(\"installedCriteria\":\"1.2.3.4\",)" - << R"(\"files\":{)" - << R"(\"00000\":{)" - << R"(\"fileName\":\"setup.exe\",)" - << R"(\"sizeInBytes\":76,)" - << R"(\"hashes\":{)" - << R"(\"sha256\":\"aGc0x9DB3CFgRwPYgX2kQsN0oUsS4/Zn4kP//f3QyLc=\")" - << R"(})" - << R"(})" - << R"(},)" - << R"(\"createdDateTime\":\"2020-10-02T22:15:56.8012002Z\")" - << R"(}")" - << R"(,)" - << R"("updateManifestSignature":")" - << R"(eyJhbGciOiJSUzI1NiIsInNqd2siOiJleUpoYkdjaU9pSlNVekkxTmlJc0ltdHBaQ0)" - << R"(k2SWtGRVZTNHlNREEzTURJdVVpNVVJbjAuZXlKcmRIa2lPaUpTVTBFaUxDSnVJam9p)" - << R"(ZGtwYWJGRjFjM1J3T1RsUFRpOXJXWEJ4TlVveFRtdG1hMGxDTjNrdldVbzJNbWRrVj)" - << R"(JWRFlteE5XWGt3WjNRd1VVVjZXVGR4TjI1UFJHMDJTbXQxYldoTVpuTTNaa1JCVTA4)" - << R"(elJIaG5SVlE0Y1dkWmQyNVdhMFpsTVVkVVowZEVXR28xVTFGQ2VIQkxVRWxQTlVRcl)" - << R"(Z6TXdSSEpKTmxGSk4xRm1hVFpRWmxaT2FsVlVTRkI2U0VaNWFFMVRUVnByYWxreGVF)" - << R"(eFBNMDFSWjBobmRXUjFNV1pzU0hGREszUmlhbms1WTI0emVtcDBNM2hUVWpKbU9URk)" - << R"(RVa0p5VWxRelJ6Vm1WbFkxVjA1U1EwOWlWVU51YUdNeWFHaHlSRU15V0dreFFWaHli)" - << R"(azlRZDNWRFJFbEZkSFIzYjBzd1QyNVFjV3BLVHk5SVVqRjJMelJPY3l0S1YwOU1VRT)" - << R"(U0ZW1aRlNqSnBXVTlSWkdwbFpteEJPRVoyVFhWMGVYVndaVXRwUTJsclYzY3lOWFpw)" - << R"(YjAxUldWSXdiMVozUjFwMU16aG1PSFJvTVVoRlZFSjJkVVZWTDFkeGVIQllXbVJQUm)" - << R"(5wa09VTkhabVZsTW5FMGIzcFFNQzlQVUUxRWQxQlRNRzVZWm0xYVVHUlhhWHBwVDNW)" - << R"(RlUwSndVRko1TjFwcFJYSjVSbVpNWjJnemNVWkVXQ3RYZDB3eFdXWmxaRzU1V1N0cF)" - << R"(VWWjNiaXRpVTNWalVHeFRlRXdyUmxJNVNuZFFaRGhNZUU1bmFHUnZZbTFCTjA1MmNX)" - << R"(SkNaakp6Wld0dE1HUkpTa0pRTm5wc2MxQmxURU5ZVW1VMU1IbE9SVk5rYUhWRlFuVn)" - << R"(hNV1k0Y1ZZek1VVklSVmx6V0dRMGMwWm5WMVZ2Wm5kM1dIaFBTM0FpTENKbElqb2lR)" - << R"(VkZCUWlJc0ltRnNaeUk2SWxKVE1qVTJJaXdpYTJsa0lqb2lRVVJWTGpJd01EY3dNaT)" - << R"(VTTGxOVUluMC5RS0ktckVJSXBIZTRPZVd1dkN4cFNYam1wbC05czNlN1d2ZGRPcFNn)" - << R"(R2dJaUN5T1hPYThjSFdlbnpzSFVMTUZBZEF0WVBnM2ZVTnl3QUNJOEI2U044UGhUc3)" - << R"(E5TFg4QjR2c0l2QlZ2S2FrTU1ucjRuZXRlVHN5V0NJbEhBeG4ydThzSlQ3amVzMmtF)" - << R"(Q2ZrNnpBeS16cElSSXhYaF8yVEl5Y2RNLVU0UmJYVDhwSGpPUTVfZTRnSFRCZjRlOD)" - << R"(RaWTdsUEw0RGtTa3haR1dnaERhOTFGVjlXbjRtc050eDdhMEJ3NEtTMURVd1c2eXE0)" - << R"(c2diTDFncVl6VG04bENaSDJOb1pDdjZZUUd4YVE5UkJ1ek5Eb0VMb0RXXzB4S3V1Vz)" - << R"(Faa3UxSDN6c3lZNlNJMVp0bkFkdDAxcmxIck5fTTlKdTBZM3EyNzJiRWxWSnVnN3F6)" - << R"(cGF5WXFucllOMTBHRWhIUW9KTkRxZnlPSHNLRG9FbTNOR1pKY2FSWVl4eHY2SVZxTF)" - << R"(JDaFZidnJ3ekdkTXdmME95aE52THFGWlhMS1RWMmhPX2t6MXZWaVNwRi1PVXVWbW5u)" - << R"(RC1ZMXFQb28xSTBJeVFYallxaGNlME01UkhiQ1drVk1Qbk5jZHFMQzZlTWhiTjJGT1)" - << R"(pHSUxSOVRqcDIyMmxucGFTbnVmMHlVbDF4MiJ9.eyJzaGEyNTYiOiJjTGRjZnBBRnN)" - << R"(yYjA3ak5zVVFzQ0hDZDBmb0J3RU5remQybTB1UWZMdGc4PSJ9.ghH76UBj3uPgpfYP)" - << R"(3Px24QUB0ZmAu9BthyL90CwkAb7TRM6yjCMxU7V7P2YERa_ATnjKygTefSzxpzc1c7)" - << R"(B86X7DyLNInp3TvRktPf6PWGMbZ_O2l7XeUNfhhJwUTsFok405AK6rWBSt49MpnKbZ)" - << R"(yEss-dVEFddun1Zr71InV2gA_BtdhkP3qS7y16zp3SpLi1eqyxgAL5hGddam7jt0pL)" - << R"(XW-ds60n3zBivFJFzhbJPqanmw6VaWtgwT7c5Z437A2xMBSdfEjSmZoZgIqgE08Pub)" - << R"(n0afZZFptE4RnuSPhqqIfusuVb4uIzr5qdZUCG2qUAN4osM6raMr5m5JF4fjvG7lCE)" - << R"(FYhtfMW7qsCEmuaX2UNWeUlDEvSUHzupysWgBwIK8a-0AoUPh56Wm_m7N-Ppj3mqhk)" - << R"(xY6qK4CcucML5VevRhUr8onQHmtp8Iwv5y2THnEWh6cSnFp9qSwexDPItdk8iwclw6)" - << R"(SsmWpv3LpsuAlMHQhrfaqERBthyp-zdvr1)" - << R"("})"; - // clang-format on - - INFO(manifest_json_strm.str()); - - const JSON_Value* downloadJsonValue{ ADUC_Json_GetRoot(manifest_json_strm.str().c_str()) }; - - CHECK(downloadJsonValue != nullptr); - - CHECK(ADUC_Json_ValidateManifest(downloadJsonValue)); -} - -TEST_CASE("ADUC_Json_GetRootAndValidateManifest: Invalid Signature Value") -{ - // Note: The signature has been edited to be invalid - std::stringstream manifest_json_strm; - // clang-format off - manifest_json_strm - << R"({)" - << R"("updateManifest":)" - << R"("{)" - << R"(\"manifestVersion\":\"2.0\",)" - << R"(\"updateId\":{)" - << R"(\"provider\":\"adu\",)" - << R"(\"name\":\"test\",)" - << R"(\"version\":\"2.0.0.0\")" - << R"(},)" - << R"(\"updateType\":\"SWUpdate\",)" - << R"(\"installedCriteria\":\"1.2.3.4\",)" - << R"(\"files\":{)" - << R"(\"00000\":{)" - << R"(\"fileName\":\"setup.exe\",)" - << R"(\"sizeInBytes\":76,)" - << R"(\"hashes\":{)" - << R"(\"sha256\":\"fSBQtjHq+MYBf3zLcISd/7rLi9lWu/khv/2yAhzZkxU=\")" - << R"(})" - << R"(})" - << R"(},)" - << R"(\"createdDateTime\":\"2020-08-25T23:37:21.3102772\")" - << R"(}")" - << R"(,)" - << R"("updateManifestSignature":")" - << R"(eyJhbGciOiJSUzI1NiIsInNqd2siOiJleUpoYkdjaU9pSlNVekkxTmlJc0ltdHBaQ0)" - << R"(k2SWtGRVZTNHlNREEzTURJdVVpSjkuZXlKcmRIa2lPaUpTVTBFaUxDSnVJam9pY2to)" - << R"(V1FrVkdTMUl4ZG5Ob1p5dEJhRWxuTDFORVVVOHplRFJyYWpORFZWUTNaa2R1U21oQm)" - << R"(JYVkVhSFpJWm1velowaDZhVEJVTWtsQmNVTXhlREpDUTFka1QyODFkamgwZFcxeFVt)" - << R"(b3ZibGx3WnprM2FtcFFRMHQxWTJSUE5tMHpOMlJqVDIxaE5EWm9OMDh3YTBod2Qwd3)" - << R"(pibFZJUjBWeVNqVkVRUzloY0ZsdWQwVmxjMlY0VkdwVU9GTndMeXRpVkhGWFJXMTZa)" - << R"(MFF6TjNCbVpFdGhjV3AwU0V4SFZtbFpkMVpJVUhwMFFtRmlkM2RxYUVGMmVubFNXUz)" - << R"(k1T1U5bWJYcEVabGh0Y2xreGNtOHZLekpvUlhGRmVXdDFhbmRSUlZscmFHcEtZU3RD)" - << R"(TkRjMkt6QnRkVWQ1VjBrMVpVbDJMMjlzZERKU1pWaDRUV0k1VFd4c1dFNTViMUF6WV)" - << R"(U1TFNVcHBZbHBOY3pkMVMyTnBkMnQ1YVZWSllWbGpUV3B6T1drdlVrVjVLMnhOT1ha)" - << R"(SlduRnlabkJEVlZoMU0zUnVNVXRuWXpKUmN5OVVaRGgwVGxSRFIxWTJkM1JXWVhGcF)" - << R"(NYQlVaRlEwVW5KRFpFMXZUelZUVG1WbVprUjVZekpzUXpkMU9EVXJiMjFVYTJOcVVH)" - << R"(cHRObVpoY0dSSmVVWXljV1Z0ZGxOQ1JHWkNOMk5oYWpWRVNVa3lOVmQzTlVWS1kyRj)" - << R"(JabmxRTlRSdGNVNVJVVE5IWTAxUllqSmtaMmhwWTJ4d2FsbHZLelF6V21kWlEyUkhk)" - << R"(R0ZhWkRKRlpreGFkMGd6VVdjeWNrUnNabXN2YVdFd0x6RjVjV2xyTDFoYU1XNXpXbF)" - << R"(JwTUVKak5VTndUMDFGY1daT1NrWlJhek5DVjI5Qk1EVnlRMW9pTENKbElqb2lRVkZC)" - << R"(UWlJc0ltRnNaeUk2SWxKVE1qVTJJaXdpYTJsa0lqb2lRVVJWTGpJd01EY3dNaTVTTG)" - << R"(xNaWZRLmlTVGdBRUJYc2Q3QUFOa1FNa2FHLUZBVjZRT0dVRXV4dUhnMllmU3VXaHRZ)" - << R"(WHFicE0takk1UlZMS2VzU0xDZWhLLWxSQzl4Ni1fTGV5eE5oMURPRmMtRmE2b0NFR3)" - << R"(dVajh6aU9GX0FUNnM2RU9tY2txUHJ4dXZDV3R5WWtrRFJGNzRkdGFLMWpOQTdTZFhy)" - << R"(Wnp2V0NzTXFPVU1OejBnQ29WUjBDczEyNTRrRk1SbVJQVmZFY2pnVDdqNGxDcHlEdV)" - << R"(dncjlTZW5TZXFnS0xZeGphYUcwc1JoOWNkaTJkS3J3Z2FOYXFBYkhtQ3JyaHhTUENU)" - << R"(QnpXTUV4WnJMWXp1ZEVvZnlZSGlWVlJoU0pwajBPUTE4ZWN1NERQWFYxVGN0MXkzaz)" - << R"(dMTGlvN244aXpLdXEybTNUeEY5dlBkcWI5TlA2U2M5LW15YXB0cGJGcEhlRmtVTC1G)" - << R"(NXl0bF9VQkZLcHdOOUNMNHdwNnlaLWpkWE5hZ3JtVV9xTDFDeVh3MW9tTkNnVG1KRj)" - << R"(NHZDNseXFLSEhEZXJEcy1NUnBtS2p3U3dwWkNRSkdEUmNSb3ZXeUwxMnZqdzNMQkpN)" - << R"(aG1VeHNFZEJhWlA1d0dkc2ZEOGxkS1lGVkZFY1owb3JNTnJVa1NNQWw2cEl4dGVmRV)" - << R"(hpeTVscW1pUHpxX0xKMWVSSXJxWTBfIn0.eyJzaGEyNTYiOiI3Mk9BRTJmME5iVDAr)" - << R"(VEw5MzdvNzB4bzhvTzk2Z21WTFlESnB4WEh6ZVhFPSJ9.asdxe9ylLitBHD14QsqSC)" - << R"(O1lhrsrqqMdJo73at50-C3B2OVu6n5uiQ-6AOnuwEY07cRtxLcUli92HiLFy-itD57)" - << R"(amI8ovIRuonLsJqcplmw6imdxDWD3CCkV_I3LfUBqjuaBew71Q2HrddHn3KVTFp562)" - << R"(xMYgFZmWiERnz7c-q4IuH_7AqvNm8leznVrCscAs5UquHqz3oHLU9xEn-Sur1aP0xl)" - << R"(bN-USD9WET5wXLpiu9ECZ86CFTpc_i3zlEKpl8Vbvsb0NHW_932Lrye6nz3TsYQNFx)" - << R"(Mcn5EIvHZoxIs_yHEtkJFyjFnktojrxFxGKZ5nFH-CrQH6VIwSSIH1FkJOIJiI8Qto)" - << R"(vzlqdDkZNLMYQ3uM1yKt3anXTpwHbuBrpYKQXN4T7bWN_9PWxyhnzKIDi6BulyrD8-)" - << R"(H8X7P_S7WBoFigb-nNrMFoSEm0qgAND01B0xJmsKf4Q6eB6L7k1S0bJPx5DwrPVW-9)" - << R"(TK8GXM0VjZYZGtiLCPUTa6SVRKTey)" - << R"("})"; - // clang-format on - - INFO(manifest_json_strm.str()); - - const JSON_Value* downloadJsonValue{ ADUC_Json_GetRoot(manifest_json_strm.str().c_str()) }; - - CHECK(downloadJsonValue != nullptr); - - CHECK(!ADUC_Json_ValidateManifest(downloadJsonValue)); -} -TEST_CASE("ADUC_Json_GetRootAndValidateManifest: Changed updateManifest ") -{ - // Note: Testing that the expected hash within the updateManifestSignature does not - // equal the one calculated from the updateManifest - - std::stringstream manifest_json_strm; - // clang-format off - manifest_json_strm - << R"({)" - << R"("updateManifest":)" - << R"("{)" - << R"(\"manifestVersion\":\"2.0\",)" - << R"(\"updateId\":{)" - << R"(\"provider\":\"adu\",)" - << R"(\"name\":\"test\",)" - << R"(\"version\":\"2.0.0.0\")" - << R"(},)" - << R"(\"updateType\":\"SWUpdate\",)" - << R"(\"installedCriteria\":\"1.2.3.4\",)" - << R"(\"files\":{)" - << R"(\"00000\":{)" - << R"(\"fileName\":\"foo.exe\",)" - << R"(\"sizeInBytes\":76,)" - << R"(\"hashes\":{)" - << R"(\"sha256\":\"aGc0x9DB3CFgRwPYgX2kQsN0oUsS4/Zn4kP//f3QyLc=\")" - << R"(})" - << R"(})" - << R"(},)" - << R"(\"createdDateTime\":\"2020-10-02T22:15:56.8012002Z\")" - << R"(}")" - << R"(,)" - << R"("updateManifestSignature":")" - << R"(eyJhbGciOiJSUzI1NiIsInNqd2siOiJleUpoYkdjaU9pSlNVekkxTmlJc0ltdHBaQ0)" - << R"(k2SWtGRVZTNHlNREEzTURJdVVpNVVJbjAuZXlKcmRIa2lPaUpTVTBFaUxDSnVJam9p)" - << R"(ZGtwYWJGRjFjM1J3T1RsUFRpOXJXWEJ4TlVveFRtdG1hMGxDTjNrdldVbzJNbWRrVj)" - << R"(JWRFlteE5XWGt3WjNRd1VVVjZXVGR4TjI1UFJHMDJTbXQxYldoTVpuTTNaa1JCVTA4)" - << R"(elJIaG5SVlE0Y1dkWmQyNVdhMFpsTVVkVVowZEVXR28xVTFGQ2VIQkxVRWxQTlVRcl)" - << R"(Z6TXdSSEpKTmxGSk4xRm1hVFpRWmxaT2FsVlVTRkI2U0VaNWFFMVRUVnByYWxreGVF)" - << R"(eFBNMDFSWjBobmRXUjFNV1pzU0hGREszUmlhbms1WTI0emVtcDBNM2hUVWpKbU9URk)" - << R"(RVa0p5VWxRelJ6Vm1WbFkxVjA1U1EwOWlWVU51YUdNeWFHaHlSRU15V0dreFFWaHli)" - << R"(azlRZDNWRFJFbEZkSFIzYjBzd1QyNVFjV3BLVHk5SVVqRjJMelJPY3l0S1YwOU1VRT)" - << R"(U0ZW1aRlNqSnBXVTlSWkdwbFpteEJPRVoyVFhWMGVYVndaVXRwUTJsclYzY3lOWFpw)" - << R"(YjAxUldWSXdiMVozUjFwMU16aG1PSFJvTVVoRlZFSjJkVVZWTDFkeGVIQllXbVJQUm)" - << R"(5wa09VTkhabVZsTW5FMGIzcFFNQzlQVUUxRWQxQlRNRzVZWm0xYVVHUlhhWHBwVDNW)" - << R"(RlUwSndVRko1TjFwcFJYSjVSbVpNWjJnemNVWkVXQ3RYZDB3eFdXWmxaRzU1V1N0cF)" - << R"(VWWjNiaXRpVTNWalVHeFRlRXdyUmxJNVNuZFFaRGhNZUU1bmFHUnZZbTFCTjA1MmNX)" - << R"(SkNaakp6Wld0dE1HUkpTa0pRTm5wc2MxQmxURU5ZVW1VMU1IbE9SVk5rYUhWRlFuVn)" - << R"(hNV1k0Y1ZZek1VVklSVmx6V0dRMGMwWm5WMVZ2Wm5kM1dIaFBTM0FpTENKbElqb2lR)" - << R"(VkZCUWlJc0ltRnNaeUk2SWxKVE1qVTJJaXdpYTJsa0lqb2lRVVJWTGpJd01EY3dNaT)" - << R"(VTTGxOVUluMC5RS0ktckVJSXBIZTRPZVd1dkN4cFNYam1wbC05czNlN1d2ZGRPcFNn)" - << R"(R2dJaUN5T1hPYThjSFdlbnpzSFVMTUZBZEF0WVBnM2ZVTnl3QUNJOEI2U044UGhUc3)" - << R"(E5TFg4QjR2c0l2QlZ2S2FrTU1ucjRuZXRlVHN5V0NJbEhBeG4ydThzSlQ3amVzMmtF)" - << R"(Q2ZrNnpBeS16cElSSXhYaF8yVEl5Y2RNLVU0UmJYVDhwSGpPUTVfZTRnSFRCZjRlOD)" - << R"(RaWTdsUEw0RGtTa3haR1dnaERhOTFGVjlXbjRtc050eDdhMEJ3NEtTMURVd1c2eXE0)" - << R"(c2diTDFncVl6VG04bENaSDJOb1pDdjZZUUd4YVE5UkJ1ek5Eb0VMb0RXXzB4S3V1Vz)" - << R"(Faa3UxSDN6c3lZNlNJMVp0bkFkdDAxcmxIck5fTTlKdTBZM3EyNzJiRWxWSnVnN3F6)" - << R"(cGF5WXFucllOMTBHRWhIUW9KTkRxZnlPSHNLRG9FbTNOR1pKY2FSWVl4eHY2SVZxTF)" - << R"(JDaFZidnJ3ekdkTXdmME95aE52THFGWlhMS1RWMmhPX2t6MXZWaVNwRi1PVXVWbW5u)" - << R"(RC1ZMXFQb28xSTBJeVFYallxaGNlME01UkhiQ1drVk1Qbk5jZHFMQzZlTWhiTjJGT1)" - << R"(pHSUxSOVRqcDIyMmxucGFTbnVmMHlVbDF4MiJ9.eyJzaGEyNTYiOiJjTGRjZnBBRnN)" - << R"(yYjA3ak5zVVFzQ0hDZDBmb0J3RU5remQybTB1UWZMdGc4PSJ9.ghH76UBj3uPgpfYP)" - << R"(3Px24QUB0ZmAu9BthyL90CwkAb7TRM6yjCMxU7V7P2YERa_ATnjKygTefSzxpzc1c7)" - << R"(B86X7DyLNInp3TvRktPf6PWGMbZ_O2l7XeUNfhhJwUTsFok405AK6rWBSt49MpnKbZ)" - << R"(yEss-dVEFddun1Zr71InV2gA_BtdhkP3qS7y16zp3SpLi1eqyxgAL5hGddam7jt0pL)" - << R"(XW-ds60n3zBivFJFzhbJPqanmw6VaWtgwT7c5Z437A2xMBSdfEjSmZoZgIqgE08Pub)" - << R"(n0afZZFptE4RnuSPhqqIfusuVb4uIzr5qdZUCG2qUAN4osM6raMr5m5JF4fjvG7lCE)" - << R"(FYhtfMW7qsCEmuaX2UNWeUlDEvSUHzupysWgBwIK8a-0AoUPh56Wm_m7N-Ppj3mqhk)" - << R"(xY6qK4CcucML5VevRhUr8onQHmtp8Iwv5y2THnEWh6cSnFp9qSwexDPItdk8iwclw6)" - << R"(SsmWpv3LpsuAlMHQhrfaqERBthyp-zdvr1)" - << R"("})"; - // clang-format on - - INFO(manifest_json_strm.str()); - - const JSON_Value* downloadJsonValue{ ADUC_Json_GetRoot(manifest_json_strm.str().c_str()) }; - - CHECK(downloadJsonValue != nullptr); - - CHECK(!ADUC_Json_ValidateManifest(downloadJsonValue)); -} - -TEST_CASE("ADUC_Json_GetRootAndValidateManifest: Empty Update Manifest") -{ - std::stringstream manifest_json_strm; - // clang-format off - manifest_json_strm - << R"({)" - << R"("updateManifest":"{)" - << R"(}",)" - << R"("fileUrls":{)" - << R"("00000":"http://setup.exe")" - << R"(},)" - << R"("updateManifestSignature": "")" - << R"(})"; - // clang-format on - INFO(manifest_json_strm.str()); - - const JSON_Value* downloadJsonValue{ ADUC_Json_GetRoot(manifest_json_strm.str().c_str()) }; - - CHECK(downloadJsonValue != nullptr); - - CHECK(!ADUC_Json_ValidateManifest(downloadJsonValue)); -} - -TEST_CASE("ADUC_Json_GetUpdateAction: Valid") -{ - // clang-format off - auto updateactionVal = GENERATE( // NOLINT(google-build-using-namespace) - ADUCITF_UpdateAction_Cancel, - ADUCITF_UpdateAction_ProcessDeployment); - // clang-format on - - SECTION("Verify update action values") - { - unsigned updateAction; - std::stringstream strm; - strm << R"({ "workflow" : {"action": )" << updateactionVal << R"( } })"; - INFO(strm.str()) - const JSON_Value* updateActionJson{ ADUC_Json_GetRoot(strm.str().c_str()) }; - REQUIRE(updateActionJson != nullptr); - CHECK(ADUC_Json_GetUpdateAction(updateActionJson, &updateAction)); - CHECK(updateAction == updateactionVal); - } -} - -TEST_CASE("ADUC_Json_GetUpdateAction: Invalid") -{ - unsigned updateAction; - std::stringstream strm; - const JSON_Value* updateActionJsonValue{ ADUC_Json_GetRoot(R"({ "workflow" : { "action": "foo" }})") }; - INFO(strm.str()) - REQUIRE(updateActionJsonValue != nullptr); - CHECK(!ADUC_Json_GetUpdateAction(updateActionJsonValue, &updateAction)); -} - -TEST_CASE("ADUC_Json_GetExpectedUpdateId: Valid") -{ - std::string provider{ "example-provider" }; - std::string name{ "example-name" }; - std::string version{ "1.2.3.4" }; - - std::stringstream expectedUpdateIdJsonStrm; - - // clang-format off - expectedUpdateIdJsonStrm - << R"({)" - << R"("workflow" : { )" - << R"( "action": 0 )" - << R"( },)" - << R"("updateManifest":"{)" - << R"(\"updateId\":{)" - << R"(\"provider\":\")" << provider << R"(\",)" - << R"(\"name\":\")" << name << R"(\",)" - << R"(\"version\":\")" << version << R"(\")" - << R"(})" - << R"(}")" - << R"(})"; - // clang-format on - - const JSON_Value* expectedUpdateIdJsonValue{ ADUC_Json_GetRoot(expectedUpdateIdJsonStrm.str().c_str()) }; - REQUIRE(expectedUpdateIdJsonValue != nullptr); - - ADUC_UpdateId* expectedUpdateId = nullptr; - CHECK(ADUC_Json_GetUpdateId(expectedUpdateIdJsonValue, &expectedUpdateId)); - - CHECK_THAT(expectedUpdateId->Provider, Equals(provider)); - CHECK_THAT(expectedUpdateId->Name, Equals(name)); - CHECK_THAT(expectedUpdateId->Version, Equals(version)); -} - -TEST_CASE("ADUC_Json_GetExpectedUpdateId: Invalid") -{ - SECTION("Missing Update ID returns nullptr") - { - std::stringstream expectedUpdateIdJsonStrm; - - // clang-format off - expectedUpdateIdJsonStrm - << R"({)" - << R"("workflow" : { )" - << R"( "action": 0 )" - << R"( },)" - << R"("updateManifest":"{)" - << R"(}")" - << R"(})"; - // clang-format on - - const JSON_Value* expectedUpdateIdJsonValue{ ADUC_Json_GetRoot(expectedUpdateIdJsonStrm.str().c_str()) }; - REQUIRE(expectedUpdateIdJsonValue != nullptr); - ADUC_UpdateId* expectedUpdateId = nullptr; - - CHECK(!ADUC_Json_GetUpdateId(expectedUpdateIdJsonValue, &expectedUpdateId)); - } - - SECTION("Invalid Update ID returns nullptr") - { - std::stringstream expectedUpdateIdJsonStrm; - - // clang-format off - expectedUpdateIdJsonStrm - << R"({)" - << R"("workflow" : { )" - << R"( "action": 0 )" - << R"( },)" - << R"("updateManifest":"{)" - << R"(\"updateId\":[\"a\"])" - << R"(}")" - << R"(})"; - // clang-format on - - const JSON_Value* expectedUpdateIdJsonValue{ ADUC_Json_GetRoot(expectedUpdateIdJsonStrm.str().c_str()) }; - REQUIRE(expectedUpdateIdJsonValue != nullptr); - ADUC_UpdateId* expectedUpdateId = nullptr; - CHECK(!ADUC_Json_GetUpdateId(expectedUpdateIdJsonValue, &expectedUpdateId)); - } -} - -TEST_CASE("ADUC_Json_GetInstalledCriteria: Valid") -{ - // clang-format off - std::string installedCriteriaJson{ - R"({)" - R"("action": 0,)" - R"("updateManifest": "{)" - R"(\"installedCriteria\":\"1.2.3.4\")" - R"(}")" - R"(})" - }; - // clang-format on - - const JSON_Value* installedCriteriaJsonValue{ ADUC_Json_GetRoot(installedCriteriaJson.c_str()) }; - REQUIRE(installedCriteriaJsonValue != nullptr); - cstr_wrapper installedCriteria; - CHECK(ADUC_Json_GetInstalledCriteria(installedCriteriaJsonValue, installedCriteria.address_of())); - CHECK_THAT(installedCriteria.get(), Equals("1.2.3.4")); -} - -TEST_CASE("ADUC_Json_GetInstalledCriteria: Invalid") -{ - SECTION("Missing installed criteria returns nullptr") - { - // clang-format off - std::string installedCriteriaJson{ - R"({)" - R"("action": 0,)" - R"("updateManifest": "{)" - R"(}")" - R"(})" - }; - // clang-format on - - const JSON_Value* installedCriteriaJsonValue{ ADUC_Json_GetRoot(installedCriteriaJson.c_str()) }; - REQUIRE(installedCriteriaJsonValue != nullptr); - cstr_wrapper installedCriteria; - CHECK(!ADUC_Json_GetInstalledCriteria(installedCriteriaJsonValue, installedCriteria.address_of())); - } - - SECTION("Invalid installed criteria returns nullptr") - { - // clang-format off - std::string installedCriteriaJson{ - R"({)" - R"("action": 0,)" - R"("updateManifest": "{)" - R"(\"installedCriteria\":\"[\"a\"]\")" - R"(}")" - R"(})" - }; - // clang-format on - - const JSON_Value* installedCriteriaJsonValue{ ADUC_Json_GetRoot(installedCriteriaJson.c_str()) }; - REQUIRE(installedCriteriaJsonValue != nullptr); - cstr_wrapper installedCriteria; - CHECK(!ADUC_Json_GetInstalledCriteria(installedCriteriaJsonValue, installedCriteria.address_of())); - } -} - -TEST_CASE("ADUC_Json_GetFiles: Valid") -{ - // clang-format off - const std::map files{ - {"w2cy42AR2pOR6uqQ7367IdIj+AnaCArEsSlNHjosQJY=", "http://file1" }, - {"x2cy42AR2pOR6uqQ7367IdIj+AnaCArEsSlNHjosQJY=", "http://file2" } - }; - - // clang-format on - - std::stringstream main_json_strm; - - std::stringstream url_json_strm; - - main_json_strm - << R"({"Action":0, "updateManifest":"{\"updateId\":{\"provider\": \"Azure\",\"name\": \"IOT-Firmware\",\"version\": \"1.2.0.0\"},\"files\":{)"; - url_json_strm << R"("fileUrls": {)"; - - int i = 0; - for (auto it = files.begin(); it != files.end(); ++it) - { - main_json_strm << R"(\")" << i << R"(\":{)"; - main_json_strm << R"(\"fileName\":\"file)" << i << R"(\", \"hashes\": {\"sha256\": \")"; - main_json_strm << it->first << R"(\"}})"; - - url_json_strm << R"(")" << i << R"(":")" << it->second << R"(")"; - - if (it != (--files.end())) - { - main_json_strm << ","; - url_json_strm << ","; - } - - ++i; - } - - url_json_strm << R"(})"; - main_json_strm << R"(}}",)" << url_json_strm.str() << "}"; - - INFO(main_json_strm.str()); - - const JSON_Value* downloadJsonValue{ ADUC_Json_GetRoot(main_json_strm.str().c_str()) }; - REQUIRE(downloadJsonValue != nullptr); - - unsigned int fileCount; - ADUC_FileEntity* fileEntities; - CHECK(ADUC_Json_GetFiles(downloadJsonValue, &fileCount, &fileEntities)); - REQUIRE(fileCount == 2); - - for (unsigned int i = 0; i < fileCount; ++i) - { - std::stringstream strm; - strm << "file" << i; - // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) - CHECK_THAT(fileEntities[i].TargetFilename, Equals(strm.str())); - // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) - CHECK_THAT( - fileEntities[i].DownloadUri, - Equals( - files.find(ADUC_HashUtils_GetHashValue(fileEntities[i].Hash, fileEntities[i].HashCount, 0))->second)); - } - - ADUC_FileEntityArray_Free(fileCount, fileEntities); -} - -TEST_CASE("ADUC_Json_GetFiles: Invalid") -{ - SECTION("Files missing returns nullptr") - { - const JSON_Value* downloadJsonValue{ ADUC_Json_GetRoot(R"({ "action": 0,"TargetVersion": "1.2.3.4" })") }; - REQUIRE(downloadJsonValue != nullptr); - - unsigned int fileCount; - ADUC_FileEntity* fileEntities; - CHECK(!ADUC_Json_GetFiles(downloadJsonValue, &fileCount, &fileEntities)); - } - - SECTION("Empty files returns nullptr") - { - const JSON_Value* downloadJsonValue{ - ADUC_Json_GetRoot( - R"({ "action": 0,"TargetVersion": "1.2.3.4", "updateManifest":"{ \"files\": {} }", "fileUrls": { "001":"https://file1"} })") - }; - REQUIRE(downloadJsonValue != nullptr); - - unsigned int fileCount; - ADUC_FileEntity* fileEntities; - CHECK(!ADUC_Json_GetFiles(downloadJsonValue, &fileCount, &fileEntities)); - } - - SECTION("Empty fileURLs returns nullptr") - { - const JSON_Value* downloadJsonValue{ - ADUC_Json_GetRoot( - R"({ "action": 0,"TargetVersion": "1.2.3.4", "updateManifest":"{ \"files\": {\"001\":{\"fileName\":\"file1\", \"hashes\":{\"sha256\":\"w2cy42AR2pOR6uqQ7367IdIj+AnaCArEsSlNHjosQJY=\"}}}", "fileUrls": {} })") - }; - REQUIRE(downloadJsonValue != nullptr); - - unsigned int fileCount; - ADUC_FileEntity* fileEntities; - CHECK(!ADUC_Json_GetFiles(downloadJsonValue, &fileCount, &fileEntities)); - } -} diff --git a/src/agent/adu_core_interface/tests/main.cpp b/src/agent/adu_core_interface/tests/main.cpp deleted file mode 100644 index 834f3201b..000000000 --- a/src/agent/adu_core_interface/tests/main.cpp +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @file main.cpp - * @brief Catch2 Unit Test main file. - * - * @copyright Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ -// https://github.com/catchorg/Catch2/blob/master/docs/own-main.md -#define CATCH_CONFIG_RUNNER -#include - -// https://github.com/Azure/umock-c/blob/master/doc/umock_c.md -#include - -#include - -int main(int argc, char* argv[]) -{ - int result; - - // Global setup - result = umock_c_init([](UMOCK_C_ERROR_CODE error_code) -> void { - std::cout << "*** umock_c failed, err=" << error_code << std::endl; - }); - if (result != 0) - { - std::cout << "umock_c_init_failed, err=" << result << std::endl; - return result; - } - - result = Catch::Session().run(argc, argv); - if (result != 0) - { - std::cout << "Catch session failed, err=" << result << std::endl; - } - - // Global cleanup. - umock_c_deinit(); - - return result; -} diff --git a/src/agent/adu_core_interface/tests/result_ut.cpp b/src/agent/adu_core_interface/tests/result_ut.cpp deleted file mode 100644 index dc03cac4c..000000000 --- a/src/agent/adu_core_interface/tests/result_ut.cpp +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @file result_ut.cpp - * @brief Unit tests for result.h - * - * @copyright Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ -#include - -#include "aduc/result.h" - -static uint8_t FacilityFromExtendedResultCode(ADUC_Result_t extendedResultCode) -{ - return (static_cast(extendedResultCode) >> 0x1C) & 0xF; -} - -static uint32_t CodeFromExtendedResultCode(ADUC_Result_t extendedResultCode) -{ - return extendedResultCode & 0xFFFFFFF; -} - -TEST_CASE("IsAducResultCode: Valid") -{ - auto resultVal = GENERATE(as{}, 1, 2, INT32_MAX); // NOLINT(google-build-using-namespace) - - INFO(resultVal); - CHECK(IsAducResultCodeSuccess(resultVal)); -} - -TEST_CASE("IsAducResultCode: Invalid") -{ - auto resultVal = GENERATE(as{}, 0, -1, INT32_MIN); // NOLINT(google-build-using-namespace) - - INFO(resultVal); - CHECK(IsAducResultCodeFailure(resultVal)); -} - -TEST_CASE("EXTENDEDRESULTCODE") -{ - auto errorVal = GENERATE(as{}, 0, 1, 0xfffff); // NOLINT(google-build-using-namespace) - - SECTION("MAKE_ADUC_EXTENDEDRESULTCODE") - { - const ADUC_Result_t erc{ MAKE_ADUC_EXTENDEDRESULTCODE(ADUC_FACILITY_LOWERLAYER, 0, errorVal) }; - CHECK(FacilityFromExtendedResultCode(erc) == ADUC_FACILITY_LOWERLAYER); - CHECK(CodeFromExtendedResultCode(erc) == errorVal); - } - - SECTION("ADUC_FACILITY_DELIVERY_OPTIMIZATION") - { - const ADUC_Result_t erc{ MAKE_ADUC_DELIVERY_OPTIMIZATION_EXTENDEDRESULTCODE(errorVal) }; - CHECK(FacilityFromExtendedResultCode(erc) == ADUC_FACILITY_DELIVERY_OPTIMIZATION); - CHECK(CodeFromExtendedResultCode(erc) == errorVal); - } - - SECTION("ADUC_FACILITY_LOWERLAYER") - { - const ADUC_Result_t erc{ MAKE_ADUC_ERRNO_EXTENDEDRESULTCODE(errorVal) }; - CHECK(FacilityFromExtendedResultCode(erc) == ADUC_FACILITY_UNKNOWN); - CHECK(CodeFromExtendedResultCode(erc) == errorVal); - } -} - -// Note: /usr/include/asm-generic/errno.h definitions - -#define EPERM 1 -#define ENOMEM 12 -#define ENOTRECOVERABLE 131 - -TEST_CASE("ADUC_ERC Macros") -{ - CHECK(FacilityFromExtendedResultCode(ADUC_ERC_NOTRECOVERABLE) == ADUC_FACILITY_UNKNOWN); - CHECK(CodeFromExtendedResultCode(ADUC_ERC_NOTRECOVERABLE) == ENOTRECOVERABLE); - - CHECK(FacilityFromExtendedResultCode(ADUC_ERC_NOMEM) == ADUC_FACILITY_UNKNOWN); - CHECK(CodeFromExtendedResultCode(ADUC_ERC_NOMEM) == ENOMEM); - - CHECK(FacilityFromExtendedResultCode(ADUC_ERC_NOTPERMITTED) == ADUC_FACILITY_UNKNOWN); - CHECK(CodeFromExtendedResultCode(ADUC_ERC_NOTPERMITTED) == EPERM); -} diff --git a/src/agent/adu_core_interface/tests/startup_workflowdata_ut.cpp b/src/agent/adu_core_interface/tests/startup_workflowdata_ut.cpp deleted file mode 100644 index fbfd7d0af..000000000 --- a/src/agent/adu_core_interface/tests/startup_workflowdata_ut.cpp +++ /dev/null @@ -1,172 +0,0 @@ -/** - * @file startup_workflowdata_ut.cpp - * @brief Unit tests for ADUC_Workflow_HandleStartupWorkflowData - * @copyright Copyright (c) Microsoft Corp. - * Licensed under the MIT License. - */ - -#include "workflow_test_utils.h" -#include -#include -#include -#include -#include - -static std::string get_update_manifest_json_path() { - std::string path{ ADUC_TEST_DATA_FOLDER }; - path += "/startupworkflowdata/updateManifest.json"; - return path; -} - -static unsigned int s_handleUpdateAction_call_count = 0; -static void Mock_ADUC_Workflow_HandleUpdateAction(ADUC_WorkflowData* workflowData) -{ - UNREFERENCED_PARAMETER(workflowData); - - ++s_handleUpdateAction_call_count; -} - -static unsigned int s_setUpdateStateWithResult_call_count = 0; -static void Mock_ADUC_Workflow_SetUpdateStateWithResult( - ADUC_WorkflowData* workflowData, ADUCITF_State updateState, ADUC_Result result) -{ - UNREFERENCED_PARAMETER(workflowData); - UNREFERENCED_PARAMETER(updateState); - UNREFERENCED_PARAMETER(result); - - ++s_setUpdateStateWithResult_call_count; -} - -static ADUC_Result Mock_IsInstalledCallback(ADUC_Token token, ADUC_WorkflowDataToken workflowDataToken) -{ - UNREFERENCED_PARAMETER(token); - UNREFERENCED_PARAMETER(workflowDataToken); - - ADUC_Result result = { ADUC_Result_Success }; - return result; -} - -// NOLINTNEXTLINE(readability-non-const-parameter) -static ADUC_Result Mock_SandboxCreateCallback(ADUC_Token token, const char* workflowId, char* workFolder) -{ - UNREFERENCED_PARAMETER(token); - UNREFERENCED_PARAMETER(workflowId); - UNREFERENCED_PARAMETER(workFolder); - - ADUC_Result result = { ADUC_Result_Success, 0 }; - return result; -} - -static void Mock_SandboxDestroyCallback(ADUC_Token token, const char* workflowId, const char* workFolder) -{ - UNREFERENCED_PARAMETER(token); - UNREFERENCED_PARAMETER(workflowId); - UNREFERENCED_PARAMETER(workFolder); -} - -static void Reset_MockStats() -{ - s_handleUpdateAction_call_count = 0; - s_setUpdateStateWithResult_call_count = 0; -} - -TEST_CASE("ADUC_Workflow_HandleStartupWorkflowData") -{ - SECTION("It should exit early when workflowData is NULL") - { - Reset_MockStats(); - - ADUC_Workflow_HandleStartupWorkflowData(nullptr /* currentWorkflowData */); - CHECK(s_handleUpdateAction_call_count == 0); - CHECK(s_setUpdateStateWithResult_call_count == 0); - } - - SECTION("It should exit early when StartupIdleCallSent is true") - { - Reset_MockStats(); - - ADUC_WorkflowData workflowData{}; - workflowData.StartupIdleCallSent = true; - - ADUC_Workflow_HandleStartupWorkflowData(&workflowData); - - CHECK(s_handleUpdateAction_call_count == 0); - CHECK(s_setUpdateStateWithResult_call_count == 0); - CHECK(workflowData.StartupIdleCallSent == true); - } - - SECTION("It should call HandleUpdateAction for ProcessDeployment workflow action") - { - Reset_MockStats(); - - auto nextWorkflow = (ADUC_Workflow*)malloc(sizeof(ADUC_Workflow)); // NOLINT - REQUIRE(nextWorkflow != nullptr); - - ADUC_Result result = workflow_init_from_file(get_update_manifest_json_path().c_str(), true, (void**)&nextWorkflow); // NOLINT - REQUIRE(IsAducResultCodeSuccess(result.ResultCode)); - - ADUC_WorkflowData workflowData{}; - ADUC_TestOverride_Hooks hooks{}; - hooks.HandleUpdateActionFunc_TestOverride = Mock_ADUC_Workflow_HandleUpdateAction; - hooks.SetUpdateStateWithResultFunc_TestOverride = Mock_ADUC_Workflow_SetUpdateStateWithResult; - workflowData.TestOverrides = &hooks; - - workflowData.WorkflowHandle = nextWorkflow; - workflowData.StartupIdleCallSent = false; - workflowData.UpdateActionCallbacks.IsInstalledCallback = Mock_IsInstalledCallback; - workflowData.UpdateActionCallbacks.SandboxDestroyCallback = Mock_SandboxDestroyCallback; - workflowData.UpdateActionCallbacks.SandboxCreateCallback = Mock_SandboxCreateCallback; - - ADUC_Workflow_HandleStartupWorkflowData(&workflowData); - CHECK(s_handleUpdateAction_call_count == 1); - CHECK(s_setUpdateStateWithResult_call_count == 0); - CHECK(workflowData.StartupIdleCallSent == true); - } - - SECTION("It should exit early and transition to Idle(success) for null workflowData") - { - Reset_MockStats(); - - auto nextWorkflow = (ADUC_Workflow*)malloc(sizeof(ADUC_Workflow)); // NOLINT - REQUIRE(nextWorkflow != nullptr); - - ADUC_Result result = workflow_init_from_file(get_update_manifest_json_path().c_str(), true, (void**)&nextWorkflow); // NOLINT - REQUIRE(IsAducResultCodeSuccess(result.ResultCode)); - - ADUC_Workflow_HandleStartupWorkflowData(nullptr); - CHECK(s_handleUpdateAction_call_count == 0); - CHECK(s_setUpdateStateWithResult_call_count == 0); - } -} - -// workflowData->WorkflowHandle will be NULL in this case -TEST_CASE("HandleStartupWorkflowData from ADUCoreInterface_Connected") -{ - SECTION("It should set StartupIdleCallSent") - { - // Arrange - ADUC_WorkflowData workflowData{}; - ADUC_TestOverride_Hooks hooks{}; - workflowData.TestOverrides = &hooks; - - // Act - ADUC_Workflow_HandleStartupWorkflowData(&workflowData); - - // Assert - CHECK(workflowData.StartupIdleCallSent); - } - - SECTION("It should handle workflow persistence") - { - // Arrange - ADUC_WorkflowData workflowData{}; - ADUC_TestOverride_Hooks hooks{}; - workflowData.TestOverrides = &hooks; - - // Act - ADUC_Workflow_HandleStartupWorkflowData(&workflowData); - - // Assert - CHECK(workflowData.StartupIdleCallSent); - } -} diff --git a/src/agent/adu_core_interface/tests/testdata/adu_core_export_helpers/updateManifest.json b/src/agent/adu_core_interface/tests/testdata/adu_core_export_helpers/updateManifest.json index 309fafe3f..bcd2b76bb 100644 --- a/src/agent/adu_core_interface/tests/testdata/adu_core_export_helpers/updateManifest.json +++ b/src/agent/adu_core_interface/tests/testdata/adu_core_export_helpers/updateManifest.json @@ -5,7 +5,7 @@ "f2c5d1f3b0295db0f": "http://duinstance2.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/9ff068f7c2bf43eb9561da14a7cbcecd/motor-firmware-1.1.json", "f13b5435aab7c18da": "http://duinstance2.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/c02058a476a242d7bc0e3c576c180051/contoso-motor-installscript.sh" }, - "updateManifest": "{\"manifestVersion\":\"4\",\"updateId\":{\"provider\":\"Contoso\",\"name\":\"Virtual-Vacuum\",\"version\":\"20.0\"},\"compatibility\":[{\"deviceManufacturer\":\"contoso\",\"deviceModel\":\"virtual-vacuum-v1\"}],\"instructions\":{\"steps\":[{\"handler\":\"microsoft/apt:1\",\"files\":[\"f483750ebb885d32c\"],\"handlerProperties\":{\"installedCriteria\":\"apt-update-tree-1.0\"}},{\"type\":\"reference\",\"detachedManifestFileId\":\"f222b9ffefaaac577\"}]},\"files\":{\"f483750ebb885d32c\":{\"fileName\":\"apt-manifest-tree-1.0.json\",\"sizeInBytes\":136,\"hashes\":{\"sha256\":\"Uk1vsEL/nT4btMngo0YSJjheOL2aqm6/EAFhzPb0rXs=\"}},\"f222b9ffefaaac577\":{\"fileName\":\"contoso.contoso-virtual-motors.1.1.updatemanifest.json\",\"sizeInBytes\":1031,\"hashes\":{\"sha256\":\"9Rnjw7ThZhGacOGn3uvvVq0ccQTHc/UFSL9khR2oKsc=\"}}},\"createdDateTime\":\"2022-01-27T13:45:05.8993329Z\"}", + "updateManifest": "{\"manifestVersion\":\"5\",\"updateId\":{\"provider\":\"Contoso\",\"name\":\"Virtual-Vacuum\",\"version\":\"20.0\"},\"compatibility\":[{\"deviceManufacturer\":\"contoso\",\"deviceModel\":\"virtual-vacuum-v1\"}],\"instructions\":{\"steps\":[{\"handler\":\"microsoft/apt:1\",\"files\":[\"f483750ebb885d32c\"],\"handlerProperties\":{\"installedCriteria\":\"apt-update-tree-1.0\"}},{\"type\":\"reference\",\"detachedManifestFileId\":\"f222b9ffefaaac577\"}]},\"files\":{\"f483750ebb885d32c\":{\"fileName\":\"apt-manifest-tree-1.0.json\",\"sizeInBytes\":136,\"hashes\":{\"sha256\":\"Uk1vsEL/nT4btMngo0YSJjheOL2aqm6/EAFhzPb0rXs=\"}},\"f222b9ffefaaac577\":{\"fileName\":\"contoso.contoso-virtual-motors.1.1.updatemanifest.json\",\"sizeInBytes\":1031,\"hashes\":{\"sha256\":\"9Rnjw7ThZhGacOGn3uvvVq0ccQTHc/UFSL9khR2oKsc=\"}}},\"createdDateTime\":\"2022-01-27T13:45:05.8993329Z\"}", "updateManifestSignature": "eyJhbGciOiJSUzI1NiIsInNqd2siOiJleUpoYkdjaU9pSlNVekkxTmlJc0ltdHBaQ0k2SWtGRVZTNHlNREEzTURJdVVpSjkuZXlKcmRIa2lPaUpTVTBFaUxDSnVJam9pYkV4bWMwdHZPRmwwWW1Oak1sRXpUalV3VlhSTVNXWlhVVXhXVTBGRlltTm9LMFl2WTJVM1V6Rlpja3BvV0U5VGNucFRaa051VEhCVmFYRlFWSGMwZWxndmRHbEJja0ZGZFhrM1JFRmxWVzVGU0VWamVEZE9hM2QzZVRVdk9IcExaV3AyWTBWWWNFRktMMlV6UWt0SE5FVTBiMjVtU0ZGRmNFOXplSGRQUzBWbFJ6QkhkamwzVjB3emVsUmpUblprUzFoUFJGaEdNMVZRWlVveGIwZGlVRkZ0Y3pKNmJVTktlRUppZEZOSldVbDBiWFpwWTNneVpXdGtWbnBYUm5jdmRrdFVUblZMYXpob2NVczNTRkptYWs5VlMzVkxXSGxqSzNsSVVVa3dZVVpDY2pKNmEyc3plR2d4ZEVWUFN6azRWMHBtZUdKamFsQnpSRTgyWjNwWmVtdFlla05OZW1Fd1R6QkhhV0pDWjB4QlZGUTVUV1k0V1ZCd1dVY3lhblpQWVVSVmIwTlJiakpWWTFWU1RtUnNPR2hLWW5scWJscHZNa3B5SzFVNE5IbDFjVTlyTjBZMFdubFRiMEoyTkdKWVNrZ3lXbEpTV2tab0wzVlRiSE5XT1hkU2JWbG9XWEoyT1RGRVdtbHhhemhJVWpaRVUyeHVabTVsZFRJNFJsUm9SVzF0YjNOVlRUTnJNbGxNYzBKak5FSnZkWEIwTTNsaFNEaFpia3BVTnpSMU16TjFlakU1TDAxNlZIVnFTMmMzVkdGcE1USXJXR0owYmxwRU9XcFVSMkY1U25Sc2FFWmxWeXRJUXpVM1FYUkJSbHBvY1ZsM2VVZHJXQ3M0TTBGaFVGaGFOR0V4VHpoMU1qTk9WVWQxTWtGd04yOU5NVTR3ZVVKS0swbHNUM29pTENKbElqb2lRVkZCUWlJc0ltRnNaeUk2SWxKVE1qVTJJaXdpYTJsa0lqb2lRVVJWTGpJeE1EWXdPUzVTTGxNaWZRLlJLS2VBZE02dGFjdWZpSVU3eTV2S3dsNFpQLURMNnEteHlrTndEdkljZFpIaTBIa2RIZ1V2WnoyZzZCTmpLS21WTU92dXp6TjhEczhybXo1dnMwT1RJN2tYUG1YeDZFLUYyUXVoUXNxT3J5LS1aN2J3TW5LYTNkZk1sbkthWU9PdURtV252RWMyR0hWdVVTSzREbmw0TE9vTTQxOVlMNThWTDAtSEthU18xYmNOUDhXYjVZR08xZXh1RmpiVGtIZkNIU0duVThJeUFjczlGTjhUT3JETHZpVEtwcWtvM3RiSUwxZE1TN3NhLWJkZExUVWp6TnVLTmFpNnpIWTdSanZGbjhjUDN6R2xjQnN1aVQ0XzVVaDZ0M05rZW1UdV9tZjdtZUFLLTBTMTAzMFpSNnNTR281azgtTE1sX0ZaUmh4djNFZFNtR2RBUTNlMDVMRzNnVVAyNzhTQWVzWHhNQUlHWmcxUFE3aEpoZGZHdmVGanJNdkdTSVFEM09wRnEtZHREcEFXbUo2Zm5sZFA1UWxYek5tQkJTMlZRQUtXZU9BYjh0Yjl5aVhsemhtT1dLRjF4SzlseHpYUG9GNmllOFRUWlJ4T0hxTjNiSkVISkVoQmVLclh6YkViV2tFNm4zTEoxbkd5M1htUlVFcER0Umdpa0tBUzZybFhFT0VneXNjIn0.eyJzaGEyNTYiOiJqSW12eGpsc2pqZ29JeUJuYThuZTk2d0RYYlVsU3N6eGFoM0NibkF6STFJPSJ9.PzpvU13h6VhN8VHXUTYKAlpDW5t3JaQ-gs895_Q10XshKPYpeZUtViXGHGC-aQSQAYPhhYV-lLia9niXzZz4Qs4ehwFLHJfkmKR8eRwWvoOgJtAY0IIUA_8SeShmoOc9cdpC35N3OeaM4hV9shxvvrphDib5sLpkrv3LQrt3DHvK_L2n0HsybC-pwS7MzaSUIYoU-fXwZo6x3z7IbSaSNwS0P-50qeV99Mc0AUSIvB26GjmjZ2gEH5R3YD9kp0DOrYvE5tIymVHPTqkmunv2OrjKu2UOhNj8Om3RoVzxIkVM89cVGb1u1yB2kxEmXogXPz64cKqQWm22tV-jalS4dAc_1p9A9sKzZ632HxnlavOBjTKDGFgM95gg8M5npXBP3QIvkwW3yervCukViRUKIm-ljpDmnBJsZTMx0uzTaAk5XgoCUCADuLLol8EXB-0V4m2w-6tV6kAzRiwkqw1PRrGqplf-gmfU7TuFlQ142-EZLU5rK_dAiQRXx-f7LxNH", "workflow": { "action": 3, diff --git a/src/agent/adu_core_interface/tests/testdata/workflow_reboot/updateActionForActionBundle.json b/src/agent/adu_core_interface/tests/testdata/workflow_reboot/updateActionForActionBundle.json index c347135a4..fb683c67f 100644 --- a/src/agent/adu_core_interface/tests/testdata/workflow_reboot/updateActionForActionBundle.json +++ b/src/agent/adu_core_interface/tests/testdata/workflow_reboot/updateActionForActionBundle.json @@ -3,7 +3,7 @@ "action": 3, "id": "e99c69ca-3188-43a3-80af-310616c7751d" }, - "updateManifest": "{\"manifestVersion\":\"4\",\"updateId\":{\"provider\":\"Contoso\",\"name\":\"Virtual-Vacuum\",\"version\":\"20.0\"},\"compatibility\":[{\"deviceManufacturer\":\"contoso\",\"deviceModel\":\"virtual-vacuum-v1\"}],\"instructions\":{\"steps\":[{\"handler\":\"microsoft/apt:1\",\"files\":[\"f483750ebb885d32c\"],\"handlerProperties\":{\"installedCriteria\":\"apt-update-tree-1.0\"}},{\"type\":\"reference\",\"detachedManifestFileId\":\"f222b9ffefaaac577\"}]},\"files\":{\"f483750ebb885d32c\":{\"fileName\":\"apt-manifest-tree-1.0.json\",\"sizeInBytes\":136,\"hashes\":{\"sha256\":\"Uk1vsEL/nT4btMngo0YSJjheOL2aqm6/EAFhzPb0rXs=\"}},\"f222b9ffefaaac577\":{\"fileName\":\"contoso.contoso-virtual-motors.1.1.updatemanifest.json\",\"sizeInBytes\":1031,\"hashes\":{\"sha256\":\"9Rnjw7ThZhGacOGn3uvvVq0ccQTHc/UFSL9khR2oKsc=\"}}},\"createdDateTime\":\"2022-01-27T13:45:05.8993329Z\"}", + "updateManifest": "{\"manifestVersion\":\"5\",\"updateId\":{\"provider\":\"Contoso\",\"name\":\"Virtual-Vacuum\",\"version\":\"20.0\"},\"compatibility\":[{\"deviceManufacturer\":\"contoso\",\"deviceModel\":\"virtual-vacuum-v1\"}],\"instructions\":{\"steps\":[{\"handler\":\"microsoft/apt:1\",\"files\":[\"f483750ebb885d32c\"],\"handlerProperties\":{\"installedCriteria\":\"apt-update-tree-1.0\"}},{\"type\":\"reference\",\"detachedManifestFileId\":\"f222b9ffefaaac577\"}]},\"files\":{\"f483750ebb885d32c\":{\"fileName\":\"apt-manifest-tree-1.0.json\",\"sizeInBytes\":136,\"hashes\":{\"sha256\":\"Uk1vsEL/nT4btMngo0YSJjheOL2aqm6/EAFhzPb0rXs=\"}},\"f222b9ffefaaac577\":{\"fileName\":\"contoso.contoso-virtual-motors.1.1.updatemanifest.json\",\"sizeInBytes\":1031,\"hashes\":{\"sha256\":\"9Rnjw7ThZhGacOGn3uvvVq0ccQTHc/UFSL9khR2oKsc=\"}}},\"createdDateTime\":\"2022-01-27T13:45:05.8993329Z\"}", "updateManifestSignature": "eyJhbGciOiJSUzI1NiIsInNqd2siOiJleUpoYkdjaU9pSlNVekkxTmlJc0ltdHBaQ0k2SWtGRVZTNHlNREEzTURJdVVpSjkuZXlKcmRIa2lPaUpTVTBFaUxDSnVJam9pYkV4bWMwdHZPRmwwWW1Oak1sRXpUalV3VlhSTVNXWlhVVXhXVTBGRlltTm9LMFl2WTJVM1V6Rlpja3BvV0U5VGNucFRaa051VEhCVmFYRlFWSGMwZWxndmRHbEJja0ZGZFhrM1JFRmxWVzVGU0VWamVEZE9hM2QzZVRVdk9IcExaV3AyWTBWWWNFRktMMlV6UWt0SE5FVTBiMjVtU0ZGRmNFOXplSGRQUzBWbFJ6QkhkamwzVjB3emVsUmpUblprUzFoUFJGaEdNMVZRWlVveGIwZGlVRkZ0Y3pKNmJVTktlRUppZEZOSldVbDBiWFpwWTNneVpXdGtWbnBYUm5jdmRrdFVUblZMYXpob2NVczNTRkptYWs5VlMzVkxXSGxqSzNsSVVVa3dZVVpDY2pKNmEyc3plR2d4ZEVWUFN6azRWMHBtZUdKamFsQnpSRTgyWjNwWmVtdFlla05OZW1Fd1R6QkhhV0pDWjB4QlZGUTVUV1k0V1ZCd1dVY3lhblpQWVVSVmIwTlJiakpWWTFWU1RtUnNPR2hLWW5scWJscHZNa3B5SzFVNE5IbDFjVTlyTjBZMFdubFRiMEoyTkdKWVNrZ3lXbEpTV2tab0wzVlRiSE5XT1hkU2JWbG9XWEoyT1RGRVdtbHhhemhJVWpaRVUyeHVabTVsZFRJNFJsUm9SVzF0YjNOVlRUTnJNbGxNYzBKak5FSnZkWEIwTTNsaFNEaFpia3BVTnpSMU16TjFlakU1TDAxNlZIVnFTMmMzVkdGcE1USXJXR0owYmxwRU9XcFVSMkY1U25Sc2FFWmxWeXRJUXpVM1FYUkJSbHBvY1ZsM2VVZHJXQ3M0TTBGaFVGaGFOR0V4VHpoMU1qTk9WVWQxTWtGd04yOU5NVTR3ZVVKS0swbHNUM29pTENKbElqb2lRVkZCUWlJc0ltRnNaeUk2SWxKVE1qVTJJaXdpYTJsa0lqb2lRVVJWTGpJeE1EWXdPUzVTTGxNaWZRLlJLS2VBZE02dGFjdWZpSVU3eTV2S3dsNFpQLURMNnEteHlrTndEdkljZFpIaTBIa2RIZ1V2WnoyZzZCTmpLS21WTU92dXp6TjhEczhybXo1dnMwT1RJN2tYUG1YeDZFLUYyUXVoUXNxT3J5LS1aN2J3TW5LYTNkZk1sbkthWU9PdURtV252RWMyR0hWdVVTSzREbmw0TE9vTTQxOVlMNThWTDAtSEthU18xYmNOUDhXYjVZR08xZXh1RmpiVGtIZkNIU0duVThJeUFjczlGTjhUT3JETHZpVEtwcWtvM3RiSUwxZE1TN3NhLWJkZExUVWp6TnVLTmFpNnpIWTdSanZGbjhjUDN6R2xjQnN1aVQ0XzVVaDZ0M05rZW1UdV9tZjdtZUFLLTBTMTAzMFpSNnNTR281azgtTE1sX0ZaUmh4djNFZFNtR2RBUTNlMDVMRzNnVVAyNzhTQWVzWHhNQUlHWmcxUFE3aEpoZGZHdmVGanJNdkdTSVFEM09wRnEtZHREcEFXbUo2Zm5sZFA1UWxYek5tQkJTMlZRQUtXZU9BYjh0Yjl5aVhsemhtT1dLRjF4SzlseHpYUG9GNmllOFRUWlJ4T0hxTjNiSkVISkVoQmVLclh6YkViV2tFNm4zTEoxbkd5M1htUlVFcER0Umdpa0tBUzZybFhFT0VneXNjIn0.eyJzaGEyNTYiOiJqSW12eGpsc2pqZ29JeUJuYThuZTk2d0RYYlVsU3N6eGFoM0NibkF6STFJPSJ9.PzpvU13h6VhN8VHXUTYKAlpDW5t3JaQ-gs895_Q10XshKPYpeZUtViXGHGC-aQSQAYPhhYV-lLia9niXzZz4Qs4ehwFLHJfkmKR8eRwWvoOgJtAY0IIUA_8SeShmoOc9cdpC35N3OeaM4hV9shxvvrphDib5sLpkrv3LQrt3DHvK_L2n0HsybC-pwS7MzaSUIYoU-fXwZo6x3z7IbSaSNwS0P-50qeV99Mc0AUSIvB26GjmjZ2gEH5R3YD9kp0DOrYvE5tIymVHPTqkmunv2OrjKu2UOhNj8Om3RoVzxIkVM89cVGb1u1yB2kxEmXogXPz64cKqQWm22tV-jalS4dAc_1p9A9sKzZ632HxnlavOBjTKDGFgM95gg8M5npXBP3QIvkwW3yervCukViRUKIm-ljpDmnBJsZTMx0uzTaAk5XgoCUCADuLLol8EXB-0V4m2w-6tV6kAzRiwkqw1PRrGqplf-gmfU7TuFlQ142-EZLU5rK_dAiQRXx-f7LxNH", "fileUrls": { "f483750ebb885d32c": "http://duinstance2.b.nlu.dl.adu.microsoft.com/westus2/e5cc19d5e9174c93ada35cc315f1fb1d/apt-manifest-tree-1.0.json", diff --git a/src/agent/adu_core_interface/tests/workflow_reboot_ut.cpp b/src/agent/adu_core_interface/tests/workflow_reboot_ut.cpp deleted file mode 100644 index 9d3256658..000000000 --- a/src/agent/adu_core_interface/tests/workflow_reboot_ut.cpp +++ /dev/null @@ -1,592 +0,0 @@ -/** - * @file adu_core_export_helpers_ut.cpp - * @brief Unit tests for adu_core_export_helpers.h - * - * @copyright Copyright (c) Microsoft Corp. - * Licensed under the MIT License. - */ -#include -#include -using Catch::Matchers::Equals; -#include -#include -#include -#include -#include -#include -#include -#include -#include // mkdir -#include - -#include "aduc/adu_core_export_helpers.h" -#include "aduc/adu_core_interface.h" -#include "aduc/agent_workflow.h" -#include "aduc/c_utils.h" -#include "aduc/content_handler.hpp" -#include "aduc/result.h" -#include "aduc/types/workflow.h" -#include "aduc/workflow_data_utils.h" -#include "aduc/workflow_internal.h" -#include "aduc/workflow_utils.h" -#include "workflow_test_utils.h" - -// clang-format off - -EXTERN_C_BEGIN -// fwd-decl to completion signal handler fn. -void ADUC_Workflow_WorkCompletionCallback(const void* workCompletionToken, ADUC_Result result, bool isAsync); -EXTERN_C_END - -// Note: g_iotHubClientHandleForADUComponent declared in adu_core_intefaace.h - -// -// Test Helpers -// - -static std::mutex cv_mutex; -static std::condition_variable workCompletionCallbackCV; - -static bool s_workflowComplete = false; -static std::string s_expectedWorkflowIdWhenIdle; - -#define ADUC_ClientHandle_Invalid (-1) - -static unsigned s_mockWorkCompletionCallbackCallCount = 0; - -static IdleCallbackFunc s_platform_idle_callback = nullptr; - -static ADUC_ResultCode s_download_result_code = ADUC_Result_Download_Success; -static ADUC_ResultCode s_install_result_code = ADUC_Result_Install_Success; -static ADUC_ResultCode s_apply_result_code = ADUC_Result_Apply_Success; -static ADUC_ResultCode s_cancel_result_code = ADUC_Result_Cancel_Success; -static ADUC_ResultCode s_isInstalled_result_code = ADUC_Result_IsInstalled_NotInstalled; -static int s_reboot_system_return_code = 0; -static unsigned s_Mock_RebootSystem_call_count = 0; - - -bool s_workflowBeforeRebootIsDone = false; -bool s_idleDone = false; - -static void Mock_Idle_Callback(ADUC_Token token, const char* workflowId) -{ - CHECK(token != nullptr); - CHECK(workflowId != nullptr); - CHECK(strcmp(workflowId, "e99c69ca-3188-43a3-80af-310616c7751d") == 0); - - // call the original update callback - REQUIRE(s_platform_idle_callback != nullptr); - (*s_platform_idle_callback)(token, workflowId); - - // notify now so that test can cleanup - { - std::unique_lock lock(cv_mutex); - - s_idleDone = true; // update condition - - // need to manually unlock before notifying to prevent waking wait - // thread to only block again because lock was already taken - lock.unlock(); - workCompletionCallbackCV.notify_one(); - } -} - -extern "C" -{ - static void Mock_DownloadProgressCallback( - const char* /*workflowId*/, - const char* /*fileId*/, - ADUC_DownloadProgressState /*state*/, - uint64_t /*bytesTransferred*/, - uint64_t /*bytesTotal*/) - { - } - - static void Mock_WorkCompletionCallback(const void* workCompletionToken, ADUC_Result result, bool isAsync) - { - CHECK(workCompletionToken != nullptr); - CHECK(IsAducResultCodeSuccess(result.ResultCode)); - CHECK(result.ExtendedResultCode == 0); - - auto methodCallData = (ADUC_MethodCall_Data*)workCompletionToken; // NOLINT - auto workflowData = methodCallData->WorkflowData; - - switch (s_mockWorkCompletionCallbackCallCount) - { - case 0: - // - // Process Deployment - // - REQUIRE_FALSE(isAsync); - REQUIRE(ADUC_WorkflowData_GetLastReportedState(workflowData) == ADUCITF_State_Idle); - REQUIRE(ADUC_WorkflowData_GetCurrentAction(workflowData) == ADUCITF_UpdateAction_ProcessDeployment); - REQUIRE(workflow_get_current_workflowstep(workflowData->WorkflowHandle) == ADUCITF_WorkflowStep_ProcessDeployment); - REQUIRE(workflow_get_operation_in_progress(workflowData->WorkflowHandle) == true); - REQUIRE(workflow_get_operation_cancel_requested(workflowData->WorkflowHandle) == false); - break; - - case 1: - // - // Download - // - REQUIRE(isAsync); - REQUIRE(ADUC_WorkflowData_GetLastReportedState(workflowData) == ADUCITF_State_DownloadStarted); - REQUIRE(ADUC_WorkflowData_GetCurrentAction(workflowData) == ADUCITF_UpdateAction_ProcessDeployment); - REQUIRE(workflow_get_current_workflowstep(workflowData->WorkflowHandle) == ADUCITF_WorkflowStep_Download); - - REQUIRE(workflowData->IsRegistered == false); - REQUIRE(workflow_get_operation_in_progress(workflowData->WorkflowHandle) == true); - REQUIRE(workflow_get_operation_cancel_requested(workflowData->WorkflowHandle) == false); - break; - - case 2: - // - // Install - // - REQUIRE(isAsync); - REQUIRE(ADUC_WorkflowData_GetLastReportedState(workflowData) == ADUCITF_State_InstallStarted); - REQUIRE(ADUC_WorkflowData_GetCurrentAction(workflowData) == ADUCITF_UpdateAction_ProcessDeployment); - REQUIRE(workflow_get_current_workflowstep(workflowData->WorkflowHandle) == ADUCITF_WorkflowStep_Install); - - REQUIRE(workflowData->IsRegistered == false); - REQUIRE(workflow_get_operation_in_progress(workflowData->WorkflowHandle) == true); - REQUIRE(workflow_get_operation_cancel_requested(workflowData->WorkflowHandle) == false); - break; - - case 3: - // - // Apply - // - REQUIRE(isAsync); - REQUIRE(ADUC_WorkflowData_GetLastReportedState(workflowData) == ADUCITF_State_ApplyStarted); - REQUIRE(ADUC_WorkflowData_GetCurrentAction(workflowData) == ADUCITF_UpdateAction_ProcessDeployment); - REQUIRE(workflow_get_current_workflowstep(workflowData->WorkflowHandle) == ADUCITF_WorkflowStep_Apply); - - REQUIRE(workflowData->IsRegistered == false); - REQUIRE(workflow_get_operation_in_progress(workflowData->WorkflowHandle) == true); - REQUIRE(workflow_get_operation_cancel_requested(workflowData->WorkflowHandle) == false); - - s_platform_idle_callback = workflowData->UpdateActionCallbacks.IdleCallback; - workflowData->UpdateActionCallbacks.IdleCallback = Mock_Idle_Callback; - - { - std::unique_lock lock(cv_mutex); - - s_workflowBeforeRebootIsDone = true; // update condition - - // need to manually unlock before notifying to prevent waking wait - // thread to only block again because lock was already taken - lock.unlock(); - workCompletionCallbackCV.notify_one(); - } - break; - - default: - REQUIRE(false); - break; - } - - ++s_mockWorkCompletionCallbackCallCount; - - // Call the normal work completion callback to continue the workflow processing - ADUC_Workflow_WorkCompletionCallback(workCompletionToken, result, isAsync); - } -} - -// Mock the content handler so that these tests do not require the simulator platform or simulator content handler. -// A pointer to mock content handler instance will be set in the workflowData in the below tests. -class MockContentHandler : public ContentHandler -{ -public: - // Delete copy ctor, copy assignment, move ctor and move assignment operators. - MockContentHandler(const MockContentHandler&) = delete; - MockContentHandler& operator=(const MockContentHandler&) = delete; - MockContentHandler(MockContentHandler&&) = delete; - MockContentHandler& operator=(MockContentHandler&&) = delete; - - MockContentHandler() = default; - ~MockContentHandler() override = default; - - ADUC_Result Download(const ADUC_WorkflowData* workflowData) override { - UNREFERENCED_PARAMETER(workflowData); - - ADUC_Result result = { s_download_result_code, 0 }; - return result; - } - - ADUC_Result Install(const ADUC_WorkflowData* workflowData) override { - UNREFERENCED_PARAMETER(workflowData); - - ADUC_Result result = { s_install_result_code, 0 }; - - switch (result.ResultCode) - { - case ADUC_Result_Install_RequiredImmediateAgentRestart: - workflow_request_immediate_agent_restart(workflowData->WorkflowHandle); - break; - case ADUC_Result_Install_RequiredAgentRestart: - workflow_request_agent_restart(workflowData->WorkflowHandle); - break; - case ADUC_Result_Install_RequiredImmediateReboot: - workflow_request_immediate_reboot(workflowData->WorkflowHandle); - break; - case ADUC_Result_Install_RequiredReboot: - workflow_request_reboot(workflowData->WorkflowHandle); - break; - } - - return result; - } - - ADUC_Result Apply(const ADUC_WorkflowData* workflowData) override { - UNREFERENCED_PARAMETER(workflowData); - - ADUC_Result result = { s_apply_result_code, 0 }; - - switch (result.ResultCode) - { - case ADUC_Result_Apply_RequiredImmediateAgentRestart: - workflow_request_immediate_agent_restart(workflowData->WorkflowHandle); - break; - case ADUC_Result_Apply_RequiredAgentRestart: - workflow_request_agent_restart(workflowData->WorkflowHandle); - break; - case ADUC_Result_Apply_RequiredImmediateReboot: - workflow_request_immediate_reboot(workflowData->WorkflowHandle); - break; - case ADUC_Result_Apply_RequiredReboot: - workflow_request_reboot(workflowData->WorkflowHandle); - break; - } - - return result; - } - - ADUC_Result Cancel(const ADUC_WorkflowData* workflowData) override { - UNREFERENCED_PARAMETER(workflowData); - - ADUC_Result result = { s_cancel_result_code, 0 }; - return result; - } - - ADUC_Result IsInstalled(const ADUC_WorkflowData* workflowData) override { - UNREFERENCED_PARAMETER(workflowData); - - ADUC_Result result = { s_isInstalled_result_code, 0 }; - return result; - } -}; - -class ADUC_Test_ReportPropertyAsyncValues -{ -public: - void - set(void* deviceHandleIn, - const unsigned char* reportedStateIn, - size_t reportedStateLenIn, - IOTHUB_CLIENT_REPORTED_STATE_CALLBACK_TYPE reportedStateCallbackIn, - void* userContextCallbackIn) - { - deviceHandle = deviceHandleIn; - - // we interpret the octets to be bytes of a string as it is a json string (utf-8 or ascii is fine). - // copy the bytes into the std::string wholesale which is bitwise correct. - std::string reportedState; - reportedState.clear(); - reportedState.reserve(reportedStateLenIn); - - // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) - std::for_each(reportedStateIn, reportedStateIn + reportedStateLenIn, [&](unsigned char c) { - reportedState.push_back(c); - }); - - reportedStates.push_back(reportedState); - - reportedStateCallback = reportedStateCallbackIn; - userContextCallback = userContextCallbackIn; - } - - std::vector reportedStates; - void* deviceHandle{ nullptr }; - IOTHUB_CLIENT_REPORTED_STATE_CALLBACK_TYPE reportedStateCallback{ nullptr }; - const void* userContextCallback{ nullptr }; -} s_SendReportedStateValues; - -// NOLINTNEXTLINE(readability-non-const-parameter) -static ADUC_Result Mock_SandboxCreateCallback(ADUC_Token token, const char* workflowId, char* workFolder) -{ - UNREFERENCED_PARAMETER(token); - UNREFERENCED_PARAMETER(workflowId); - UNREFERENCED_PARAMETER(workFolder); - - ADUC_Result result = { ADUC_Result_Success, 0 }; - return result; -} - -static void Mock_SandboxDestroyCallback(ADUC_Token token, const char* workflowId, const char* workFolder) -{ - UNREFERENCED_PARAMETER(token); - UNREFERENCED_PARAMETER(workflowId); - UNREFERENCED_PARAMETER(workFolder); -} - -static void Mock_IdleCallback(ADUC_Token token, const char* workflowId) -{ - UNREFERENCED_PARAMETER(token); - CHECK(s_expectedWorkflowIdWhenIdle == workflowId); - s_workflowComplete = true; -} - -static ADUC_Result Mock_IsInstalledCallback(ADUC_Token token, ADUC_WorkflowDataToken workflowData) -{ - UNREFERENCED_PARAMETER(token); - UNREFERENCED_PARAMETER(workflowData); - - ADUC_Result result = { ADUC_Result_IsInstalled_Installed, 0 }; - return result; -} - -static int Mock_RebootSystem() -{ - ++s_Mock_RebootSystem_call_count; - return s_reboot_system_return_code; -} - -static IOTHUB_CLIENT_RESULT_TYPE mockClientHandle_SendReportedState( - ADUC_CLIENT_HANDLE_TYPE deviceHandle, - const unsigned char* reportedState, - size_t reportedStateLen, - IOTHUB_CLIENT_REPORTED_STATE_CALLBACK_TYPE reportedStateCallback, - void* userContextCallback) -{ - s_SendReportedStateValues.set( - deviceHandle, reportedState, reportedStateLen, reportedStateCallback, userContextCallback); - - return ADUC_IOTHUB_CLIENT_OK; -} - -static void wait_for_workflow_complete() -{ - const unsigned MaxIterations = 100; - const unsigned SleepIntervalInMs = 10; - - unsigned count = 0; - while (count++ < MaxIterations && !s_workflowComplete) - { - std::this_thread::sleep_for(std::chrono::milliseconds(SleepIntervalInMs)); - } - CHECK(s_workflowComplete); -} - -static void Reset_Mocks_State() -{ - s_mockWorkCompletionCallbackCallCount = 0; - s_workflowComplete = false; - s_expectedWorkflowIdWhenIdle = ""; - - s_download_result_code = ADUC_Result_Download_Success; - s_install_result_code = ADUC_Result_Install_Success; - s_apply_result_code = ADUC_Result_Apply_Success; - s_cancel_result_code = ADUC_Result_Cancel_Success; - s_isInstalled_result_code = ADUC_Result_IsInstalled_NotInstalled; - - s_reboot_system_return_code = 0; - s_Mock_RebootSystem_call_count = 0; - - s_workflowBeforeRebootIsDone = false; - s_idleDone = false; - - s_SendReportedStateValues.reportedStates.clear(); -} - -class TestCaseFixture -{ -public: - TestCaseFixture() - { - m_previousDeviceHandle = g_iotHubClientHandleForADUComponent; - g_iotHubClientHandleForADUComponent = (void*)ADUC_ClientHandle_Invalid; // NOLINT - } - - ~TestCaseFixture() - { - g_iotHubClientHandleForADUComponent = m_previousDeviceHandle; - } - - TestCaseFixture(const TestCaseFixture&) = delete; - TestCaseFixture& operator=(const TestCaseFixture&) = delete; - TestCaseFixture(TestCaseFixture&&) = delete; - TestCaseFixture& operator=(TestCaseFixture&&) = delete; - -private: - void* m_previousDeviceHandle; -}; - -class WorkflowRebootManagedWorkflowData -{ - ADUC_WorkflowData m_workflowData{}; - -public: - WorkflowRebootManagedWorkflowData() - { - // - // Setup test hooks - // - - auto hooks = (ADUC_TestOverride_Hooks*)malloc(sizeof(ADUC_TestOverride_Hooks)); // NOLINT - memset(hooks, 0, sizeof(*hooks)); - - // Intercept the operation function completion callbacks to make assertions and - // then pass thru to ADUC_Workflow_WorkCompletionCallback to continue workflow processing. - hooks->WorkCompletionCallbackFunc_TestOverride = Mock_WorkCompletionCallback; - - // Use a mock content handler - hooks->ContentHandler_TestOverride = new MockContentHandler; - - // Don't actually reboot the system - hooks->RebootSystemFunc_TestOverride = Mock_RebootSystem; - - // Mock the low level actual client reporting so we can verify the client reporting string - hooks->ClientHandle_SendReportedStateFunc_TestOverride = (void*)mockClientHandle_SendReportedState; // NOLINT - - m_workflowData.TestOverrides = hooks; - - // - // Setup UpdateActionCallbacks - // - - ADUC_Result result = ADUC_MethodCall_Register(&(m_workflowData.UpdateActionCallbacks), 0 /*argc*/, nullptr /*argv*/); - REQUIRE(IsAducResultCodeSuccess(result.ResultCode)); - REQUIRE(result.ExtendedResultCode == 0); - m_workflowData.IsRegistered = false; // we will cleanup in dtor instead of AzureDeviceUpdateCoreInterface_Destroy - - m_workflowData.UpdateActionCallbacks.SandboxCreateCallback = Mock_SandboxCreateCallback; - m_workflowData.UpdateActionCallbacks.SandboxDestroyCallback = Mock_SandboxDestroyCallback; - m_workflowData.UpdateActionCallbacks.IdleCallback = Mock_IdleCallback; - - // setup other workflow state - m_workflowData.DownloadProgressCallback = Mock_DownloadProgressCallback; - m_workflowData.ReportStateAndResultAsyncCallback = AzureDeviceUpdateCoreInterface_ReportStateAndResultAsync; - m_workflowData.LastReportedState = ADUCITF_State_Idle; - } - - ~WorkflowRebootManagedWorkflowData() - { - free(m_workflowData.TestOverrides->ContentHandler_TestOverride); // NOLINT - delete m_workflowData.TestOverrides; - m_workflowData.TestOverrides = nullptr; - - ADUC_MethodCall_Unregister(&m_workflowData.UpdateActionCallbacks); - } - - ADUC_WorkflowData& GetWorkflowData() - { - return m_workflowData; - } - - WorkflowRebootManagedWorkflowData(const WorkflowRebootManagedWorkflowData&) = delete; - WorkflowRebootManagedWorkflowData& operator=(const WorkflowRebootManagedWorkflowData&) = delete; - WorkflowRebootManagedWorkflowData(WorkflowRebootManagedWorkflowData&&) = delete; - WorkflowRebootManagedWorkflowData& operator=(WorkflowRebootManagedWorkflowData&&) = delete; -}; - -// This test processes a workflow that requires reboot during Apply phase. -// The actual reboot action is mocked and startup is simulated afterwards by -// calling ADUC_Workflow_HandleStartupWorkflowData and then HandlePropertyUpdate. -// It asserts that proper reporting occurs after the reboot. -// The mock content handler above controls the ResultCode of Apply phase -// to result in reboot. -TEST_CASE_METHOD(TestCaseFixture, "Process Workflow Apply - Reboot Success") -{ - Reset_Mocks_State(); - - const mode_t mode = S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH; - if (::mkdir("/tmp/adu", mode) == -1) - { - REQUIRE(errno == EEXIST); - } - - if (::mkdir("/tmp/adu/workflow_reboot_ut", mode) == -1) - { - REQUIRE(errno == EEXIST); - } - - s_expectedWorkflowIdWhenIdle = "e99c69ca-3188-43a3-80af-310616c7751d"; - - std::mutex workCompletionCallbackMTX; - std::unique_lock lock(workCompletionCallbackMTX); - - WorkflowRebootManagedWorkflowData managedWorkflowDataBeforeReboot; - auto workflowData = managedWorkflowDataBeforeReboot.GetWorkflowData(); - - // - // Setup reboot - // - s_apply_result_code = ADUC_Result_Apply_RequiredImmediateReboot; // require reboot during Apply - s_reboot_system_return_code = 0; // Reboot operation will succeed - - // Initiate workflow processing due to PnP property change - workflowData.WorkflowHandle = nullptr; - workflowData.StartupIdleCallSent = true; - - std::string workflow_test_process_deployment = slurpTextFile(std::string{ ADUC_TEST_DATA_FOLDER } + "/workflow_reboot/updateActionForActionBundle.json"); - ADUC_Workflow_HandlePropertyUpdate(&workflowData, reinterpret_cast(workflow_test_process_deployment.c_str()), false /* forceDeferral */); // NOLINT - - { - std::unique_lock lock(cv_mutex); - workCompletionCallbackCV.wait(lock, [] { return s_workflowBeforeRebootIsDone; }); - } - - // Wait again for it to go to Idle - { - std::unique_lock lock(cv_mutex); - workCompletionCallbackCV.wait(lock, [] { return s_idleDone; }); - } - - wait_for_workflow_complete(); - - // Assert that reboot occurred - CHECK(s_Mock_RebootSystem_call_count == 1); - CHECK(workflowData.SystemRebootState == ADUC_SystemRebootState_InProgress); - - // - // Simulate post reboot after this line - // - - Reset_Mocks_State(); - s_expectedWorkflowIdWhenIdle = "e99c69ca-3188-43a3-80af-310616c7751d"; - - // This simulates when workflowdata is created when adu interface has just connected - WorkflowRebootManagedWorkflowData managedStartupWorkflowDataAfterReboot; - auto startupWorkflowDataAfterReboot = managedStartupWorkflowDataAfterReboot.GetWorkflowData(); - - // After reboot, have mock content handler report the update was installed successfully. - startupWorkflowDataAfterReboot.UpdateActionCallbacks.IsInstalledCallback = Mock_IsInstalledCallback; - - // Do Startup after reboot now. - // Call HandleStartupWorkflowData with NULL workflowHandle and - // then call HandlePropertyUpdate with latest twin JSON. - // Ensure that was in progress properly when it goes to idle - ADUC_Workflow_HandleStartupWorkflowData(&startupWorkflowDataAfterReboot); - ADUC_Workflow_HandlePropertyUpdate(&startupWorkflowDataAfterReboot, reinterpret_cast(workflow_test_process_deployment.c_str()), false /* forceDeferral */); - - CHECK(s_SendReportedStateValues.reportedStates.size() == 1); - - std::string expectedClientReportingString = slurpTextFile(std::string{ ADUC_TEST_DATA_FOLDER } + "/workflow_reboot/expectedClientReportingStringAfterReboot.json"); - JSON_Value* value = json_parse_string((s_SendReportedStateValues.reportedStates[0]).c_str()); - REQUIRE(value != nullptr); - - std::string actualClientReportingString_formatted = json_serialize_to_string_pretty(value); - REQUIRE_THAT(actualClientReportingString_formatted + "\n", Equals(expectedClientReportingString)); - - REQUIRE_THAT(startupWorkflowDataAfterReboot.LastCompletedWorkflowId, Equals("e99c69ca-3188-43a3-80af-310616c7751d")); - - wait_for_workflow_complete(); - - // Now simulate a duplicate workflow request due to token expiry connection refresh - s_SendReportedStateValues.reportedStates.clear(); - ADUC_Workflow_HandlePropertyUpdate(&startupWorkflowDataAfterReboot, reinterpret_cast(workflow_test_process_deployment.c_str()), false /* forceDeferral */); - CHECK(s_SendReportedStateValues.reportedStates.empty()); // did not do a duplicate report but ignored it - - wait_for_workflow_complete(); -} diff --git a/src/agent/adu_core_interface/tests/workflow_replacement_ut.cpp b/src/agent/adu_core_interface/tests/workflow_replacement_ut.cpp deleted file mode 100644 index 016e1b317..000000000 --- a/src/agent/adu_core_interface/tests/workflow_replacement_ut.cpp +++ /dev/null @@ -1,669 +0,0 @@ -/** - * @file adu_core_export_helpers_ut.cpp - * @brief Unit tests for adu_core_export_helpers.h - * - * @copyright Copyright (c) Microsoft Corp. - * Licensed under the MIT License. - */ -#include -#include -using Catch::Matchers::Equals; -#include -#include -#include -#include -#include -#include -#include - -#include "aduc/adu_core_export_helpers.h" -#include "aduc/adu_core_interface.h" -#include "aduc/agent_workflow.h" -#include "aduc/c_utils.h" -#include "aduc/client_handle.h" -#include "aduc/content_handler.hpp" -#include "aduc/result.h" -#include "aduc/types/workflow.h" -#include "aduc/workflow_data_utils.h" -#include "aduc/workflow_internal.h" -#include "aduc/workflow_utils.h" - -// clang-format off -static const char* workflow_test_process_deployment = - R"( { )" - R"( "workflow": { )" - R"( "action": 3, )" - R"( "id": "action_bundle" )" - R"( }, )" - R"( "updateManifest": "{\"manifestVersion\":\"4\",\"updateId\":{\"provider\":\"Contoso\",\"name\":\"Virtual-Vacuum\",\"version\":\"20.0\"},\"compatibility\":[{\"deviceManufacturer\":\"contoso\",\"deviceModel\":\"virtual-vacuum-v1\"}],\"instructions\":{\"steps\":[{\"handler\":\"microsoft/apt:1\",\"files\":[\"f483750ebb885d32c\"],\"handlerProperties\":{\"installedCriteria\":\"apt-update-tree-1.0\"}},{\"type\":\"reference\",\"detachedManifestFileId\":\"f222b9ffefaaac577\"}]},\"files\":{\"f483750ebb885d32c\":{\"fileName\":\"apt-manifest-tree-1.0.json\",\"sizeInBytes\":136,\"hashes\":{\"sha256\":\"Uk1vsEL/nT4btMngo0YSJjheOL2aqm6/EAFhzPb0rXs=\"}},\"f222b9ffefaaac577\":{\"fileName\":\"contoso.contoso-virtual-motors.1.1.updatemanifest.json\",\"sizeInBytes\":1031,\"hashes\":{\"sha256\":\"9Rnjw7ThZhGacOGn3uvvVq0ccQTHc/UFSL9khR2oKsc=\"}}},\"createdDateTime\":\"2022-01-27T13:45:05.8993329Z\"}", )" - R"( "updateManifestSignature": "eyJhbGciOiJSUzI1NiIsInNqd2siOiJleUpoYkdjaU9pSlNVekkxTmlJc0ltdHBaQ0k2SWtGRVZTNHlNREEzTURJdVVpSjkuZXlKcmRIa2lPaUpTVTBFaUxDSnVJam9pYkV4bWMwdHZPRmwwWW1Oak1sRXpUalV3VlhSTVNXWlhVVXhXVTBGRlltTm9LMFl2WTJVM1V6Rlpja3BvV0U5VGNucFRaa051VEhCVmFYRlFWSGMwZWxndmRHbEJja0ZGZFhrM1JFRmxWVzVGU0VWamVEZE9hM2QzZVRVdk9IcExaV3AyWTBWWWNFRktMMlV6UWt0SE5FVTBiMjVtU0ZGRmNFOXplSGRQUzBWbFJ6QkhkamwzVjB3emVsUmpUblprUzFoUFJGaEdNMVZRWlVveGIwZGlVRkZ0Y3pKNmJVTktlRUppZEZOSldVbDBiWFpwWTNneVpXdGtWbnBYUm5jdmRrdFVUblZMYXpob2NVczNTRkptYWs5VlMzVkxXSGxqSzNsSVVVa3dZVVpDY2pKNmEyc3plR2d4ZEVWUFN6azRWMHBtZUdKamFsQnpSRTgyWjNwWmVtdFlla05OZW1Fd1R6QkhhV0pDWjB4QlZGUTVUV1k0V1ZCd1dVY3lhblpQWVVSVmIwTlJiakpWWTFWU1RtUnNPR2hLWW5scWJscHZNa3B5SzFVNE5IbDFjVTlyTjBZMFdubFRiMEoyTkdKWVNrZ3lXbEpTV2tab0wzVlRiSE5XT1hkU2JWbG9XWEoyT1RGRVdtbHhhemhJVWpaRVUyeHVabTVsZFRJNFJsUm9SVzF0YjNOVlRUTnJNbGxNYzBKak5FSnZkWEIwTTNsaFNEaFpia3BVTnpSMU16TjFlakU1TDAxNlZIVnFTMmMzVkdGcE1USXJXR0owYmxwRU9XcFVSMkY1U25Sc2FFWmxWeXRJUXpVM1FYUkJSbHBvY1ZsM2VVZHJXQ3M0TTBGaFVGaGFOR0V4VHpoMU1qTk9WVWQxTWtGd04yOU5NVTR3ZVVKS0swbHNUM29pTENKbElqb2lRVkZCUWlJc0ltRnNaeUk2SWxKVE1qVTJJaXdpYTJsa0lqb2lRVVJWTGpJeE1EWXdPUzVTTGxNaWZRLlJLS2VBZE02dGFjdWZpSVU3eTV2S3dsNFpQLURMNnEteHlrTndEdkljZFpIaTBIa2RIZ1V2WnoyZzZCTmpLS21WTU92dXp6TjhEczhybXo1dnMwT1RJN2tYUG1YeDZFLUYyUXVoUXNxT3J5LS1aN2J3TW5LYTNkZk1sbkthWU9PdURtV252RWMyR0hWdVVTSzREbmw0TE9vTTQxOVlMNThWTDAtSEthU18xYmNOUDhXYjVZR08xZXh1RmpiVGtIZkNIU0duVThJeUFjczlGTjhUT3JETHZpVEtwcWtvM3RiSUwxZE1TN3NhLWJkZExUVWp6TnVLTmFpNnpIWTdSanZGbjhjUDN6R2xjQnN1aVQ0XzVVaDZ0M05rZW1UdV9tZjdtZUFLLTBTMTAzMFpSNnNTR281azgtTE1sX0ZaUmh4djNFZFNtR2RBUTNlMDVMRzNnVVAyNzhTQWVzWHhNQUlHWmcxUFE3aEpoZGZHdmVGanJNdkdTSVFEM09wRnEtZHREcEFXbUo2Zm5sZFA1UWxYek5tQkJTMlZRQUtXZU9BYjh0Yjl5aVhsemhtT1dLRjF4SzlseHpYUG9GNmllOFRUWlJ4T0hxTjNiSkVISkVoQmVLclh6YkViV2tFNm4zTEoxbkd5M1htUlVFcER0Umdpa0tBUzZybFhFT0VneXNjIn0.eyJzaGEyNTYiOiJqSW12eGpsc2pqZ29JeUJuYThuZTk2d0RYYlVsU3N6eGFoM0NibkF6STFJPSJ9.PzpvU13h6VhN8VHXUTYKAlpDW5t3JaQ-gs895_Q10XshKPYpeZUtViXGHGC-aQSQAYPhhYV-lLia9niXzZz4Qs4ehwFLHJfkmKR8eRwWvoOgJtAY0IIUA_8SeShmoOc9cdpC35N3OeaM4hV9shxvvrphDib5sLpkrv3LQrt3DHvK_L2n0HsybC-pwS7MzaSUIYoU-fXwZo6x3z7IbSaSNwS0P-50qeV99Mc0AUSIvB26GjmjZ2gEH5R3YD9kp0DOrYvE5tIymVHPTqkmunv2OrjKu2UOhNj8Om3RoVzxIkVM89cVGb1u1yB2kxEmXogXPz64cKqQWm22tV-jalS4dAc_1p9A9sKzZ632HxnlavOBjTKDGFgM95gg8M5npXBP3QIvkwW3yervCukViRUKIm-ljpDmnBJsZTMx0uzTaAk5XgoCUCADuLLol8EXB-0V4m2w-6tV6kAzRiwkqw1PRrGqplf-gmfU7TuFlQ142-EZLU5rK_dAiQRXx-f7LxNH", )" - R"( "fileUrls": { )" - R"( "f483750ebb885d32c": "http://duinstance2.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/e5cc19d5e9174c93ada35cc315f1fb1d/apt-manifest-tree-1.0.json", )" - R"( "f222b9ffefaaac577": "http://duinstance2.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/31c38c3340a84e38ae8d30ce340f4a49/contoso.contoso-virtual-motors.1.1.updatemanifest.json", )" - R"( "f2c5d1f3b0295db0f": "http://duinstance2.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/9ff068f7c2bf43eb9561da14a7cbcecd/motor-firmware-1.1.json", )" - R"( "f13b5435aab7c18da": "http://duinstance2.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/c02058a476a242d7bc0e3c576c180051/contoso-motor-installscript.sh" )" - R"( } )" - R"( } )"; - -static const char* workflow_test_process_deployment_REPLACEMENT = - R"( { )" - R"( "workflow": { )" - R"( "action": 3, )" - R"( "id": "REPLACEMENT_bundle_update" )" - R"( }, )" - R"( "updateManifest": "{\"manifestVersion\":\"4\",\"updateId\":{\"provider\":\"Contoso\",\"name\":\"Virtual-Vacuum\",\"version\":\"20.0\"},\"compatibility\":[{\"deviceManufacturer\":\"contoso\",\"deviceModel\":\"virtual-vacuum-v1\"}],\"instructions\":{\"steps\":[{\"handler\":\"microsoft/apt:1\",\"files\":[\"f483750ebb885d32c\"],\"handlerProperties\":{\"installedCriteria\":\"apt-update-tree-1.0\"}},{\"type\":\"reference\",\"detachedManifestFileId\":\"f222b9ffefaaac577\"}]},\"files\":{\"f483750ebb885d32c\":{\"fileName\":\"apt-manifest-tree-1.0.json\",\"sizeInBytes\":136,\"hashes\":{\"sha256\":\"Uk1vsEL/nT4btMngo0YSJjheOL2aqm6/EAFhzPb0rXs=\"}},\"f222b9ffefaaac577\":{\"fileName\":\"contoso.contoso-virtual-motors.1.1.updatemanifest.json\",\"sizeInBytes\":1031,\"hashes\":{\"sha256\":\"9Rnjw7ThZhGacOGn3uvvVq0ccQTHc/UFSL9khR2oKsc=\"}}},\"createdDateTime\":\"2022-01-27T13:45:05.8993329Z\"}", )" - R"( "updateManifestSignature": "eyJhbGciOiJSUzI1NiIsInNqd2siOiJleUpoYkdjaU9pSlNVekkxTmlJc0ltdHBaQ0k2SWtGRVZTNHlNREEzTURJdVVpSjkuZXlKcmRIa2lPaUpTVTBFaUxDSnVJam9pYkV4bWMwdHZPRmwwWW1Oak1sRXpUalV3VlhSTVNXWlhVVXhXVTBGRlltTm9LMFl2WTJVM1V6Rlpja3BvV0U5VGNucFRaa051VEhCVmFYRlFWSGMwZWxndmRHbEJja0ZGZFhrM1JFRmxWVzVGU0VWamVEZE9hM2QzZVRVdk9IcExaV3AyWTBWWWNFRktMMlV6UWt0SE5FVTBiMjVtU0ZGRmNFOXplSGRQUzBWbFJ6QkhkamwzVjB3emVsUmpUblprUzFoUFJGaEdNMVZRWlVveGIwZGlVRkZ0Y3pKNmJVTktlRUppZEZOSldVbDBiWFpwWTNneVpXdGtWbnBYUm5jdmRrdFVUblZMYXpob2NVczNTRkptYWs5VlMzVkxXSGxqSzNsSVVVa3dZVVpDY2pKNmEyc3plR2d4ZEVWUFN6azRWMHBtZUdKamFsQnpSRTgyWjNwWmVtdFlla05OZW1Fd1R6QkhhV0pDWjB4QlZGUTVUV1k0V1ZCd1dVY3lhblpQWVVSVmIwTlJiakpWWTFWU1RtUnNPR2hLWW5scWJscHZNa3B5SzFVNE5IbDFjVTlyTjBZMFdubFRiMEoyTkdKWVNrZ3lXbEpTV2tab0wzVlRiSE5XT1hkU2JWbG9XWEoyT1RGRVdtbHhhemhJVWpaRVUyeHVabTVsZFRJNFJsUm9SVzF0YjNOVlRUTnJNbGxNYzBKak5FSnZkWEIwTTNsaFNEaFpia3BVTnpSMU16TjFlakU1TDAxNlZIVnFTMmMzVkdGcE1USXJXR0owYmxwRU9XcFVSMkY1U25Sc2FFWmxWeXRJUXpVM1FYUkJSbHBvY1ZsM2VVZHJXQ3M0TTBGaFVGaGFOR0V4VHpoMU1qTk9WVWQxTWtGd04yOU5NVTR3ZVVKS0swbHNUM29pTENKbElqb2lRVkZCUWlJc0ltRnNaeUk2SWxKVE1qVTJJaXdpYTJsa0lqb2lRVVJWTGpJeE1EWXdPUzVTTGxNaWZRLlJLS2VBZE02dGFjdWZpSVU3eTV2S3dsNFpQLURMNnEteHlrTndEdkljZFpIaTBIa2RIZ1V2WnoyZzZCTmpLS21WTU92dXp6TjhEczhybXo1dnMwT1RJN2tYUG1YeDZFLUYyUXVoUXNxT3J5LS1aN2J3TW5LYTNkZk1sbkthWU9PdURtV252RWMyR0hWdVVTSzREbmw0TE9vTTQxOVlMNThWTDAtSEthU18xYmNOUDhXYjVZR08xZXh1RmpiVGtIZkNIU0duVThJeUFjczlGTjhUT3JETHZpVEtwcWtvM3RiSUwxZE1TN3NhLWJkZExUVWp6TnVLTmFpNnpIWTdSanZGbjhjUDN6R2xjQnN1aVQ0XzVVaDZ0M05rZW1UdV9tZjdtZUFLLTBTMTAzMFpSNnNTR281azgtTE1sX0ZaUmh4djNFZFNtR2RBUTNlMDVMRzNnVVAyNzhTQWVzWHhNQUlHWmcxUFE3aEpoZGZHdmVGanJNdkdTSVFEM09wRnEtZHREcEFXbUo2Zm5sZFA1UWxYek5tQkJTMlZRQUtXZU9BYjh0Yjl5aVhsemhtT1dLRjF4SzlseHpYUG9GNmllOFRUWlJ4T0hxTjNiSkVISkVoQmVLclh6YkViV2tFNm4zTEoxbkd5M1htUlVFcER0Umdpa0tBUzZybFhFT0VneXNjIn0.eyJzaGEyNTYiOiJqSW12eGpsc2pqZ29JeUJuYThuZTk2d0RYYlVsU3N6eGFoM0NibkF6STFJPSJ9.PzpvU13h6VhN8VHXUTYKAlpDW5t3JaQ-gs895_Q10XshKPYpeZUtViXGHGC-aQSQAYPhhYV-lLia9niXzZz4Qs4ehwFLHJfkmKR8eRwWvoOgJtAY0IIUA_8SeShmoOc9cdpC35N3OeaM4hV9shxvvrphDib5sLpkrv3LQrt3DHvK_L2n0HsybC-pwS7MzaSUIYoU-fXwZo6x3z7IbSaSNwS0P-50qeV99Mc0AUSIvB26GjmjZ2gEH5R3YD9kp0DOrYvE5tIymVHPTqkmunv2OrjKu2UOhNj8Om3RoVzxIkVM89cVGb1u1yB2kxEmXogXPz64cKqQWm22tV-jalS4dAc_1p9A9sKzZ632HxnlavOBjTKDGFgM95gg8M5npXBP3QIvkwW3yervCukViRUKIm-ljpDmnBJsZTMx0uzTaAk5XgoCUCADuLLol8EXB-0V4m2w-6tV6kAzRiwkqw1PRrGqplf-gmfU7TuFlQ142-EZLU5rK_dAiQRXx-f7LxNH", )" - R"( "fileUrls": { )" - R"( "f483750ebb885d32c": "http://duinstance2.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/e5cc19d5e9174c93ada35cc315f1fb1d/apt-manifest-tree-1.0.json", )" - R"( "f222b9ffefaaac577": "http://duinstance2.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/31c38c3340a84e38ae8d30ce340f4a49/contoso.contoso-virtual-motors.1.1.updatemanifest.json", )" - R"( "f2c5d1f3b0295db0f": "http://duinstance2.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/9ff068f7c2bf43eb9561da14a7cbcecd/motor-firmware-1.1.json", )" - R"( "f13b5435aab7c18da": "http://duinstance2.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/c02058a476a242d7bc0e3c576c180051/contoso-motor-installscript.sh" )" - R"( } )" - R"( } )"; - -static const char* expectedUpdateManifestJson = - R"( { )" - R"( "manifestVersion": "4", )" - R"( "updateId": { )" - R"( "provider": "Contoso", )" - R"( "name": "Virtual-Vacuum", )" - R"( "version": "20.0" )" - R"( }, )" - R"( "compatibility": [ )" - R"( { )" - R"( "deviceManufacturer": "contoso", )" - R"( "deviceModel": "virtual-vacuum-v1" )" - R"( } )" - R"( ], )" - R"( "instructions": { )" - R"( "steps": [ )" - R"( { )" - R"( "handler": "microsoft\/apt:1", )" - R"( "files": [ )" - R"( "f483750ebb885d32c" )" - R"( ], )" - R"( "handlerProperties": { )" - R"( "installedCriteria": "apt-update-tree-1.0" )" - R"( } )" - R"( }, )" - R"( { )" - R"( "type": "reference", )" - R"( "detachedManifestFileId": "f222b9ffefaaac577" )" - R"( } )" - R"( ] )" - R"( }, )" - R"( "files": { )" - R"( "f483750ebb885d32c": { )" - R"( "fileName": "apt-manifest-tree-1.0.json", )" - R"( "sizeInBytes": 136, )" - R"( "hashes": { )" - R"( "sha256": "Uk1vsEL\/nT4btMngo0YSJjheOL2aqm6\/EAFhzPb0rXs=" )" - R"( } )" - R"( }, )" - R"( "f222b9ffefaaac577": { )" - R"( "fileName": "contoso.contoso-virtual-motors.1.1.updatemanifest.json", )" - R"( "sizeInBytes": 1031, )" - R"( "hashes": { )" - R"( "sha256": "9Rnjw7ThZhGacOGn3uvvVq0ccQTHc\/UFSL9khR2oKsc=" )" - R"( } )" - R"( } )" - R"( }, )" - R"( "createdDateTime": "2022-01-27T13:45:05.8993329Z" )" - R"( } )"; - -EXTERN_C_BEGIN -// fwd-decl to completion signal handler fn. -void ADUC_Workflow_WorkCompletionCallback(const void* workCompletionToken, ADUC_Result result, bool isAsync); -EXTERN_C_END - -// Note: g_iotHubClientHandleForADUComponent declared in adu_core_intefaace.h - -// -// Test Helpers -// - -static uint8_t FacilityFromExtendedResultCode(ADUC_Result_t extendedResultCode) -{ - return (static_cast(extendedResultCode) >> 0x1C) & 0xF; -} - -static uint32_t CodeFromExtendedResultCode(ADUC_Result_t extendedResultCode) -{ - return extendedResultCode & 0xFFFFFFF; -} - -// Needs to be a define as INFO is method scope specific. -// Need to cast to uint16_t as catch2 doesn't have conversion for uint8_t. -#define INFO_ADUC_Result(result) \ - INFO( \ - "Code: " << (result).ResultCode << "; Extended: { 0x" << std::hex \ - << static_cast(FacilityFromExtendedResultCode((result).ExtendedResultCode)) << ", " \ - << CodeFromExtendedResultCode((result).ExtendedResultCode) << " }") - -static std::condition_variable workCompletionCallbackCV; -static std::condition_variable replacementCV; -static std::mutex replacementMutex; - -static bool s_downloadFirstWorkflow_completed = false; -static bool s_replacementWorkflowIsDone = false; -static bool s_idleDone = false; -static bool s_workflowComplete = false; -static std::string s_expectedWorkflowIdWhenIdle; - -#define ADUC_ClientHandle_Invalid (-1) - -static unsigned int s_mockWorkCompletionCallbackCallCount = 0; - -static IdleCallbackFunc s_platform_idle_callback = nullptr; - -static void Mock_Idle_Callback_for_REPLACEMENT(ADUC_Token token, const char* workflowId) -{ - CHECK(token != nullptr); - CHECK(workflowId != nullptr); - - CHECK(strcmp(workflowId, "REPLACEMENT_bundle_update") == 0); - - // call the original update callback - REQUIRE(s_platform_idle_callback != nullptr); - (*s_platform_idle_callback)(token, workflowId); - - // notify now so that test can cleanup - { - std::unique_lock lock(replacementMutex); - - s_idleDone = true; // update condition - - // need to manually unlock before notifying to prevent waking wait - // thread to only block again because lock was already taken - lock.unlock(); - replacementCV.notify_one(); - } -} - -extern "C" -{ - static void Mock_DownloadProgressCallback( - const char* /*workflowId*/, - const char* /*fileId*/, - ADUC_DownloadProgressState /*state*/, - uint64_t /*bytesTransferred*/, - uint64_t /*bytesTotal*/) - { - } - - static void Mock_WorkCompletionCallback_for_REPLACEMENT(const void* workCompletionToken, ADUC_Result result, bool isAsync) - { - CHECK(workCompletionToken != nullptr); - - auto methodCallData = (ADUC_MethodCall_Data*)workCompletionToken; // NOLINT - auto workflowData = methodCallData->WorkflowData; - - switch (s_mockWorkCompletionCallbackCallCount) - { - case 0: - // - // Process Deployment { 1st workflow } - // - REQUIRE_FALSE(isAsync); - { - auto wf = (ADUC_Workflow*)workflowData->WorkflowHandle; // NOLINT - char* propertiesJson = json_serialize_to_string_pretty(json_object_get_wrapping_value(wf->PropertiesObject)); - CHECK_THAT(propertiesJson, Equals( - "{\n" - " \"_workFolder\": \"\\/var\\/lib\\/adu\\/downloads\\/action_bundle\"\n" - "}" - )); - } - REQUIRE(ADUC_WorkflowData_GetLastReportedState(workflowData) == ADUCITF_State_Idle); - REQUIRE(ADUC_WorkflowData_GetCurrentAction(workflowData) == ADUCITF_UpdateAction_ProcessDeployment); - REQUIRE(workflow_get_current_workflowstep(workflowData->WorkflowHandle) == ADUCITF_WorkflowStep_ProcessDeployment); - REQUIRE(workflow_get_operation_in_progress(workflowData->WorkflowHandle) == true); - REQUIRE(workflow_get_operation_cancel_requested(workflowData->WorkflowHandle) == false); - CHECK(workflow_get_cancellation_type(workflowData->WorkflowHandle) == ADUC_WorkflowCancellationType_None); - break; - - case 1: - // - // Download { 1st workflow } - // - // This is completing due to Cancellation by Replacement workflow, so Cancellation should've been requested. - // - REQUIRE(isAsync); - { - auto wf = static_cast(workflowData->WorkflowHandle); - char* propertiesJson = json_serialize_to_string_pretty(json_object_get_wrapping_value(wf->PropertiesObject)); - CHECK_THAT(propertiesJson, Equals( - "{\n" - " \"_workFolder\": \"\\/var\\/lib\\/adu\\/downloads\\/action_bundle\"\n" - "}" - )); - } - REQUIRE(ADUC_WorkflowData_GetLastReportedState(workflowData) == ADUCITF_State_DownloadStarted); - REQUIRE(ADUC_WorkflowData_GetCurrentAction(workflowData) == ADUCITF_UpdateAction_ProcessDeployment); - REQUIRE(workflow_get_current_workflowstep(workflowData->WorkflowHandle) == ADUCITF_WorkflowStep_Download); - - REQUIRE(workflowData->IsRegistered == false); - REQUIRE(workflow_get_operation_in_progress(workflowData->WorkflowHandle) == true); - CHECK(workflow_get_operation_cancel_requested(workflowData->WorkflowHandle) == true); // Cancellation - CHECK(workflow_get_cancellation_type(workflowData->WorkflowHandle) == ADUC_WorkflowCancellationType_Replacement); // Should be Replacement Cancellation Type - break; - - // The remainder of the "script" is the same as the non-replacement completion callback above, which is successful processing - // of each WorkflowStep phase. - case 2: - // - // Process Deployment - // - REQUIRE_FALSE(isAsync); - { - auto wf = static_cast(workflowData->WorkflowHandle); - char* propertiesJson = json_serialize_to_string_pretty(json_object_get_wrapping_value(wf->PropertiesObject)); - CHECK_THAT(propertiesJson, Equals("{}")); - - char* updateActionObject = json_serialize_to_string_pretty(json_object_get_wrapping_value(wf->UpdateActionObject)); - CHECK_THAT(updateActionObject, Equals(json_serialize_to_string_pretty(json_parse_string(workflow_test_process_deployment_REPLACEMENT)))); - - char* updateManifestObject = json_serialize_to_string_pretty(json_object_get_wrapping_value(wf->UpdateManifestObject)); - CHECK_THAT(updateManifestObject, Equals(json_serialize_to_string_pretty(json_parse_string(expectedUpdateManifestJson)))); - } - - REQUIRE(ADUC_WorkflowData_GetLastReportedState(workflowData) == ADUCITF_State_Idle); - REQUIRE(ADUC_WorkflowData_GetCurrentAction(workflowData) == ADUCITF_UpdateAction_ProcessDeployment); - REQUIRE(workflow_get_current_workflowstep(workflowData->WorkflowHandle) == ADUCITF_WorkflowStep_ProcessDeployment); - REQUIRE(workflow_get_operation_in_progress(workflowData->WorkflowHandle) == true); - REQUIRE(workflow_get_operation_cancel_requested(workflowData->WorkflowHandle) == false); - CHECK(workflow_get_cancellation_type(workflowData->WorkflowHandle) == ADUC_WorkflowCancellationType_None); // Should be reset - break; - - case 3: - // - // Download - // - REQUIRE(isAsync); - { - auto wf = static_cast(workflowData->WorkflowHandle); - char* propertiesJson = json_serialize_to_string_pretty(json_object_get_wrapping_value(wf->PropertiesObject)); - REQUIRE(propertiesJson != nullptr); - CHECK_THAT(propertiesJson, Equals("{}")); - - char* updateActionObject = json_serialize_to_string_pretty(json_object_get_wrapping_value(wf->UpdateActionObject)); - REQUIRE(updateActionObject != nullptr); - CHECK_THAT(updateActionObject, Equals(json_serialize_to_string_pretty(json_parse_string(workflow_test_process_deployment_REPLACEMENT)))); - - char* updateManifestObject = json_serialize_to_string_pretty(json_object_get_wrapping_value(wf->UpdateManifestObject)); - REQUIRE(updateManifestObject != nullptr); - CHECK_THAT(updateManifestObject, Equals(json_serialize_to_string_pretty(json_parse_string(expectedUpdateManifestJson)))); - } - - REQUIRE(ADUC_WorkflowData_GetLastReportedState(workflowData) == ADUCITF_State_DownloadStarted); - REQUIRE(ADUC_WorkflowData_GetCurrentAction(workflowData) == ADUCITF_UpdateAction_ProcessDeployment); - REQUIRE(workflow_get_current_workflowstep(workflowData->WorkflowHandle) == ADUCITF_WorkflowStep_Download); - - REQUIRE(workflowData->IsRegistered == false); - REQUIRE(workflow_get_operation_in_progress(workflowData->WorkflowHandle) == true); - REQUIRE(workflow_get_operation_cancel_requested(workflowData->WorkflowHandle) == false); - break; - - case 4: - // - // Install - // - REQUIRE(isAsync); - { - auto wf = static_cast(workflowData->WorkflowHandle); - char* propertiesJson = json_serialize_to_string_pretty(json_object_get_wrapping_value(wf->PropertiesObject)); - REQUIRE(propertiesJson != nullptr); - CHECK_THAT(propertiesJson, Equals("{}")); - - char* updateActionObject = json_serialize_to_string_pretty(json_object_get_wrapping_value(wf->UpdateActionObject)); - REQUIRE(updateActionObject != nullptr); - CHECK_THAT(updateActionObject, Equals(json_serialize_to_string_pretty(json_parse_string(workflow_test_process_deployment_REPLACEMENT)))); - - char* updateManifestObject = json_serialize_to_string_pretty(json_object_get_wrapping_value(wf->UpdateManifestObject)); - REQUIRE(updateManifestObject != nullptr); - CHECK_THAT(updateManifestObject, Equals(json_serialize_to_string_pretty(json_parse_string(expectedUpdateManifestJson)))); - } - - REQUIRE(ADUC_WorkflowData_GetLastReportedState(workflowData) == ADUCITF_State_InstallStarted); - REQUIRE(ADUC_WorkflowData_GetCurrentAction(workflowData) == ADUCITF_UpdateAction_ProcessDeployment); - REQUIRE(workflow_get_current_workflowstep(workflowData->WorkflowHandle) == ADUCITF_WorkflowStep_Install); - - REQUIRE(workflowData->IsRegistered == false); - REQUIRE(workflow_get_operation_in_progress(workflowData->WorkflowHandle) == true); - REQUIRE(workflow_get_operation_cancel_requested(workflowData->WorkflowHandle) == false); - break; - - case 5: - // - // Apply - // - REQUIRE(isAsync); - { - auto wf = static_cast(workflowData->WorkflowHandle); - char* propertiesJson = json_serialize_to_string_pretty(json_object_get_wrapping_value(wf->PropertiesObject)); - CHECK_THAT(propertiesJson, Equals("{}")); - - char* updateActionObject = json_serialize_to_string_pretty(json_object_get_wrapping_value(wf->UpdateActionObject)); - CHECK_THAT(updateActionObject, Equals(json_serialize_to_string_pretty(json_parse_string(workflow_test_process_deployment_REPLACEMENT)))); - - char* updateManifestObject = json_serialize_to_string_pretty(json_object_get_wrapping_value(wf->UpdateManifestObject)); - CHECK_THAT(updateManifestObject, Equals(json_serialize_to_string_pretty(json_parse_string(expectedUpdateManifestJson)))); - } - - REQUIRE(ADUC_WorkflowData_GetLastReportedState(workflowData) == ADUCITF_State_ApplyStarted); - REQUIRE(ADUC_WorkflowData_GetCurrentAction(workflowData) == ADUCITF_UpdateAction_ProcessDeployment); - REQUIRE(workflow_get_current_workflowstep(workflowData->WorkflowHandle) == ADUCITF_WorkflowStep_Apply); - - REQUIRE(workflowData->IsRegistered == false); - REQUIRE(workflow_get_operation_in_progress(workflowData->WorkflowHandle) == true); - REQUIRE(workflow_get_operation_cancel_requested(workflowData->WorkflowHandle) == false); - - s_platform_idle_callback = workflowData->UpdateActionCallbacks.IdleCallback; - workflowData->UpdateActionCallbacks.IdleCallback = Mock_Idle_Callback_for_REPLACEMENT; - - { - std::unique_lock lock(replacementMutex); - - s_replacementWorkflowIsDone = true; // update condition - - // need to manually unlock before notifying to prevent waking wait - // thread to only block again because lock was already taken - lock.unlock(); - replacementCV.notify_one(); - } - break; - - default: - break; - - } - - ++s_mockWorkCompletionCallbackCallCount; - - // Call the normal work completion callback to continue the workflow processing - ADUC_Workflow_WorkCompletionCallback(workCompletionToken, result, isAsync); - } -} - -static ADUC_ResultCode s_download_result_code = ADUC_Result_Download_Success; -static ADUC_ResultCode s_install_result_code = ADUC_Result_Install_Success; -static ADUC_ResultCode s_apply_result_code = ADUC_Result_Apply_Success; -static ADUC_ResultCode s_cancel_result_code = ADUC_Result_Cancel_Success; -static ADUC_ResultCode s_isInstalled_result_code = ADUC_Result_IsInstalled_NotInstalled; - -static void Reset_Mocks_State() -{ - s_mockWorkCompletionCallbackCallCount = 0; - s_downloadFirstWorkflow_completed = false; - s_replacementWorkflowIsDone = false; - s_idleDone = false; - s_workflowComplete = false; - s_expectedWorkflowIdWhenIdle = ""; - - s_download_result_code = ADUC_Result_Download_Success; - s_install_result_code = ADUC_Result_Install_Success; - s_apply_result_code = ADUC_Result_Apply_Success; - s_cancel_result_code = ADUC_Result_Cancel_Success; - s_isInstalled_result_code = ADUC_Result_IsInstalled_NotInstalled; -} - -// Mock Content Handler that takes a long time in the download phase and waits there -// until it gets a Cancel call. -// It increments a counter so that when the 2nd ProcessDeployment comes in, it will -// not pause in continue for that one and continue processing the entire workflow. -class MockContentHandlerForReplacement : public ContentHandler -{ - unsigned int counter = 0; - bool received_cancel = false; - -public: - // Delete copy ctor, copy assignment, move ctor and move assignment operators. - MockContentHandlerForReplacement(const MockContentHandlerForReplacement&) = delete; - MockContentHandlerForReplacement& operator=(const MockContentHandlerForReplacement&) = delete; - MockContentHandlerForReplacement(MockContentHandlerForReplacement&&) = delete; - MockContentHandlerForReplacement& operator=(MockContentHandlerForReplacement&&) = delete; - - MockContentHandlerForReplacement() = default; - ~MockContentHandlerForReplacement() override = default; - - ADUC_Result Download(const ADUC_WorkflowData* workflowData) override { - UNREFERENCED_PARAMETER(workflowData); - - ADUC_Result result = { s_download_result_code }; - - if (counter++ == 0) - { - // This is the first workflow and we want to simulate a long-running download - // so that we can ensure that the replacement workflow that comes in will - // have an in-progress operation and workflow to replace. - - // poll with 500 millisecond interval until canceled - struct timeval half_sec - { - .tv_sec = 0, - .tv_usec = 500, - }; - - bool polling = false; - - while (!received_cancel) - { - select(0, nullptr, nullptr, nullptr, &half_sec); - - // Let the main thread test case move on once receive Cancel due to replacement - if (!polling) - { - std::unique_lock lock(replacementMutex); - - s_downloadFirstWorkflow_completed = true; // update condition - polling = true; - - // need to manually unlock before notifying to prevent waking wait - // thread to only block again because lock was already taken - lock.unlock(); - replacementCV.notify_one(); - } - } - } - // else it is processing the replacement workflow eventually notify_all when apply is done - // in Mock_WorkCompletionCallback_for_REPLACEMENT (3 more worker threads will created in - // succession via ADUC_Workflow_AutoTransitionWorkflow) - - return result; - } - - ADUC_Result Install(const ADUC_WorkflowData* workflowData) override { - UNREFERENCED_PARAMETER(workflowData); - - ADUC_Result result = { s_install_result_code }; - return result; - } - - ADUC_Result Apply(const ADUC_WorkflowData* workflowData) override { - UNREFERENCED_PARAMETER(workflowData); - - ADUC_Result result = { s_apply_result_code }; - return result; - } - - ADUC_Result Cancel(const ADUC_WorkflowData* workflowData) override { - UNREFERENCED_PARAMETER(workflowData); - - ADUC_Result result = { s_cancel_result_code }; - - // signal to exit poll loop in download - received_cancel = true; - - return result; - } - - ADUC_Result IsInstalled(const ADUC_WorkflowData* workflowData) override { - UNREFERENCED_PARAMETER(workflowData); - - ADUC_Result result = { s_isInstalled_result_code }; - return result; - } - -}; - -// NOLINTNEXTLINE(readability-non-const-parameter) -static ADUC_Result Mock_SandboxCreateCallback(ADUC_Token token, const char* workflowId, char* workFolder) -{ - UNREFERENCED_PARAMETER(token); - UNREFERENCED_PARAMETER(workflowId); - UNREFERENCED_PARAMETER(workFolder); - - ADUC_Result result = { ADUC_Result_Success }; - return result; -} - -static void Mock_SandboxDestroyCallback(ADUC_Token token, const char* workflowId, const char* workFolder) -{ - UNREFERENCED_PARAMETER(token); - UNREFERENCED_PARAMETER(workflowId); - UNREFERENCED_PARAMETER(workFolder); -} - -static void Mock_IdleCallback(ADUC_Token token, const char* workflowId) -{ - UNREFERENCED_PARAMETER(token); - - CHECK(s_expectedWorkflowIdWhenIdle == workflowId); - s_workflowComplete = true; -} - -static void wait_for_workflow_complete() -{ - const unsigned MaxIterations = 100; - const unsigned SleepIntervalInMs = 10; - - unsigned count = 0; - while (count++ < MaxIterations && !s_workflowComplete) - { - std::this_thread::sleep_for(std::chrono::milliseconds(SleepIntervalInMs)); - } - CHECK(s_workflowComplete); -} - -static IOTHUB_CLIENT_RESULT_TYPE mockClientHandle_SendReportedState( - ADUC_CLIENT_HANDLE_TYPE deviceHandle, - const unsigned char* reportedState, - size_t reportedStateLen, - IOTHUB_CLIENT_REPORTED_STATE_CALLBACK_TYPE reportedStateCallback, - void* userContextCallback) -{ - UNREFERENCED_PARAMETER(deviceHandle); - UNREFERENCED_PARAMETER(reportedState); - UNREFERENCED_PARAMETER(reportedStateLen); - UNREFERENCED_PARAMETER(reportedStateCallback); - UNREFERENCED_PARAMETER(userContextCallback); - - return ADUC_IOTHUB_CLIENT_OK; -} - -class TestCaseFixture -{ -public: - TestCaseFixture() - { - m_previousDeviceHandle = g_iotHubClientHandleForADUComponent; - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - g_iotHubClientHandleForADUComponent = reinterpret_cast(ADUC_ClientHandle_Invalid); - } - - ~TestCaseFixture() - { - g_iotHubClientHandleForADUComponent = m_previousDeviceHandle; - } - - TestCaseFixture(const TestCaseFixture&) = delete; - TestCaseFixture& operator=(const TestCaseFixture&) = delete; - TestCaseFixture(TestCaseFixture&&) = delete; - TestCaseFixture& operator=(TestCaseFixture&&) = delete; - -private: - ADUC_ClientHandle m_previousDeviceHandle; -}; - -// This test exercises the deployment Replacement logic when a deployment with a different workflow id comes -// in while a deployment is ongoing and the deferred processing of the next workflow during the -// workCompletionCallback of the first operation due to canceling. -TEST_CASE_METHOD(TestCaseFixture, "Process workflow - Replacement") -{ - Reset_Mocks_State(); - - s_expectedWorkflowIdWhenIdle = "REPLACEMENT_bundle_update"; - - MockContentHandlerForReplacement mockContentHandler; - - ADUC_WorkflowData workflowData{}; - - // set test overrides - ADUC_TestOverride_Hooks hooks{}; - hooks.WorkCompletionCallbackFunc_TestOverride = Mock_WorkCompletionCallback_for_REPLACEMENT; - hooks.ContentHandler_TestOverride = &mockContentHandler; - hooks.ClientHandle_SendReportedStateFunc_TestOverride = (void*)mockClientHandle_SendReportedState; // NOLINT - workflowData.TestOverrides = &hooks; - - ADUC_Result result = - ADUC_MethodCall_Register(&(workflowData.UpdateActionCallbacks), 0 /*argc*/, nullptr /*argv*/); - - workflowData.UpdateActionCallbacks.SandboxCreateCallback = Mock_SandboxCreateCallback; - workflowData.UpdateActionCallbacks.SandboxDestroyCallback = Mock_SandboxDestroyCallback; - - workflowData.DownloadProgressCallback = Mock_DownloadProgressCallback; - workflowData.ReportStateAndResultAsyncCallback = AzureDeviceUpdateCoreInterface_ReportStateAndResultAsync; - - workflowData.LastReportedState = ADUCITF_State_Idle; - - REQUIRE(IsAducResultCodeSuccess(result.ResultCode)); - REQUIRE(result.ExtendedResultCode == 0); - - // download result be failure for cancel first workflow - s_download_result_code = ADUC_Result_Failure_Cancelled; - - // simulating non-startup processing of twin - workflowData.WorkflowHandle = nullptr; - workflowData.StartupIdleCallSent = true; - - // NOLINTNEXTLINE - ADUC_Workflow_HandlePropertyUpdate(&workflowData, (const unsigned char*)workflow_test_process_deployment, false /* forceDeferral */); // workflow id => "bundle_update" - - // The mock content handler will loop with sleep during each poll iteration. It will exit poll loop when - // it receives Cancel in the content handler due to cancel request from 2nd ProcessDeployment update action - // coming in. - - // notify will come once mock content handler Download method starts polling for cancellation request. - { - std::unique_lock lock(replacementMutex); - replacementCV.wait(lock, [] { return s_downloadFirstWorkflow_completed; }); - } - - // Now we kick off the Replacement deployment, which should cause the first worker thread to exit - // poll loop in MockContent handler due to cancellation, and then that worker thread will do the auto-transition with - // the DeferredReplacementWorkflow that was saved in the current WorkflowData Handle. - - // NOLINTNEXTLINE - ADUC_Workflow_HandlePropertyUpdate(&workflowData, (const unsigned char*)workflow_test_process_deployment_REPLACEMENT, false /* forceDeferral */); // workflow id => "REPLACEMENT_bundle_update" - - // make download result be a success for the next workflow download - s_download_result_code = ADUC_Result_Download_Success; - - // Also hook into IdleCallback to know workflow is done and WorkflowHandle has been freed - workflowData.UpdateActionCallbacks.IdleCallback = Mock_IdleCallback; - - { - std::unique_lock lock(replacementMutex); - replacementCV.wait(lock, [] { return s_replacementWorkflowIsDone; }); - } - - // Wait again for it to go to Idle so we don't trash the workflowData by going out of scope - { - std::unique_lock lock(replacementMutex); - replacementCV.wait(lock, [] { return s_idleDone; }); - } - - wait_for_workflow_complete(); - - ADUC_MethodCall_Unregister(&workflowData.UpdateActionCallbacks); -} diff --git a/src/agent/adu_core_interface/tests/workflow_test_utils.cpp b/src/agent/adu_core_interface/tests/workflow_test_utils.cpp deleted file mode 100644 index bb13cd047..000000000 --- a/src/agent/adu_core_interface/tests/workflow_test_utils.cpp +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @file workflow_test_utils.c - * @brief The implementation of workflow test utilities. - * - * @copyright Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ - -#include "workflow_test_utils.h" - -#include -#include - -std::string slurpTextFile(const std::string& path) -{ - std::ifstream file_stream{path}; - std::stringstream buffer; - buffer << file_stream.rdbuf(); - return buffer.str(); -} diff --git a/src/agent/adu_core_interface/tests/workflow_test_utils.h b/src/agent/adu_core_interface/tests/workflow_test_utils.h deleted file mode 100644 index eee7252f7..000000000 --- a/src/agent/adu_core_interface/tests/workflow_test_utils.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @file workflow_test_utils.h - * @brief The header for workflow test utilities functions. - * - * @copyright Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ - -#ifndef __WORKFLOW_TEST_UTILS_H__ -#define __WORKFLOW_TEST_UTILS_H__ - -#include - -std::string slurpTextFile(const std::string& path); - -#endif // __WORKFLOW_TEST_UTILS_H__ diff --git a/src/agent/adu_core_interface/tests/workflow_ut.cpp b/src/agent/adu_core_interface/tests/workflow_ut.cpp deleted file mode 100644 index 237a35e45..000000000 --- a/src/agent/adu_core_interface/tests/workflow_ut.cpp +++ /dev/null @@ -1,375 +0,0 @@ -/** - * @file adu_core_export_helpers_ut.cpp - * @brief Unit tests for adu_core_export_helpers.h - * - * @copyright Copyright (c) Microsoft Corp. - * Licensed under the MIT License. - */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "aduc/adu_core_export_helpers.h" -#include "aduc/adu_core_interface.h" -#include "aduc/agent_workflow.h" -#include "aduc/c_utils.h" -#include "aduc/client_handle.h" -#include "aduc/content_handler.hpp" -#include "aduc/result.h" -#include "aduc/types/workflow.h" -#include "aduc/workflow_data_utils.h" -#include "aduc/workflow_internal.h" -#include "aduc/workflow_utils.h" - -// clang-format off -static const char* workflow_test_process_deployment = - R"( { )" - R"( "workflow": { )" - R"( "action": 3, )" - R"( "id": "action_bundle" )" - R"( }, )" - R"( "updateManifest": "{\"manifestVersion\":\"4\",\"updateId\":{\"provider\":\"Contoso\",\"name\":\"Virtual-Vacuum\",\"version\":\"20.0\"},\"compatibility\":[{\"deviceManufacturer\":\"contoso\",\"deviceModel\":\"virtual-vacuum-v1\"}],\"instructions\":{\"steps\":[{\"handler\":\"microsoft/apt:1\",\"files\":[\"f483750ebb885d32c\"],\"handlerProperties\":{\"installedCriteria\":\"apt-update-tree-1.0\"}},{\"type\":\"reference\",\"detachedManifestFileId\":\"f222b9ffefaaac577\"}]},\"files\":{\"f483750ebb885d32c\":{\"fileName\":\"apt-manifest-tree-1.0.json\",\"sizeInBytes\":136,\"hashes\":{\"sha256\":\"Uk1vsEL/nT4btMngo0YSJjheOL2aqm6/EAFhzPb0rXs=\"}},\"f222b9ffefaaac577\":{\"fileName\":\"contoso.contoso-virtual-motors.1.1.updatemanifest.json\",\"sizeInBytes\":1031,\"hashes\":{\"sha256\":\"9Rnjw7ThZhGacOGn3uvvVq0ccQTHc/UFSL9khR2oKsc=\"}}},\"createdDateTime\":\"2022-01-27T13:45:05.8993329Z\"}", )" - R"( "updateManifestSignature": "eyJhbGciOiJSUzI1NiIsInNqd2siOiJleUpoYkdjaU9pSlNVekkxTmlJc0ltdHBaQ0k2SWtGRVZTNHlNREEzTURJdVVpSjkuZXlKcmRIa2lPaUpTVTBFaUxDSnVJam9pYkV4bWMwdHZPRmwwWW1Oak1sRXpUalV3VlhSTVNXWlhVVXhXVTBGRlltTm9LMFl2WTJVM1V6Rlpja3BvV0U5VGNucFRaa051VEhCVmFYRlFWSGMwZWxndmRHbEJja0ZGZFhrM1JFRmxWVzVGU0VWamVEZE9hM2QzZVRVdk9IcExaV3AyWTBWWWNFRktMMlV6UWt0SE5FVTBiMjVtU0ZGRmNFOXplSGRQUzBWbFJ6QkhkamwzVjB3emVsUmpUblprUzFoUFJGaEdNMVZRWlVveGIwZGlVRkZ0Y3pKNmJVTktlRUppZEZOSldVbDBiWFpwWTNneVpXdGtWbnBYUm5jdmRrdFVUblZMYXpob2NVczNTRkptYWs5VlMzVkxXSGxqSzNsSVVVa3dZVVpDY2pKNmEyc3plR2d4ZEVWUFN6azRWMHBtZUdKamFsQnpSRTgyWjNwWmVtdFlla05OZW1Fd1R6QkhhV0pDWjB4QlZGUTVUV1k0V1ZCd1dVY3lhblpQWVVSVmIwTlJiakpWWTFWU1RtUnNPR2hLWW5scWJscHZNa3B5SzFVNE5IbDFjVTlyTjBZMFdubFRiMEoyTkdKWVNrZ3lXbEpTV2tab0wzVlRiSE5XT1hkU2JWbG9XWEoyT1RGRVdtbHhhemhJVWpaRVUyeHVabTVsZFRJNFJsUm9SVzF0YjNOVlRUTnJNbGxNYzBKak5FSnZkWEIwTTNsaFNEaFpia3BVTnpSMU16TjFlakU1TDAxNlZIVnFTMmMzVkdGcE1USXJXR0owYmxwRU9XcFVSMkY1U25Sc2FFWmxWeXRJUXpVM1FYUkJSbHBvY1ZsM2VVZHJXQ3M0TTBGaFVGaGFOR0V4VHpoMU1qTk9WVWQxTWtGd04yOU5NVTR3ZVVKS0swbHNUM29pTENKbElqb2lRVkZCUWlJc0ltRnNaeUk2SWxKVE1qVTJJaXdpYTJsa0lqb2lRVVJWTGpJeE1EWXdPUzVTTGxNaWZRLlJLS2VBZE02dGFjdWZpSVU3eTV2S3dsNFpQLURMNnEteHlrTndEdkljZFpIaTBIa2RIZ1V2WnoyZzZCTmpLS21WTU92dXp6TjhEczhybXo1dnMwT1RJN2tYUG1YeDZFLUYyUXVoUXNxT3J5LS1aN2J3TW5LYTNkZk1sbkthWU9PdURtV252RWMyR0hWdVVTSzREbmw0TE9vTTQxOVlMNThWTDAtSEthU18xYmNOUDhXYjVZR08xZXh1RmpiVGtIZkNIU0duVThJeUFjczlGTjhUT3JETHZpVEtwcWtvM3RiSUwxZE1TN3NhLWJkZExUVWp6TnVLTmFpNnpIWTdSanZGbjhjUDN6R2xjQnN1aVQ0XzVVaDZ0M05rZW1UdV9tZjdtZUFLLTBTMTAzMFpSNnNTR281azgtTE1sX0ZaUmh4djNFZFNtR2RBUTNlMDVMRzNnVVAyNzhTQWVzWHhNQUlHWmcxUFE3aEpoZGZHdmVGanJNdkdTSVFEM09wRnEtZHREcEFXbUo2Zm5sZFA1UWxYek5tQkJTMlZRQUtXZU9BYjh0Yjl5aVhsemhtT1dLRjF4SzlseHpYUG9GNmllOFRUWlJ4T0hxTjNiSkVISkVoQmVLclh6YkViV2tFNm4zTEoxbkd5M1htUlVFcER0Umdpa0tBUzZybFhFT0VneXNjIn0.eyJzaGEyNTYiOiJqSW12eGpsc2pqZ29JeUJuYThuZTk2d0RYYlVsU3N6eGFoM0NibkF6STFJPSJ9.PzpvU13h6VhN8VHXUTYKAlpDW5t3JaQ-gs895_Q10XshKPYpeZUtViXGHGC-aQSQAYPhhYV-lLia9niXzZz4Qs4ehwFLHJfkmKR8eRwWvoOgJtAY0IIUA_8SeShmoOc9cdpC35N3OeaM4hV9shxvvrphDib5sLpkrv3LQrt3DHvK_L2n0HsybC-pwS7MzaSUIYoU-fXwZo6x3z7IbSaSNwS0P-50qeV99Mc0AUSIvB26GjmjZ2gEH5R3YD9kp0DOrYvE5tIymVHPTqkmunv2OrjKu2UOhNj8Om3RoVzxIkVM89cVGb1u1yB2kxEmXogXPz64cKqQWm22tV-jalS4dAc_1p9A9sKzZ632HxnlavOBjTKDGFgM95gg8M5npXBP3QIvkwW3yervCukViRUKIm-ljpDmnBJsZTMx0uzTaAk5XgoCUCADuLLol8EXB-0V4m2w-6tV6kAzRiwkqw1PRrGqplf-gmfU7TuFlQ142-EZLU5rK_dAiQRXx-f7LxNH", )" - R"( "fileUrls": { )" - R"( "f483750ebb885d32c": "http://duinstance2.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/e5cc19d5e9174c93ada35cc315f1fb1d/apt-manifest-tree-1.0.json", )" - R"( "f222b9ffefaaac577": "http://duinstance2.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/31c38c3340a84e38ae8d30ce340f4a49/contoso.contoso-virtual-motors.1.1.updatemanifest.json", )" - R"( "f2c5d1f3b0295db0f": "http://duinstance2.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/9ff068f7c2bf43eb9561da14a7cbcecd/motor-firmware-1.1.json", )" - R"( "f13b5435aab7c18da": "http://duinstance2.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/c02058a476a242d7bc0e3c576c180051/contoso-motor-installscript.sh" )" - R"( } )" - R"( } )"; - -EXTERN_C_BEGIN -// fwd-decl to completion signal handler fn. -void ADUC_Workflow_WorkCompletionCallback(const void* workCompletionToken, ADUC_Result result, bool isAsync); -EXTERN_C_END - -// Note: g_iotHubClientHandleForADUComponent declared in adu_core_intefaace.h - -// -// Test Helpers -// - -static std::condition_variable workCompletionCallbackCV; - -static bool s_workflowComplete = false; -static std::string s_expectedWorkflowIdWhenIdle; - -#define ADUC_ClientHandle_Invalid (-1) - -static unsigned int s_mockWorkCompletionCallbackCallCount = 0; - -static IdleCallbackFunc s_platform_idle_callback = nullptr; - -static void Mock_Idle_Callback(ADUC_Token token, const char* workflowId) -{ - CHECK(token != nullptr); - CHECK(workflowId != nullptr); - CHECK(strcmp(workflowId, "action_bundle") == 0); - - // call the original update callback - REQUIRE(s_platform_idle_callback != nullptr); - (*s_platform_idle_callback)(token, workflowId); - - // notify now so that test can cleanup - workCompletionCallbackCV.notify_all(); -} - -extern "C" -{ - void Mock_DownloadProgressCallback( - const char* /*workflowId*/, - const char* /*fileId*/, - ADUC_DownloadProgressState /*state*/, - uint64_t /*bytesTransferred*/, - uint64_t /*bytesTotal*/) - { - } - - static void Mock_WorkCompletionCallback(const void* workCompletionToken, ADUC_Result result, bool isAsync) - { - CHECK(workCompletionToken != nullptr); - CHECK(IsAducResultCodeSuccess(result.ResultCode)); - CHECK(result.ExtendedResultCode == 0); - - auto methodCallData = (ADUC_MethodCall_Data*)workCompletionToken; // NOLINT - auto workflowData = methodCallData->WorkflowData; - - switch (s_mockWorkCompletionCallbackCallCount) - { - case 0: - // - // Process Deployment - // - REQUIRE_FALSE(isAsync); - REQUIRE(ADUC_WorkflowData_GetLastReportedState(workflowData) == ADUCITF_State_Idle); - REQUIRE(ADUC_WorkflowData_GetCurrentAction(workflowData) == ADUCITF_UpdateAction_ProcessDeployment); - REQUIRE(workflow_get_current_workflowstep(workflowData->WorkflowHandle) == ADUCITF_WorkflowStep_ProcessDeployment); - REQUIRE(workflow_get_operation_in_progress(workflowData->WorkflowHandle) == true); - REQUIRE(workflow_get_operation_cancel_requested(workflowData->WorkflowHandle) == false); - break; - - case 1: - // - // Download - // - REQUIRE(isAsync); - REQUIRE(ADUC_WorkflowData_GetLastReportedState(workflowData) == ADUCITF_State_DownloadStarted); - REQUIRE(ADUC_WorkflowData_GetCurrentAction(workflowData) == ADUCITF_UpdateAction_ProcessDeployment); - REQUIRE(workflow_get_current_workflowstep(workflowData->WorkflowHandle) == ADUCITF_WorkflowStep_Download); - - REQUIRE(workflowData->IsRegistered == false); - REQUIRE(workflow_get_operation_in_progress(workflowData->WorkflowHandle) == true); - REQUIRE(workflow_get_operation_cancel_requested(workflowData->WorkflowHandle) == false); - break; - - case 2: - // - // Install - // - REQUIRE(isAsync); - REQUIRE(ADUC_WorkflowData_GetLastReportedState(workflowData) == ADUCITF_State_InstallStarted); - REQUIRE(ADUC_WorkflowData_GetCurrentAction(workflowData) == ADUCITF_UpdateAction_ProcessDeployment); - REQUIRE(workflow_get_current_workflowstep(workflowData->WorkflowHandle) == ADUCITF_WorkflowStep_Install); - - REQUIRE(workflowData->IsRegistered == false); - REQUIRE(workflow_get_operation_in_progress(workflowData->WorkflowHandle) == true); - REQUIRE(workflow_get_operation_cancel_requested(workflowData->WorkflowHandle) == false); - break; - - case 3: - // - // Apply - // - REQUIRE(isAsync); - REQUIRE(ADUC_WorkflowData_GetLastReportedState(workflowData) == ADUCITF_State_ApplyStarted); - REQUIRE(ADUC_WorkflowData_GetCurrentAction(workflowData) == ADUCITF_UpdateAction_ProcessDeployment); - REQUIRE(workflow_get_current_workflowstep(workflowData->WorkflowHandle) == ADUCITF_WorkflowStep_Apply); - - REQUIRE(workflowData->IsRegistered == false); - REQUIRE(workflow_get_operation_in_progress(workflowData->WorkflowHandle) == true); - REQUIRE(workflow_get_operation_cancel_requested(workflowData->WorkflowHandle) == false); - - s_platform_idle_callback = workflowData->UpdateActionCallbacks.IdleCallback; - workflowData->UpdateActionCallbacks.IdleCallback = Mock_Idle_Callback; - - workCompletionCallbackCV.notify_all(); - break; - - default: - workCompletionCallbackCV.notify_all(); - } - - ++s_mockWorkCompletionCallbackCallCount; - - // Call the normal work completion callback to continue the workflow processing - ADUC_Workflow_WorkCompletionCallback(workCompletionToken, result, isAsync); - } -} - -static ADUC_ResultCode s_download_result_code = ADUC_Result_Download_Success; -static ADUC_ResultCode s_install_result_code = ADUC_Result_Install_Success; -static ADUC_ResultCode s_apply_result_code = ADUC_Result_Apply_Success; -static ADUC_ResultCode s_cancel_result_code = ADUC_Result_Cancel_Success; -static ADUC_ResultCode s_isInstalled_result_code = ADUC_Result_IsInstalled_NotInstalled; - -static void Reset_Mocks_State() -{ - s_mockWorkCompletionCallbackCallCount = 0; - s_workflowComplete = false; - s_expectedWorkflowIdWhenIdle = ""; - - s_download_result_code = ADUC_Result_Download_Success; - s_install_result_code = ADUC_Result_Install_Success; - s_apply_result_code = ADUC_Result_Apply_Success; - s_cancel_result_code = ADUC_Result_Cancel_Success; - s_isInstalled_result_code = ADUC_Result_IsInstalled_NotInstalled; -} - -// Mock the content handler so that these tests do not require the simulator platform or simulator content handler. -// A pointer to mock content handler instance will be set in the workflowData in the below tests. -class MockContentHandler : public ContentHandler -{ -public: - // Delete copy ctor, copy assignment, move ctor and move assignment operators. - MockContentHandler(const MockContentHandler&) = delete; - MockContentHandler& operator=(const MockContentHandler&) = delete; - MockContentHandler(MockContentHandler&&) = delete; - MockContentHandler& operator=(MockContentHandler&&) = delete; - - MockContentHandler() = default; - ~MockContentHandler() override = default; - - ADUC_Result Download(const ADUC_WorkflowData* workflowData) override - { - UNREFERENCED_PARAMETER(workflowData); - ADUC_Result result = { s_download_result_code }; - return result; - } - - ADUC_Result Install(const ADUC_WorkflowData* workflowData) override - { - UNREFERENCED_PARAMETER(workflowData); - ADUC_Result result = { s_install_result_code }; - return result; - } - - ADUC_Result Apply(const ADUC_WorkflowData* workflowData) override - { - UNREFERENCED_PARAMETER(workflowData); - ADUC_Result result = { s_apply_result_code }; - return result; - } - - ADUC_Result Cancel(const ADUC_WorkflowData* workflowData) override - { - UNREFERENCED_PARAMETER(workflowData); - ADUC_Result result = { s_cancel_result_code }; - return result; - } - - ADUC_Result IsInstalled(const ADUC_WorkflowData* workflowData) override - { - UNREFERENCED_PARAMETER(workflowData); - ADUC_Result result = { s_isInstalled_result_code }; - return result; - } -}; - -// NOLINTNEXTLINE(readability-non-const-parameter) -static ADUC_Result Mock_SandboxCreateCallback(ADUC_Token token, const char* workflowId, char* workFolder) -{ - UNREFERENCED_PARAMETER(token); - UNREFERENCED_PARAMETER(workflowId); - UNREFERENCED_PARAMETER(workFolder); - - ADUC_Result result = { ADUC_Result_Success }; - return result; -} - -static void Mock_SandboxDestroyCallback(ADUC_Token token, const char* workflowId, const char* workFolder) -{ - UNREFERENCED_PARAMETER(token); - UNREFERENCED_PARAMETER(workflowId); - UNREFERENCED_PARAMETER(workFolder); -} - -static void Mock_IdleCallback(ADUC_Token token, const char* workflowId) -{ - UNREFERENCED_PARAMETER(token); - CHECK(s_expectedWorkflowIdWhenIdle == workflowId); - s_workflowComplete = true; -} - -static void wait_for_workflow_complete() -{ - const unsigned MaxIterations = 100; - const unsigned SleepIntervalInMs = 10; - - unsigned count = 0; - while (count++ < MaxIterations && !s_workflowComplete) - { - std::this_thread::sleep_for(std::chrono::milliseconds(SleepIntervalInMs)); - } - CHECK(s_workflowComplete); -} - -class TestCaseFixture -{ -public: - TestCaseFixture() - { - m_previousDeviceHandle = g_iotHubClientHandleForADUComponent; - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - g_iotHubClientHandleForADUComponent = reinterpret_cast(ADUC_ClientHandle_Invalid); - } - - ~TestCaseFixture() - { - g_iotHubClientHandleForADUComponent = m_previousDeviceHandle; - } - - TestCaseFixture(const TestCaseFixture&) = delete; - TestCaseFixture& operator=(const TestCaseFixture&) = delete; - TestCaseFixture(TestCaseFixture&&) = delete; - TestCaseFixture& operator=(TestCaseFixture&&) = delete; - -private: - ADUC_ClientHandle m_previousDeviceHandle; -}; - -static IOTHUB_CLIENT_RESULT mockClientHandle_SendReportedState( - ADUC_CLIENT_HANDLE_TYPE deviceHandle, - const unsigned char* reportedState, - size_t reportedStateLen, - IOTHUB_CLIENT_REPORTED_STATE_CALLBACK_TYPE reportedStateCallback, - void* userContextCallback) -{ - UNREFERENCED_PARAMETER(deviceHandle); - UNREFERENCED_PARAMETER(reportedState); - UNREFERENCED_PARAMETER(reportedStateLen); - UNREFERENCED_PARAMETER(reportedStateCallback); - UNREFERENCED_PARAMETER(userContextCallback); - return IOTHUB_CLIENT_OK; -} - -// This test exercises the happy path for the entire agent-orchestrated workflow -// via HandlePropertyUpdate, but with mocked -// ADUC_Workflow_WorkCompletionCallback and mocked ContentHandler layer. -// It does not depend on simulator platform / simulator content handler and -// exercises the platform layer as well, including the async worker threads for -// the individual operations (i.e. download, install, apply). -TEST_CASE_METHOD(TestCaseFixture, "Process Workflow E2E Functional") -{ - Reset_Mocks_State(); - s_expectedWorkflowIdWhenIdle = "action_bundle"; - - std::mutex workCompletionCallbackMTX; - std::unique_lock lock(workCompletionCallbackMTX); - - MockContentHandler mockContentHandler; - - ADUC_WorkflowData workflowData{}; - - // set test overrides - ADUC_TestOverride_Hooks hooks{}; - hooks.WorkCompletionCallbackFunc_TestOverride = Mock_WorkCompletionCallback; - hooks.ContentHandler_TestOverride = &mockContentHandler; - hooks.ClientHandle_SendReportedStateFunc_TestOverride = (void*)mockClientHandle_SendReportedState; // NOLINT - workflowData.TestOverrides = &hooks; - - ADUC_Result result = - ADUC_MethodCall_Register(&(workflowData.UpdateActionCallbacks), 0 /*argc*/, nullptr /*argv*/); - - workflowData.UpdateActionCallbacks.SandboxCreateCallback = Mock_SandboxCreateCallback; - workflowData.UpdateActionCallbacks.SandboxDestroyCallback = Mock_SandboxDestroyCallback; - workflowData.UpdateActionCallbacks.IdleCallback = Mock_IdleCallback; - - workflowData.DownloadProgressCallback = Mock_DownloadProgressCallback; - workflowData.ReportStateAndResultAsyncCallback = AzureDeviceUpdateCoreInterface_ReportStateAndResultAsync; - workflowData.LastReportedState = ADUCITF_State_Idle; - - REQUIRE(IsAducResultCodeSuccess(result.ResultCode)); - REQUIRE(result.ExtendedResultCode == 0); - - // simulating non-startup processing of twin - workflowData.WorkflowHandle = nullptr; - workflowData.StartupIdleCallSent = true; - ADUC_Workflow_HandlePropertyUpdate(&workflowData, (const unsigned char*)workflow_test_process_deployment, false /* forceDeferral */); // NOLINT - - // Wait for entire workflow to complete - workCompletionCallbackCV.wait(lock); - - // Wait again for it to go to Idle so we don't trash the workflowData by going out of scope - workCompletionCallbackCV.wait(lock); - - wait_for_workflow_complete(); - - ADUC_MethodCall_Unregister(&workflowData.UpdateActionCallbacks); -} diff --git a/src/agent/command_helper/CMakeLists.txt b/src/agent/command_helper/CMakeLists.txt new file mode 100644 index 000000000..2f7443e39 --- /dev/null +++ b/src/agent/command_helper/CMakeLists.txt @@ -0,0 +1,24 @@ +project (command_helper) + +include (agentRules) + +compileasc99 () + +add_library (${PROJECT_NAME} STATIC ./src/command_helper.c ) +add_library (aduc::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +target_compile_definitions ( + ${PROJECT_NAME} + PRIVATE ADUC_COMMANDS_FIFO_NAME="${ADUC_COMMANDS_FIFO_NAME}" + ADUC_FILE_GROUP="${ADUC_FILE_GROUP}" + ADUC_FILE_USER="${ADUC_FILE_USER}" +) + +target_include_directories ( + ${PROJECT_NAME} + PUBLIC inc + PRIVATE ${ADUC_EXPORT_INCLUDES}) + +target_link_libraries (${PROJECT_NAME} + PRIVATE aduc::logging + aduc::permission_utils ) diff --git a/src/agent/command_helper/inc/aduc/command_helper.h b/src/agent/command_helper/inc/aduc/command_helper.h new file mode 100644 index 000000000..0cd9f2873 --- /dev/null +++ b/src/agent/command_helper/inc/aduc/command_helper.h @@ -0,0 +1,64 @@ +/** + * @file command_helper.h + * @brief A helper library for inter-agent commands support. + * + * @copyright Copyright (c) Microsoft Corp. + * Licensed under the MIT License. + */ + +#ifndef ADUC_COMMAND_HELPER_H +#define ADUC_COMMAND_HELPER_H + +/** + * @brief Callback method for a command. + * + * @param workCompletionData Method and value to call when work completes if *Result_InProgress returned. + * @param workflowData Data about what to download. + */ +typedef _Bool (*ADUC_CommandCallbackFunc)(const char* command, void* commandContext); + +/** + * @brief A struct containing a basic command information. + */ +typedef struct _tagADUC_Command +{ + const char* commandText; /**< command text */ + ADUC_CommandCallbackFunc callback; /**< callback function for the command */ +} ADUC_Command; + +/** + * @brief Send specified @p command to the main Device Update agent process. + * + * @param command A command to send. + * + * @return _Bool Returns true if success. + */ +_Bool SendCommand(const char* command); + +/** + * @brief Initialize command listener thread. + */ +_Bool InitializeCommandListenerThread(); + +/** + * @brief Uninitialize command listener thread. + */ +void UninitializeCommandListenerThread(); + +/** + * @brief Register command. + * + * @param command An ADUC_Command information. + * @return int If success, returns index of the registered command. Otherwise, returns -1. + */ +int RegisterCommand(ADUC_Command* command); + +/** + * @brief Unregister command. + * + * @param command Pointer to a command to unregister. + * @return _Bool If success, return true. Otherwise, returns false. + */ +_Bool UnregisterCommand(ADUC_Command* command); + +#endif /* ADUC_COMMAND_HELPER_H */ diff --git a/src/agent/command_helper/src/command_helper.c b/src/agent/command_helper/src/command_helper.c new file mode 100644 index 000000000..b13da8586 --- /dev/null +++ b/src/agent/command_helper/src/command_helper.c @@ -0,0 +1,385 @@ +/** + * @file command_helper.c + * @brief A helper library for inter-agent commands support. + * + * @copyright Copyright (c) Microsoft Corp. + * Licensed under the MIT License. + */ + +#include "aduc/command_helper.h" +#include "aduc/logging.h" +#include "aduc/permission_utils.h" + +#include +#include +#include // getgrnm +#include // pthread_* +#include // _Bool +#include // getline +#include // free +#include // strlen +#include // mkfifo +#include // sleep + +// For version 1.0, we're supporting only 1 command. +#define MAX_COMMAND_ARRAY_SIZE 1 +// Max command length including null. +#define COMMAND_MAX_LEN 64 +#define DELAY_BETWEEN_FAILED_OPERATION_SECONDS 10 + +static pthread_mutex_t g_commandQueueMutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_t g_commandListenerThread; +static _Bool g_commandListenerThreadCreated = false; +static _Bool g_terminate_thread_request = false; + +_Bool ADUC_OnReprocessUpdate(const char* command, void* context); + +static ADUC_Command* g_commands[MAX_COMMAND_ARRAY_SIZE] = {}; + +/** + * @brief Register command. + * + * @param command An ADUC_Command information. + * @return int If success, returns index of the registered command. Otherwise, returns -1. + */ +int RegisterCommand(ADUC_Command* command) +{ + pthread_mutex_lock(&g_commandQueueMutex); + int res = -1; + // Find an empty slot to register a new command. + for (int i = 0; i < MAX_COMMAND_ARRAY_SIZE; i++) + { + if (g_commands[i] == NULL) + { + Log_Info("Command register at slot#%d", i); + g_commands[i] = command; + res = i; + goto done; + } + } + + Log_Error("No space available for command."); +done: + pthread_mutex_unlock(&g_commandQueueMutex); + return res; +} + +/** + * @brief Unregister command. + * + * @param command Pointer to a command to unregister. + * @return _Bool If success, return true. Otherwise, returns false. + */ +_Bool UnregisterCommand(ADUC_Command* command) +{ + _Bool res = false; + pthread_mutex_lock(&g_commandQueueMutex); + for (int i = 0; i < MAX_COMMAND_ARRAY_SIZE; i++) + { + if (g_commands[i] == command) + { + Log_Info("Unregister command from stop#%d", i); + g_commands[i] = NULL; + res = true; + goto done; + } + } + Log_Warn("Command not found."); + +done: + pthread_mutex_unlock(&g_commandQueueMutex); + return res; +} + +/** + * @brief Create a FIFO named pipe file. + * + * @return _Bool Returns true if success. + */ +static _Bool TryCreateFIFOPipe() +{ + // Try to create file if doesn't exist. + struct stat st; + if (stat(ADUC_COMMANDS_FIFO_NAME, &st) == -1) + { + // Create FIFO pipe for commands. + // Only write to pipe + if (mkfifo(ADUC_COMMANDS_FIFO_NAME, S_IRGRP | S_IWGRP | S_IRUSR | S_IWUSR) != 0) + { + int error_no = errno; + switch (error_no) + { + case EACCES: + Log_Error("No permission"); + break; + case EDQUOT: + Log_Error("The user's quota of disk blocks or inodes on the filesystem has been exhausted."); + break; + + case EEXIST: + Log_Error("pathname already exists."); + break; + + case ENAMETOOLONG: + Log_Error("Path or file name is too long."); + break; + + case ENOENT: + Log_Error("A directory component in pathname does not exist. (%s)", ADUC_COMMANDS_FIFO_NAME); + break; + + case ENOSPC: + Log_Error("The directory or filesystem has no room for the new file."); + break; + + case ENOTDIR: + Log_Error("A component used as a directory in pathname is not, in fact, a directory."); + break; + + case EROFS: + Log_Error("Pathname refers to a read-only filesystem."); + break; + + default: + Log_Error("Cannot create named pipe. errno '%d'.", error_no); + break; + } + return false; + } + } + + Log_Info("Command FIFO file created successfully."); + return true; +} + +/** + * @brief Perform following security checks: + * - The FIFO pipe owners must be adu:adu. + * - The calling process' effective group must be 'root' or 'adu'. + * + * @return _Bool + */ +static _Bool SecurityChecks() +{ + if (!(PermissionUtils_CheckOwnership(ADUC_COMMANDS_FIFO_NAME, ADUC_FILE_USER, ADUC_FILE_GROUP))) + { + Log_Error("Security error: '%s' has invalid owners.", ADUC_COMMANDS_FIFO_NAME); + return false; + } + + // Verify current user + struct group* grp = getgrnam("adu"); + if (grp == NULL) + { + // Failed to get 'adu' group information, bail. + Log_Error("Cannot get 'adu' group info."); + return false; + } + + gid_t gid = getegid(); + if (gid != 0 /* root */ + && gid != grp->gr_gid /* adu */) + { + return false; + } + + return true; +} + +/** + * @brief + * + * @return void* + */ +static void* ADUC_CommandListenerThread() +{ + _Bool threadCreated = false; + int fileDescriptor = 0; + + if (!TryCreateFIFOPipe() || !SecurityChecks()) + { + goto done; + } + + threadCreated = true; + char commandLine[COMMAND_MAX_LEN]; + + do + { + // Open file for read, if needed. + if (fileDescriptor <= 0) + { + fileDescriptor = open(ADUC_COMMANDS_FIFO_NAME, O_RDONLY); + if (fileDescriptor <= 0) + { + Log_Error("Cannot open '%s' for read.", ADUC_COMMANDS_FIFO_NAME); + sleep(DELAY_BETWEEN_FAILED_OPERATION_SECONDS); + continue; + } + } + + Log_Info("Wait for command..."); + // By default, read() is blocked, until at least one writer open a file descriptor. + // For simplicity, we are leveraging this behavior instead of loop+sleep or 'select()' or 'poll()'. + ssize_t readSize = read(fileDescriptor, commandLine, sizeof(commandLine)); + if (readSize < 0) + { + // An error occurred. + Log_Warn("Read error (error:%d).", errno); + // Close current file descriptor and retry in next 'DELAY_BETWEEN_FAILED_OPERATION_SECONDS' seconds. + close(fileDescriptor); + fileDescriptor = -1; + sleep(DELAY_BETWEEN_FAILED_OPERATION_SECONDS); + continue; + } + + if (readSize == 0) + { + // EOF, in this case, no more data written to the pipe. + // Close and reopen the reader (above), to reset the block state. + // Note: regardless of fclose() result, fileDescriptor is no longer valid. + close(fileDescriptor); + fileDescriptor = -1; + continue; + } + + if (readSize < sizeof(commandLine)) + { + // Bad input size. Discard this... + Log_Warn( + "Received command with invalid size (%d bytes, expected %d). Ignored.", readSize, sizeof(commandLine)); + continue; + } + + // Process command. + pthread_mutex_lock(&g_commandQueueMutex); + const ADUC_Command* matchedCommand = NULL; + for (int i = 0; i < MAX_COMMAND_ARRAY_SIZE; i++) + { + if (g_commands[i] != NULL) + { + size_t commandTextLen = strlen(g_commands[i]->commandText); + if (readSize < commandTextLen) + { + continue; + } + + if (strcmp(commandLine, g_commands[i]->commandText) == 0) + { + matchedCommand = g_commands[i]; + break; + } + } + } + pthread_mutex_unlock(&g_commandQueueMutex); + + if (matchedCommand == NULL) + { + Log_Warn("Unsupported command received. '%s'", commandLine); + continue; + } + + // Command matched. + Log_Info("Executing command handler function for '%s'", commandLine); + if (!matchedCommand->callback(commandLine, NULL)) + { + Log_Error("Cannot execute a command handler for '%s'.", commandLine); + continue; + } + } while (!g_terminate_thread_request); + +done: + close(fileDescriptor); + if (!threadCreated) + { + Log_Error("Cannot start the command listener thread."); + } + return NULL; +} + +/** + * @brief Send specified @p command to the main Device Update agent process. + * + * @param command A command to send. + * + * @return _Bool Returns true if success. + */ +_Bool SendCommand(const char* command) +{ + static char buffer[COMMAND_MAX_LEN]; + _Bool success = false; + int fd = -1; + if (command == NULL || *command == '\0') + { + Log_Error("Command is null or empty."); + goto done; + } + + if (strlen(command) > COMMAND_MAX_LEN - 1) + { + Log_Error("Command is too long (63 characters max)."); + goto done; + } + + // Check if the writer can access the pipe. + if (!SecurityChecks()) + { + goto done; + } + + fd = open(ADUC_COMMANDS_FIFO_NAME, O_WRONLY); + if (fd < 0) + { + Log_Error("Fail to open pipe."); + goto done; + } + + // Copy command to buffer and fill the remaining buffer (if any) with additional null bytes. + strncpy(buffer, command, sizeof(buffer)); + ssize_t size = write(fd, buffer, sizeof(buffer)); + if (size != sizeof(buffer)) + { + Log_Error("Fail to send command."); + goto done; + } + + Log_Info("Command sent successfully."); + success = true; +done: + if (fd >= 0) + { + close(fd); + } + return success; +} + +/** + * @brief Initialize command listener thread. + */ +_Bool InitializeCommandListenerThread() +{ + if (g_commandListenerThreadCreated) + { + Log_Warn("Command listener thread already created."); + return false; + } + + Log_Info("Initializing command listener thread"); + + if (pthread_create(&g_commandListenerThread, NULL, ADUC_CommandListenerThread, NULL) == 0) + { + g_commandListenerThreadCreated = true; + return true; + } + + return false; +} + +/** + * @brief Uninitialize command listener thread. + */ +void UninitializeCommandListenerThread() +{ + Log_Info("De-initializing command listener thread"); + g_terminate_thread_request = true; +} diff --git a/src/agent/device_info_interface/CMakeLists.txt b/src/agent/device_info_interface/CMakeLists.txt index 05e70250b..f90e0ba63 100644 --- a/src/agent/device_info_interface/CMakeLists.txt +++ b/src/agent/device_info_interface/CMakeLists.txt @@ -16,6 +16,8 @@ target_link_digital_twin_client (${PROJECT_NAME} PUBLIC) target_link_libraries ( ${PROJECT_NAME} - PUBLIC aduc::c_utils - aduc::communication_abstraction - PRIVATE aduc::logging aduc::pnp_helper IotHubClient::iothub_client) + PUBLIC aduc::c_utils aduc::communication_abstraction + PRIVATE aduc::d2c_messaging + aduc::logging + aduc::pnp_helper + IotHubClient::iothub_client) diff --git a/src/agent/device_info_interface/inc/aduc/device_info_interface.h b/src/agent/device_info_interface/inc/aduc/device_info_interface.h index 3184fe93f..9c452e8e2 100644 --- a/src/agent/device_info_interface/inc/aduc/device_info_interface.h +++ b/src/agent/device_info_interface/inc/aduc/device_info_interface.h @@ -56,10 +56,8 @@ void DeviceInfoInterface_Destroy(void** componentContext); /** * @brief Report any changed DeviceInfo properties up to server. - * - * @return DIGITALTWIN_CLIENT_RESULT Result code. */ -IOTHUB_CLIENT_RESULT DeviceInfoInterface_ReportChangedPropertiesAsync(); +void DeviceInfoInterface_ReportChangedPropertiesAsync(); EXTERN_C_END diff --git a/src/agent/device_info_interface/src/device_info_interface.c b/src/agent/device_info_interface/src/device_info_interface.c index 528a95f3e..49ae7287c 100644 --- a/src/agent/device_info_interface/src/device_info_interface.c +++ b/src/agent/device_info_interface/src/device_info_interface.c @@ -8,6 +8,7 @@ #include "aduc/device_info_interface.h" #include "aduc/c_utils.h" #include "aduc/client_handle_helper.h" +#include "aduc/d2c_messaging.h" #include "aduc/device_info_exports.h" #include "aduc/logging.h" #include "aduc/string_c_utils.h" // atoint64t @@ -167,12 +168,7 @@ void DeviceInfoInterface_Connected(void* componentContext) // // After DeviceInfoInterface is registered, report current DeviceInfo properties, e.g. software version. // - - IOTHUB_CLIENT_RESULT reportResult = DeviceInfoInterface_ReportChangedPropertiesAsync(); - if (reportResult != IOTHUB_CLIENT_OK) - { - Log_Warn("DeviceInfoInterface_ReportChangedPropertiesAsync() failed, %u", reportResult); - } + DeviceInfoInterface_ReportChangedPropertiesAsync(); } void DeviceInfoInterface_Destroy(void** componentContext) @@ -183,59 +179,20 @@ void DeviceInfoInterface_Destroy(void** componentContext) DeviceInfoInterfaceData_Free(); } -IOTHUB_CLIENT_RESULT ReportChangedProperty(DeviceInfoInterface_Data* data) +/** + * @brief This function is called when the message is no longer being process. + * + * @param context The ADUC_D2C_Message object + * @param status The message status. + */ +static void OnDeviceInfoD2CMessageCompleted(void* context, ADUC_D2C_Message_Status status) { - ADUC_ClientHandle deviceClientLL = g_iotHubClientHandleForDeviceInfoComponent; - IOTHUB_CLIENT_RESULT iothubClientResult = IOTHUB_CLIENT_OK; - STRING_HANDLE jsonToSend = NULL; - - if (!data->IsDirty) - { - goto done; - } - - const char* propertyName = data->PropertyName; - const char* propertyValue = data->Value; - - Log_Info("Reporting changed property: %s, value: %s", propertyName, propertyValue); - - jsonToSend = PnP_CreateReportedProperty(g_deviceInfoPnPComponentName, propertyName, propertyValue); - - if (jsonToSend == NULL) - { - Log_Error( - "Unable to build reported property response for propertyName=%s, propertyValue=%s", - propertyName, - propertyValue); - iothubClientResult = IOTHUB_CLIENT_ERROR; - goto done; - } - - const char* jsonToSendStr = STRING_c_str(jsonToSend); - size_t jsonToSendStrLen = strlen(jsonToSendStr); - - iothubClientResult = ClientHandle_SendReportedState( - deviceClientLL, (const unsigned char*)jsonToSendStr, jsonToSendStrLen, NULL, NULL); - - if (iothubClientResult != IOTHUB_CLIENT_OK) - { - Log_Error( - "DeviceInfoInterface: Reporting property %s failed, error: %d, %s", - propertyName, - iothubClientResult, - MU_ENUM_TO_STRING(IOTHUB_CLIENT_RESULT, iothubClientResult)); - goto done; - } - -done: - STRING_delete(jsonToSend); - - return iothubClientResult; + UNREFERENCED_PARAMETER(context); + Log_Debug("Send message completed (status:%d)", status); } -IOTHUB_CLIENT_RESULT DeviceInfoInterface_ReportChangedPropertiesAsync() +void DeviceInfoInterface_ReportChangedPropertiesAsync() { - IOTHUB_CLIENT_RESULT iothubClientResult = IOTHUB_CLIENT_OK; RefreshDeviceInfoInterfaceData(); STRING_HANDLE jsonToSend = NULL; @@ -264,7 +221,6 @@ IOTHUB_CLIENT_RESULT DeviceInfoInterface_ReportChangedPropertiesAsync() if (!atoul(propertyValue, &val)) { Log_Error("Cannot convert property value to number. Value: %s", propertyValue); - iothubClientResult = IOTHUB_CLIENT_ERROR; goto done; } json_object_set_number(root_object, propertyName, val); @@ -278,28 +234,24 @@ IOTHUB_CLIENT_RESULT DeviceInfoInterface_ReportChangedPropertiesAsync() if (jsonToSend == NULL) { Log_Error("Unable to build reported property for DeviceInformation component."); - iothubClientResult = IOTHUB_CLIENT_ERROR; goto done; } - const char* jsonToSendStr = STRING_c_str(jsonToSend); - size_t jsonToSendStrLen = strlen(jsonToSendStr); - - iothubClientResult = ClientHandle_SendReportedState( - g_iotHubClientHandleForDeviceInfoComponent, (const unsigned char*)jsonToSendStr, jsonToSendStrLen, NULL, NULL); - - if (iothubClientResult != IOTHUB_CLIENT_OK) + if (!ADUC_D2C_Message_SendAsync( + ADUC_D2C_Message_Type_Device_Information, + &g_iotHubClientHandleForDeviceInfoComponent, + STRING_c_str(jsonToSend), + NULL /* responseCallback */, + OnDeviceInfoD2CMessageCompleted, + NULL /* statusChangedCallback */, + NULL /* userData */)) { - Log_Error( - "DeviceInfoInterface: Report property failed, error: %d, %s", - iothubClientResult, - MU_ENUM_TO_STRING(IOTHUB_CLIENT_RESULT, iothubClientResult)); + Log_Error("Unable to send device information."); + goto done; } done: json_value_free(root_value); json_free_serialized_string(serialized_string); STRING_delete(jsonToSend); - - return iothubClientResult; } diff --git a/src/agent/pnp_helper/src/pnp_protocol.c b/src/agent/pnp_helper/src/pnp_protocol.c index 20408432f..2112ef6fa 100644 --- a/src/agent/pnp_helper/src/pnp_protocol.c +++ b/src/agent/pnp_helper/src/pnp_protocol.c @@ -196,9 +196,9 @@ static void VisitComponentProperties( { // This should never happen because we are simply accessing parson tree. Do not pass NULL to application in case it does occur. LogError( - "Unexpected error retrieving the property name and/or value of component=%s at element at index=%lu", + "Unexpected error retrieving the property name and/or value of component=%s at element at index=%zu", objectName, - (unsigned long)i); + i); continue; } diff --git a/src/agent/src/health_management.c b/src/agent/src/health_management.c index 1e5baf410..b4927c8a9 100644 --- a/src/agent/src/health_management.c +++ b/src/agent/src/health_management.c @@ -13,6 +13,7 @@ #include "aduc/system_utils.h" // for SystemUtils_IsDir, SystemUtils_IsFile #include // for STRING_HANDLE, STRING_delete, STRING_c_str #include +#include #include #include #include @@ -20,17 +21,32 @@ /** * @brief The users that must exist on the system. */ -static const char* aduc_required_users[] = { ADUC_FILE_USER, DO_FILE_USER }; +static const char* aduc_required_users[] = { ADUC_FILE_USER }; + +/** + * @brief The optional users. + */ +static const char* aduc_optional_users[] = { DO_FILE_USER }; /** * @brief The groups that must exist on the system. */ -static const char* aduc_required_groups[] = { ADUC_FILE_GROUP, DO_FILE_GROUP }; +static const char* aduc_required_groups[] = { ADUC_FILE_GROUP }; + +/** + * @brief The optional groups. + */ +static const char* aduc_optional_groups[] = { DO_FILE_GROUP }; + +/** + * @brief The supplementary groups for ADUC_FILE_USER + */ +static const char* aduc_required_group_memberships[] = {}; /** * @brief The supplementary groups for ADUC_FILE_USER */ -static const char* aduc_required_group_memberships[] = { +static const char* aduc_optional_group_memberships[] = { DO_FILE_GROUP // allows agent to set connection_string for DO }; @@ -112,12 +128,21 @@ static _Bool ReportMissingRequiredUsers() { if (!PermissionUtils_UserExists(aduc_required_users[i])) { - Log_Error("User '%s' does not exist.", aduc_required_users[i]); + Log_Error("Required user '%s' does not exist.", aduc_required_users[i]); result = false; // continue on to next user } } + for (int i = 0; i < ARRAY_SIZE(aduc_optional_users); ++i) + { + if (!PermissionUtils_UserExists(aduc_optional_users[i])) + { + Log_Warn("Optional user '%s' does not exist.", aduc_optional_users[i]); + // continue on to next user + } + } + return result; } @@ -134,12 +159,21 @@ static _Bool ReportMissingRequiredGroups() { if (!PermissionUtils_GroupExists(aduc_required_groups[i])) { - Log_Error("Group '%s' does not exist.", aduc_required_groups[i]); + Log_Error("Required group '%s' does not exist.", aduc_required_groups[i]); result = false; // continue on to next user } } + for (int i = 0; i < ARRAY_SIZE(aduc_optional_groups); ++i) + { + if (!PermissionUtils_GroupExists(aduc_optional_groups[i])) + { + Log_Warn("Optional group '%s' does not exist.", aduc_optional_groups[i]); + // continue on to next user + } + } + return result; } @@ -163,11 +197,20 @@ static _Bool ReportMissingGroupMemberships() } } + // ADUC group memberships + for (int i = 0; i < ARRAY_SIZE(aduc_optional_group_memberships); ++i) + { + if (!PermissionUtils_UserInSupplementaryGroup(ADUC_FILE_USER, aduc_optional_group_memberships[i])) + { + Log_Warn("User '%s' is not a member of '%s' group.", ADUC_FILE_USER, aduc_optional_group_memberships[i]); + // continue evaluating next membership + } + } + // DO group memberships if (!PermissionUtils_UserInSupplementaryGroup(DO_FILE_USER, ADUC_FILE_GROUP)) { - Log_Error("User '%s' is not a member of '%s' group.", DO_FILE_USER, ADUC_FILE_GROUP); - result = false; + Log_Warn("User '%s' is not a member of '%s' group.", DO_FILE_USER, ADUC_FILE_GROUP); } return result; @@ -212,7 +255,7 @@ static _Bool CheckConfDirOwnershipAndPermissions() const char* path = ADUC_CONF_FOLDER; - if (SystemUtils_IsDir(path)) + if (SystemUtils_IsDir(path, NULL)) { if (!PermissionUtils_CheckOwnership(path, ADUC_FILE_USER, ADUC_FILE_GROUP)) { @@ -222,8 +265,8 @@ static _Bool CheckConfDirOwnershipAndPermissions() } // owning user can read, write, and list entries in dir. - // group members and everyone else can read and list entries in dir. - const mode_t expected_permissions = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; + // group members can read and list entries in dir. + const mode_t expected_permissions = S_IRWXU | S_IRGRP | S_IXGRP; if (!PermissionUtils_VerifyFilemodeExact(path, expected_permissions)) { @@ -253,7 +296,7 @@ static _Bool CheckConfFile() const char* path = ADUC_CONF_FILE_PATH; - if (SystemUtils_IsFile(path)) + if (SystemUtils_IsFile(path, NULL)) { if (!PermissionUtils_CheckOwnership(path, ADUC_FILE_USER, ADUC_FILE_GROUP)) { @@ -261,7 +304,7 @@ static _Bool CheckConfFile() result = false; } - const mode_t bitmask = S_IRUSR | S_IRGRP | S_IROTH; + const mode_t bitmask = S_IRUSR | S_IRGRP; if (!PermissionUtils_VerifyFilemodeBitmask(path, bitmask)) { @@ -288,9 +331,15 @@ static _Bool CheckLogDir() const char* dir = ADUC_LOG_FOLDER; - if (!SystemUtils_IsDir(dir)) + int err; + if (!SystemUtils_IsDir(dir, &err)) { - Log_Error("'%s' does not exist or is not a directory", dir); + if (err != 0) + { + Log_Error("Cannot get '%s' status. (errno: %d)", dir, errno); + goto done; + } + Log_Error("'%s' is not a directory", dir); goto done; } @@ -323,13 +372,20 @@ static _Bool CheckLogDir() * @param expectedPermissions The expected permissions of the file object. * @returns true if everything is correct. */ -static _Bool CheckDirOwnershipAndVerifyFilemodeExact(const char* path, const char* user, const char* group, const mode_t expected_permissions) +static _Bool CheckDirOwnershipAndVerifyFilemodeExact( + const char* path, const char* user, const char* group, const mode_t expected_permissions) { _Bool result = false; - if (!SystemUtils_IsDir(path)) + int err; + if (!SystemUtils_IsDir(path, &err)) { - Log_Error("'%s' does not exist or is not a directory", path); + if (err != 0) + { + Log_Error("Cannot get '%s' status. (errno: %d)", path, errno); + goto done; + } + Log_Error("'%s' is not a directory", path); goto done; } @@ -360,7 +416,8 @@ static _Bool CheckDataDir() { // Note: "Other" bits are cleared to align with ADUC_SystemUtils_MkDirRecursiveDefault and packaging. const mode_t expected_permissions = S_IRWXU | S_IRWXG; - return CheckDirOwnershipAndVerifyFilemodeExact(ADUC_DATA_FOLDER, ADUC_FILE_USER, ADUC_FILE_GROUP, expected_permissions); + return CheckDirOwnershipAndVerifyFilemodeExact( + ADUC_DATA_FOLDER, ADUC_FILE_USER, ADUC_FILE_GROUP, expected_permissions); } /** @@ -371,7 +428,8 @@ static _Bool CheckDownloadsDir() { // Note: "Other" bits are cleared to align with ADUC_SystemUtils_MkDirRecursiveDefault and packaging. const mode_t expected_permissions = S_IRWXU | S_IRWXG; - return CheckDirOwnershipAndVerifyFilemodeExact(ADUC_DOWNLOADS_FOLDER, ADUC_FILE_USER, ADUC_FILE_GROUP, expected_permissions); + return CheckDirOwnershipAndVerifyFilemodeExact( + ADUC_DOWNLOADS_FOLDER, ADUC_FILE_USER, ADUC_FILE_GROUP, expected_permissions); } /** @@ -384,7 +442,7 @@ static _Bool CheckAgentBinary() const char* path = ADUC_AGENT_FILEPATH; - if (SystemUtils_IsFile(path)) + if (SystemUtils_IsFile(path, NULL)) { if (!PermissionUtils_CheckOwnerUid(path, 0 /* root */)) { @@ -423,7 +481,7 @@ static _Bool CheckShellBinary() const char* path = ADUSHELL_FILE_PATH; - if (SystemUtils_IsFile(path)) + if (SystemUtils_IsFile(path, NULL)) { if (!PermissionUtils_CheckOwnerUid(path, 0 /* root */)) { @@ -527,7 +585,8 @@ _Bool HealthCheck(const ADUC_LaunchArguments* launchArgs) ADUC_ConfigInfo config = {}; if (!ADUC_ConfigInfo_Init(&config, ADUC_CONF_FILE_PATH)) { - Log_Warn("Cannot read configuration file: %s", ADUC_CONF_FILE_PATH); + Log_Error("Failed to initialize from config file: %s", ADUC_CONF_FILE_PATH); + goto done; } if (!IsConnectionInfoValid(launchArgs, &config)) diff --git a/src/agent/src/main.c b/src/agent/src/main.c index a46fa05d3..9d434c3d6 100644 --- a/src/agent/src/main.c +++ b/src/agent/src/main.c @@ -11,13 +11,17 @@ #include "aduc/agent_workflow.h" #include "aduc/c_utils.h" #include "aduc/client_handle_helper.h" +#include "aduc/command_helper.h" #include "aduc/config_utils.h" #include "aduc/connection_string_utils.h" +#include "aduc/d2c_messaging.h" #include "aduc/device_info_interface.h" #include "aduc/extension_manager.h" #include "aduc/extension_utils.h" #include "aduc/health_management.h" +#include "aduc/https_proxy_utils.h" #include "aduc/logging.h" +#include "aduc/permission_utils.h" #include "aduc/string_c_utils.h" #include "aduc/system_utils.h" #include @@ -32,7 +36,16 @@ #include #include #include -#include +#include + +#ifdef ADUC_ALLOW_MQTT +# include +#endif + +#ifdef ADUC_ALLOW_MQTT_OVER_WEBSOCKETS +# include +#endif + #include #include #include @@ -50,11 +63,10 @@ */ #define EIS_PROVISIONING_TIMEOUT 2000 -#define SECONDS_IN_MONTH (30 /* day/mo */ * 24 /* hr/day */ * 60 /* min/hr */ * 60 /*sec/min */) /** * @brief Time after startup the connection string will be provisioned for by the Edge Identity Service */ -#define EIS_TOKEN_EXPIRY_TIME (3 * SECONDS_IN_MONTH) +#define EIS_TOKEN_EXPIRY_TIME_IN_SECONDS (12 /* hr */ * 60 /* min/hr */ * 60 /*sec/min */) /** * @brief Make getopt* stop parsing as soon as non-option argument is encountered. @@ -77,7 +89,7 @@ * Customers should change this ID to match their device model ID. */ -static const char g_aduModelId[] = "dtmi:azure:iot:deviceUpdateModel;1"; +static const char g_aduModelId[] = "dtmi:azure:iot:deviceUpdateModel;2"; // Name of ADU Agent subcomponent that this device implements. static const char g_aduPnPComponentName[] = "deviceUpdate"; @@ -88,14 +100,16 @@ static const char g_deviceInfoPnPComponentName[] = "deviceInformation"; // Name of the Diagnostics subcomponent that this device is using static const char g_diagnosticsPnPComponentName[] = "diagnosticInformation"; -// Engine type for an OpenSSL Engine -static const OPTION_OPENSSL_KEY_TYPE x509_key_from_engine = KEY_TYPE_ENGINE; +ADUC_LaunchArguments launchArgs; /** * @brief Global IoT Hub client handle. */ ADUC_ClientHandle g_iotHubClientHandle = NULL; +// Engine type for an OpenSSL Engine +static const OPTION_OPENSSL_KEY_TYPE x509_key_from_engine = KEY_TYPE_ENGINE; + /** * @brief Determines if we're shutting down. * @@ -145,8 +159,13 @@ typedef void (*PnPComponentPropertyUpdateCallback)( const char* propertyName, JSON_Value* propertyValue, int version, + ADUC_PnPComponentClient_PropertyUpdate_Context* sourceContext, void* userContextCallback); +static ADUC_PnPComponentClient_PropertyUpdate_Context g_iotHubInitiatedPnPPropertyChangeContext = { false, false }; + +static ADUC_PnPComponentClient_PropertyUpdate_Context g_deviceInitiatedRetryPnPPropertyChangeContext = { true, true }; + /** * @brief Defines an PnP Component Client that this agent supports. */ @@ -160,7 +179,6 @@ typedef struct tagPnPComponentEntry const PnPComponentDestroyFunc Destroy; const PnPComponentPropertyUpdateCallback PnPPropertyUpdateCallback; /**< Called when a component's property is updated. (optional) */ - // // Following data is dynamic. // Must be initialized to NULL in map and remain last entries in this struct. @@ -176,15 +194,8 @@ typedef struct tagPnPComponentEntry */ // NOLINTNEXTLINE(cppcoreguidelines-interfaces-global-init) static PnPComponentEntry componentList[] = { - { - g_deviceInfoPnPComponentName, - &g_iotHubClientHandleForDeviceInfoComponent, - DeviceInfoInterface_Create, - DeviceInfoInterface_Connected, - NULL /* DoWork method - not used */, - DeviceInfoInterface_Destroy, - NULL, /* PropertyUpdateCallback - not used */ - }, + // Important: the 'deviceUpdate' component must before first entry here. + // This entry will be referenced by ADUC_PnPDeviceTwin_RetryUpdateCommand_Callback function below. { g_aduPnPComponentName, &g_iotHubClientHandleForADUComponent, @@ -194,12 +205,21 @@ static PnPComponentEntry componentList[] = { AzureDeviceUpdateCoreInterface_Destroy, AzureDeviceUpdateCoreInterface_PropertyUpdateCallback }, + { + g_deviceInfoPnPComponentName, + &g_iotHubClientHandleForDeviceInfoComponent, + DeviceInfoInterface_Create, + DeviceInfoInterface_Connected, + NULL /* DoWork method - not used */, + DeviceInfoInterface_Destroy, + NULL /* PropertyUpdateCallback - not used */ + }, { g_diagnosticsPnPComponentName, &g_iotHubClientHandleForDiagnosticsComponent, DiagnosticsInterface_Create, DiagnosticsInterface_Connected, - NULL, + NULL /* DoWork method - not used */, DiagnosticsInterface_Destroy, DiagnosticsInterface_PropertyUpdateCallback }, @@ -207,6 +227,33 @@ static PnPComponentEntry componentList[] = { // clang-format on +static void ADUC_Refresh_IotHub_Connection_SAS_Token(); + +ADUC_ExtensionRegistrationType GetRegistrationTypeFromArg(const char* arg) +{ + if (strcmp(arg, "updateContentHandler") == 0) + { + return ExtensionRegistrationType_UpdateContentHandler; + } + + if (strcmp(arg, "contentDownloader") == 0) + { + return ExtensionRegistrationType_ContentDownloadHandler; + } + + if (strcmp(arg, "componentEnumerator") == 0) + { + return ExtensionRegistrationType_ComponentEnumerator; + } + + if (strcmp(arg, "downloadHandler") == 0) + { + return ExtensionRegistrationType_DownloadHandler; + } + + return ExtensionRegistrationType_None; +} + /** * @brief Parse command-line arguments. * @param argc arguments count. @@ -241,11 +288,11 @@ int ParseLaunchArguments(const int argc, char** argv, ADUC_LaunchArguments* laun { "health-check", no_argument, 0, 'h' }, { "log-level", required_argument, 0, 'l' }, { "connection-string", required_argument, 0, 'c' }, - { "register-content-handler", required_argument, 0, 'C' }, - { "register-component-enumerator", required_argument, 0, 'E' }, - { "register-content-downloader", required_argument, 0, 'D' }, - { "update-type", required_argument, 0, 'u' }, + { "register-extension", required_argument, 0, 'E' }, + { "extension-type", required_argument, 0, 't' }, + { "extension-id", required_argument, 0, 'i' }, { "run-as-owner", no_argument, 0, 'a' }, + { "command", required_argument, 0, 'C' }, { 0, 0, 0, 0 } }; // clang-format on @@ -256,7 +303,7 @@ int ParseLaunchArguments(const int argc, char** argv, ADUC_LaunchArguments* laun int option = getopt_long( argc, argv, - STOP_PARSE_ON_NONOPTION_ARG RET_COLON_FOR_MISSING_OPTIONARG "avehcu:l:r:d:n:C:E:D:", + STOP_PARSE_ON_NONOPTION_ARG RET_COLON_FOR_MISSING_OPTIONARG "avehcu:l:d:n:E:t:i:C:", long_options, &option_index); @@ -302,19 +349,19 @@ int ParseLaunchArguments(const int argc, char** argv, ADUC_LaunchArguments* laun break; case 'C': - launchArgs->contentHandlerFilePath = optarg; + launchArgs->ipcCommand = optarg; break; - case 'D': - launchArgs->contentDownloaderFilePath = optarg; + case 'E': + launchArgs->extensionFilePath = optarg; break; - case 'E': - launchArgs->componentEnumeratorFilePath = optarg; + case 't': + launchArgs->extensionRegistrationType = GetRegistrationTypeFromArg(optarg); break; - case 'u': - launchArgs->updateType = optarg; + case 'i': + launchArgs->extensionId = optarg; break; case ':': @@ -423,31 +470,37 @@ _Bool ADUC_SetDiagnosticsDeviceNameFromConnectionString(const char* connectionSt // /** - * @brief Destroy IoTHub device client handle. - * - * @param deviceHandle IoTHub device client handle. + * @brief Uninitialize all PnP components' handler. */ -static void ADUC_DeviceClient_Destroy(ADUC_ClientHandle clientHandle) +static void ADUC_PnP_Components_Destroy() { - if (clientHandle != NULL) + for (unsigned index = 0; index < ARRAY_SIZE(componentList); ++index) { - ClientHandle_Destroy(clientHandle); + PnPComponentEntry* entry = componentList + index; + + if (entry->Destroy != NULL) + { + entry->Destroy(&(entry->Context)); + } } } /** - * @brief Uninitialize all PnP components' handler. + * @brief Refreshes the client handle associated with each of the components in the componentList + * + * @param clientHandle new handle to be set on each of the components */ -void ADUC_PnP_Components_Destroy() +static void ADUC_PnP_Components_HandleRefresh(ADUC_ClientHandle clientHandle) { - for (unsigned index = 0; index < ARRAY_SIZE(componentList); ++index) + Log_Info("Refreshing the handle for the PnP channels."); + + const size_t componentCount = ARRAY_SIZE(componentList); + + for (size_t index = 0; index < componentCount; ++index) { PnPComponentEntry* entry = componentList + index; - if (entry->Destroy != NULL) - { - entry->Destroy(&(entry->Context)); - } + *(entry->clientHandle) = clientHandle; } } @@ -459,7 +512,7 @@ void ADUC_PnP_Components_Destroy() * @param argv Size of argc. * @return _Bool True on success. */ -_Bool ADUC_PnP_Components_Create(ADUC_ClientHandle clientHandle, int argc, char** argv) +static _Bool ADUC_PnP_Components_Create(ADUC_ClientHandle clientHandle, int argc, char** argv) { Log_Info("Initializing PnP components."); _Bool succeeded = false; @@ -468,6 +521,7 @@ _Bool ADUC_PnP_Components_Create(ADUC_ClientHandle clientHandle, int argc, char* for (unsigned index = 0; index < componentCount; ++index) { PnPComponentEntry* entry = componentList + index; + if (!entry->Create(&entry->Context, argc, argv)) { Log_Error("Failed to initialize PnP component '%s'.", entry->ComponentName); @@ -497,7 +551,8 @@ static void ADUC_PnP_ComponentClient_PropertyUpdate_Callback( int version, void* userContextCallback) { - ADUC_ClientHandle clientHandle = (ADUC_ClientHandle)userContextCallback; + ADUC_PnPComponentClient_PropertyUpdate_Context* sourceContext = + (ADUC_PnPComponentClient_PropertyUpdate_Context*)userContextCallback; Log_Debug("ComponentName:%s, propertyName:%s", componentName, propertyName); @@ -517,7 +572,8 @@ static void ADUC_PnP_ComponentClient_PropertyUpdate_Callback( supported = true; if (entry->PnPPropertyUpdateCallback != NULL) { - entry->PnPPropertyUpdateCallback(clientHandle, propertyName, propertyValue, version, entry->Context); + entry->PnPPropertyUpdateCallback( + *(entry->clientHandle), propertyName, propertyValue, version, sourceContext, entry->Context); } else { @@ -540,7 +596,7 @@ static void ADUC_PnP_ComponentClient_PropertyUpdate_Callback( // Note: This is an array of weak references to componentList[i].componentName // as such the size of componentList must be equal to the size of g_modeledComponents -static const char* g_modeledComponents[3]; +static const char* g_modeledComponents[ARRAY_SIZE(componentList)]; static const size_t g_numModeledComponents = ARRAY_SIZE(g_modeledComponents); @@ -557,6 +613,31 @@ static void InitializeModeledComponents() g_modeledComponents[i] = componentList[i].ComponentName; } } + +// +// ADUC_PnP_DeviceTwin_Callback is invoked by IoT SDK when a twin - either full twin or a PATCH update - arrives. +// +static void ADUC_PnPDeviceTwin_RetryUpdateCommand_Callback( + DEVICE_TWIN_UPDATE_STATE updateState, const unsigned char* payload, size_t size, void* userContextCallback) +{ + // Invoke PnP_ProcessTwinData to actually process the data. PnP_ProcessTwinData uses a visitor pattern to parse + // the JSON and then visit each property, invoking PnP_TempControlComponent_ApplicationPropertyCallback on each element. + if (PnP_ProcessTwinData( + updateState, + payload, + size, + g_modeledComponents, + 1, // Only process the first entry, which is 'deviceUpdate' PnP component. + ADUC_PnP_ComponentClient_PropertyUpdate_Callback, + userContextCallback) + == false) + { + // If we're unable to parse the JSON for any reason (typically because the JSON is malformed or we ran out of memory) + // there is no action we can take beyond logging. + Log_Error("Unable to process twin JSON. Ignoring any desired property update requests."); + } +} + // // ADUC_PnP_DeviceTwin_Callback is invoked by IoT SDK when a twin - either full twin or a PATCH update - arrives. // @@ -604,26 +685,107 @@ static void ADUC_ConnectionStatus_Callback( { UNREFERENCED_PARAMETER(userContextCallback); - Log_Debug("IotHub connection status: %d, reason:%d", result, reason); + Log_Debug("IotHub connection status: %d, reason: %d", result, reason); + + if (result == IOTHUB_CLIENT_CONNECTION_UNAUTHENTICATED) + { + switch (reason) + { + case IOTHUB_CLIENT_CONNECTION_EXPIRED_SAS_TOKEN: + Log_Warn("IotHub connection SAS token expired. Attempting refresh."); + // Try to refresh the sas token, then set an IotHub client handle on every PnP sub-component. + ADUC_Refresh_IotHub_Connection_SAS_Token(); + break; + default: + break; + } + } +} + +static IOTHUB_CLIENT_TRANSPORT_PROVIDER GetIotHubProtocolFromConfig() +{ +#ifdef ADUC_GET_IOTHUB_PROTOCOL_FROM_CONFIG + IOTHUB_CLIENT_TRANSPORT_PROVIDER transportProvider = NULL; + + ADUC_ConfigInfo config; + if (ADUC_ConfigInfo_Init(&config, ADUC_CONF_FILE_PATH)) + { + if (config.iotHubProtocol != NULL) + { + if (strcmp(config.iotHubProtocol, "mqtt") == 0) + { + transportProvider = MQTT_Protocol; + Log_Info("IotHub Protocol: MQTT"); + } + else if (strcmp(config.iotHubProtocol, "mqtt/ws") == 0) + { + transportProvider = MQTT_WebSocket_Protocol; + Log_Info("IotHub Protocol: MQTT/WS"); + } + else + { + Log_Error( + "Unsupported 'iotHubProtocol' value of '%s' from '" ADUC_CONF_FILE_PATH "'.", + config.iotHubProtocol); + } + } + else + { + Log_Warn("Missing 'iotHubProtocol' setting from '" ADUC_CONF_FILE_PATH "'. Default to MQTT."); + transportProvider = MQTT_Protocol; + Log_Info("IotHub Protocol: MQTT"); + } + + ADUC_ConfigInfo_UnInit(&config); + } + else + { + Log_Error("Failed to initialize config file '" ADUC_CONF_FILE_PATH "'."); + } + + return transportProvider; + +#else + +# ifdef ADUC_ALLOW_MQTT + Log_Info("IotHub Protocol: MQTT"); + return MQTT_Protocol; +# endif // ADUC_ALLOW_MQTT + +# ifdef ADUC_ALLOW_MQTT_OVER_WEBSOCKETS + Log_Info("IotHub Protocol: MQTT/WS"); + return MQTT_WebSocket_Protocol; +# endif // ADUC_ALLOW_MQTT_OVER_WEBSOCKETS + +#endif // ADUC_GET_IOTHUB_PROTOCOL_FROM_CONFIG } /** * @brief Creates an IoTHub device client handler and register all callbacks. - * + * @details should use ADUC_DeviceClient_Destroy() to uninit clientHandle + * @param clientHandle clientHandle to be initialized with the connection info and launchArgs * @param connInfo struct containing the connection information for the DeviceClient * @param launchArgs Launch command-line arguments. * @return true on success, false on failure */ -_Bool ADUC_DeviceClient_Create(ADUC_ConnectionInfo* connInfo, const ADUC_LaunchArguments* launchArgs) +static _Bool ADUC_DeviceClient_Create( + ADUC_ClientHandle clientHandle, ADUC_ConnectionInfo* connInfo, const ADUC_LaunchArguments* launchArgs) { IOTHUB_CLIENT_RESULT iothubResult; + HTTP_PROXY_OPTIONS proxyOptions = {}; bool result = true; + bool shouldSetProxyOptions = InitializeProxyOptions(&proxyOptions); Log_Info("Attempting to create connection to IotHub using type: %s ", ADUC_ConnType_ToString(connInfo->connType)); + IOTHUB_CLIENT_TRANSPORT_PROVIDER transportProvider = GetIotHubProtocolFromConfig(); + if (transportProvider == NULL) + { + result = false; + } // Create a connection to IoTHub. - if (!ClientHandle_CreateFromConnectionString( - &g_iotHubClientHandle, connInfo->connType, connInfo->connectionString, MQTT_Protocol)) + else if (!ClientHandle_CreateFromConnectionString( + &g_iotHubClientHandle, connInfo->connType, connInfo->connectionString, transportProvider)) { Log_Error("Failure creating IotHub device client using MQTT protocol. Check your connection string."); result = false; @@ -646,6 +808,14 @@ _Bool ADUC_DeviceClient_Create(ADUC_ConnectionInfo* connInfo, const ADUC_LaunchA Log_Error("Unable to set IotHub certificate, error=%d", iothubResult); result = false; } + else if ( + shouldSetProxyOptions + && (iothubResult = + ClientHandle_SetOption(g_iotHubClientHandle, OPTION_HTTP_PROXY, &proxyOptions) != IOTHUB_CLIENT_OK)) + { + Log_Error("Could not set http proxy options, error=%d ", iothubResult); + result = false; + } else if ( connInfo->certificateString != NULL && connInfo->authType == ADUC_AuthType_NestedEdgeCert && (iothubResult = @@ -683,11 +853,6 @@ _Bool ADUC_DeviceClient_Create(ADUC_ConnectionInfo* connInfo, const ADUC_LaunchA Log_Error("Unable to set IotHub OpenSSL Private Key Type, error=%d", iothubResult); result = false; } - // Create PnP components. - else if (!ADUC_PnP_Components_Create(g_iotHubClientHandle, launchArgs->argc, launchArgs->argv)) - { - result = false; - } // Sets the name of ModelId for this PnP device. // This *MUST* be set before the client is connected to IoTHub. We do not automatically connect when the // handle is created, but will implicitly connect to subscribe for device method and device twin callbacks below. @@ -703,7 +868,7 @@ _Bool ADUC_DeviceClient_Create(ADUC_ConnectionInfo* connInfo, const ADUC_LaunchA // This will also automatically retrieve the full twin for the application. else if ( (iothubResult = ClientHandle_SetClientTwinCallback( - g_iotHubClientHandle, ADUC_PnPDeviceTwin_Callback, (void*)g_iotHubClientHandle)) + g_iotHubClientHandle, ADUC_PnPDeviceTwin_Callback, &g_iotHubInitiatedPnPPropertyChangeContext)) != IOTHUB_CLIENT_OK) { Log_Error("Unable to set device twin callback, error=%d", iothubResult); @@ -714,7 +879,7 @@ _Bool ADUC_DeviceClient_Create(ADUC_ConnectionInfo* connInfo, const ADUC_LaunchA ClientHandle_SetConnectionStatusCallback(g_iotHubClientHandle, ADUC_ConnectionStatus_Callback, NULL)) != IOTHUB_CLIENT_OK) { - Log_Error("Unable to set connection status calback, error=%d", iothubResult); + Log_Error("Unable to set connection status callback, error=%d", iothubResult); result = false; } else @@ -729,9 +894,27 @@ _Bool ADUC_DeviceClient_Create(ADUC_ConnectionInfo* connInfo, const ADUC_LaunchA g_iotHubClientHandle = NULL; } + if (shouldSetProxyOptions) + { + UninitializeProxyOptions(&proxyOptions); + } + return result; } +/** + * @brief Destroy IoTHub device client handle. + * + * @param deviceHandle IoTHub device client handle. + */ +static void ADUC_DeviceClient_Destroy(ADUC_ClientHandle clientHandle) +{ + if (clientHandle != NULL) + { + ClientHandle_Destroy(clientHandle); + } +} + /** * @brief Scans the connection string and returns the connection type related to the string * @details The connection string must use the valid, correct format for the DeviceId and/or the ModuleId @@ -849,7 +1032,7 @@ _Bool GetConnectionInfoFromIdentityService(ADUC_ConnectionInfo* info) Log_Info("Requesting connection string from the Edge Identity Service"); - time_t expirySecsSinceEpoch = time(NULL) + EIS_TOKEN_EXPIRY_TIME; + time_t expirySecsSinceEpoch = time(NULL) + EIS_TOKEN_EXPIRY_TIME_IN_SECONDS; EISUtilityResult eisProvisionResult = RequestConnectionStringFromEISWithExpiry(expirySecsSinceEpoch, EIS_PROVISIONING_TIMEOUT, info); @@ -869,6 +1052,127 @@ _Bool GetConnectionInfoFromIdentityService(ADUC_ConnectionInfo* info) return succeeded; } +/** + * @brief Invokes PnPHandleCommandCallback on every PnPComponentEntry. + * + * @param command The string contains command (and options) from other component or process. + * @param commandContext A data context associated with the command. + * @return _Bool + */ +static _Bool RetryUpdateCommandHandler(const char* command, void* commandContext) +{ + UNREFERENCED_PARAMETER(command); + UNREFERENCED_PARAMETER(commandContext); + IOTHUB_CLIENT_RESULT iothubResult = ClientHandle_GetTwinAsync( + g_iotHubClientHandle, + ADUC_PnPDeviceTwin_RetryUpdateCommand_Callback, + &g_deviceInitiatedRetryPnPPropertyChangeContext); + + return iothubResult == IOTHUB_CLIENT_OK; +} + +// This command can be use by other process, to tell a DU agent to retry the current update, if exist. +ADUC_Command redoUpdateCommand = { "retry-update", RetryUpdateCommandHandler }; + +/** + * @brief Gets the agent configuration information and loads it according to the provisioning scenario + * + * @param info the connection information that will be configured + * @return true on success; false on failure + */ +_Bool GetAgentConfigInfo(ADUC_ConnectionInfo* info) +{ + _Bool success = false; + ADUC_ConfigInfo config = {}; + if (info == NULL) + { + return false; + } + + if (!ADUC_ConfigInfo_Init(&config, ADUC_CONF_FILE_PATH)) + { + Log_Error("No connection string set from launch arguments or configuration file"); + goto done; + } + + const ADUC_AgentInfo* agent = ADUC_ConfigInfo_GetAgent(&config, 0); + if (agent == NULL) + { + Log_Error("ADUC_ConfigInfo_GetAgent failed to get the agent information."); + goto done; + } + + if (strcmp(agent->connectionType, "AIS") == 0) + { + if (!GetConnectionInfoFromIdentityService(info)) + { + Log_Error("Failed to get connection information from AIS."); + goto done; + } + } + else if (strcmp(agent->connectionType, "string") == 0) + { + if (!GetConnectionInfoFromConnectionString(info, agent->connectionData)) + { + goto done; + } + } + else + { + Log_Error("The connection type %s is not supported", agent->connectionType); + goto done; + } + + if (!ADUC_SetDiagnosticsDeviceNameFromConnectionString(info->connectionString)) + { + Log_Error("Setting DiagnosticsDeviceName failed"); + goto done; + } + + success = true; + +done: + if (!success) + { + ADUC_ConnectionInfo_DeAlloc(info); + } + + ADUC_ConfigInfo_UnInit(&config); + + return success; +} + +/** + * @brief Refresh the IotHub connection, then then set an IotHub client handle on every PnP sub-component. + * + * Note: Learn more about IotHub SAS tokens at https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-dev-guide-sas?tabs=node#sas-tokens + * + */ +static void ADUC_Refresh_IotHub_Connection_SAS_Token() +{ + ADUC_DeviceClient_Destroy(g_iotHubClientHandle); + + ADUC_ConnectionInfo info = {}; + if (!GetAgentConfigInfo(&info)) + { + goto done; + } + + if (!ADUC_DeviceClient_Create(g_iotHubClientHandle, &info, &launchArgs)) + { + Log_Error("ADUC_DeviceClient_Create failed"); + goto done; + } + + ADUC_PnP_Components_HandleRefresh(g_iotHubClientHandle); + + Log_Info("Successfully refreshed SAS Token"); + +done: + + ADUC_ConnectionInfo_DeAlloc(&info); +} + /** * @brief Handles the startup of the agent * @details Provisions the connection string with the CLI or either @@ -882,7 +1186,10 @@ _Bool StartupAgent(const ADUC_LaunchArguments* launchArgs) ADUC_ConnectionInfo info = {}; - ADUC_ConfigInfo config = {}; + if (!ADUC_D2C_Messaging_Init()) + { + goto done; + } if (launchArgs->connectionString != NULL) { @@ -904,7 +1211,7 @@ _Bool StartupAgent(const ADUC_LaunchArguments* launchArgs) goto done; } - if (!ADUC_DeviceClient_Create(&connInfo, launchArgs)) + if (!ADUC_DeviceClient_Create(g_iotHubClientHandle, &connInfo, launchArgs)) { Log_Error("ADUC_DeviceClient_Create failed"); goto done; @@ -912,52 +1219,24 @@ _Bool StartupAgent(const ADUC_LaunchArguments* launchArgs) } else { - if (!ADUC_ConfigInfo_Init(&config, ADUC_CONF_FILE_PATH)) - { - Log_Error("No connnection string set from launch arguments or configuration file"); - goto done; - } - - const ADUC_AgentInfo* agent = ADUC_ConfigInfo_GetAgent(&config, 0); - if (agent == NULL) - { - Log_Error("ADUC_ConfigInfo_GetAgent failed to get the agent information."); - goto done; - } - if (strcmp(agent->connectionType, "AIS") == 0) - { - if (!GetConnectionInfoFromIdentityService(&info)) - { - Log_Error("Failed to get connection information from AIS."); - goto done; - } - } - else if (strcmp(agent->connectionType, "string") == 0) + if (!GetAgentConfigInfo(&info)) { - if (!GetConnectionInfoFromConnectionString(&info, agent->connectionData)) - { - goto done; - } - } - else - { - Log_Error("The connection type %s is not supported", agent->connectionType); goto done; } - if (!ADUC_SetDiagnosticsDeviceNameFromConnectionString(info.connectionString)) - { - Log_Error("Setting DiagnosticsDeviceName failed"); - goto done; - } - - if (!ADUC_DeviceClient_Create(&info, launchArgs)) + if (!ADUC_DeviceClient_Create(g_iotHubClientHandle, &info, launchArgs)) { Log_Error("ADUC_DeviceClient_Create failed"); goto done; } } + if (!ADUC_PnP_Components_Create(g_iotHubClientHandle, launchArgs->argc, launchArgs->argv)) + { + Log_Error("ADUC_PnP_Components_Create failed"); + goto done; + } + ADUC_Result result; // The connection string is valid (IoT hub connection successful) and we are ready for further processing. @@ -971,6 +1250,18 @@ _Bool StartupAgent(const ADUC_LaunchArguments* launchArgs) result = ExtensionManager_InitializeContentDownloader(NULL /*initializeData*/); } + if (InitializeCommandListenerThread()) + { + RegisterCommand(&redoUpdateCommand); + } + else + { + Log_Error( + "Cannot initialize the command listener thread. Running another instance of DU Agent with --command will not work correctly."); + // Note: even though we can't create command listener here, we need to ensure that + // the agent stay alive and connected to the IoT hub. + } + if (IsAducResultCodeFailure(result.ResultCode)) { // Since it is nested edge and if DO fails to accept the connection string, then we go ahead and @@ -984,7 +1275,6 @@ _Bool StartupAgent(const ADUC_LaunchArguments* launchArgs) done: ADUC_ConnectionInfo_DeAlloc(&info); - ADUC_ConfigInfo_UnInit(&config); return succeeded; } @@ -994,6 +1284,8 @@ _Bool StartupAgent(const ADUC_LaunchArguments* launchArgs) void ShutdownAgent() { Log_Info("Agent is shutting down with signal %d.", g_shutdownSignal); + ADUC_D2C_Messaging_Uninit(); + UninitializeCommandListenerThread(); ADUC_PnP_Components_Destroy(); ADUC_DeviceClient_Destroy(g_iotHubClientHandle); DiagnosticsComponent_DestroyDeviceName(); @@ -1020,11 +1312,48 @@ void OnShutdownSignal(int sig) void OnRestartSignal(int sig) { // Note: Main loop will break once this becomes true. We rely on the 'Restart' setting in - // adu-agent.service file to instruct systemd to restart the agent. + // deviceupdate-agent.service file to instruct systemd to restart the agent. Log_Info("Restart signal detect."); g_shutdownSignal = sig; } +/** + * @brief Sets effective user id as specified in du-config.json (agents[#].ranAs property), + * and Sets effective group id to as ADUC_FILE_GROUP. + * + * This to ensure that the agent process is run with the intended privileges, and the resource that + * created by the agent has the correct ownership. + * + * @return _Bool + */ +_Bool RunAsDesiredUser() +{ + _Bool success = false; + ADUC_ConfigInfo config = {}; + if (!ADUC_ConfigInfo_Init(&config, ADUC_CONF_FILE_PATH)) + { + Log_Error("Cannot read configuration file."); + return false; + } + + if (!PermissionUtils_SetProcessEffectiveGID(ADUC_FILE_GROUP)) + { + Log_Error("Failed to set process effective group to '%s'. (errno:%d)", ADUC_FILE_GROUP, errno); + goto done; + } + + if (!PermissionUtils_SetProcessEffectiveUID(config.agents[0].runas)) + { + Log_Error("Failed to set process effective user to '%s'. (errno:%d)", config.agents[0].runas, errno); + goto done; + } + + success = true; +done: + ADUC_ConfigInfo_UnInit(&config); + return success; +} + // // Main. // @@ -1044,8 +1373,6 @@ int main(int argc, char** argv) { InitializeModeledComponents(); - ADUC_LaunchArguments launchArgs; - int ret = ParseLaunchArguments(argc, argv, &launchArgs); if (ret < 0) { @@ -1070,40 +1397,80 @@ int main(int argc, char** argv) return 1; } - if (launchArgs.contentHandlerFilePath != NULL) + if (launchArgs.extensionFilePath != NULL) { - if (launchArgs.updateType == NULL) + switch (launchArgs.extensionRegistrationType) { - Log_Error("Missing --update-type argument."); + case ExtensionRegistrationType_None: + Log_Error("Missing --extension-type argument."); return 1; - } - if (RegisterUpdateContentHandler(launchArgs.updateType, launchArgs.contentHandlerFilePath)) - { - return 0; - } + case ExtensionRegistrationType_UpdateContentHandler: + if (launchArgs.extensionId == NULL) + { + Log_Error("Missing --extension-id argument."); + return 1; + } - return 1; + if (RegisterUpdateContentHandler(launchArgs.extensionId, launchArgs.extensionFilePath)) + { + return 0; + } + + return 1; + + case ExtensionRegistrationType_ComponentEnumerator: + if (RegisterComponentEnumeratorExtension(launchArgs.extensionFilePath)) + { + return 0; + } + + return 1; + + case ExtensionRegistrationType_ContentDownloadHandler: + if (RegisterContentDownloaderExtension(launchArgs.extensionFilePath)) + { + return 0; + } + + return 1; + + case ExtensionRegistrationType_DownloadHandler: + if (launchArgs.extensionId == NULL) + { + Log_Error("Missing --extension-id argument."); + return 1; + } + + if (RegisterDownloadHandler(launchArgs.extensionId, launchArgs.extensionFilePath)) + { + return 0; + } + + return 1; + + default: + Log_Error("Unknown ExtensionRegistrationType: %d", launchArgs.extensionRegistrationType); + return 1; + } } - if (launchArgs.componentEnumeratorFilePath != NULL) + // This instance of an agent is launched for sending command to the main agent process. + if (launchArgs.ipcCommand != NULL) { - if (RegisterComponentEnumeratorExtension(launchArgs.componentEnumeratorFilePath)) + if (SendCommand(launchArgs.ipcCommand)) { return 0; } - return 1; } - if (launchArgs.contentDownloaderFilePath != NULL) + // Switch to specified agent.runas user. + // Note: it's important that we do this only when we're not performing any + // high-privileged tasks, such as, registering agent's extension(s). + if (!RunAsDesiredUser()) { - if (RegisterContentDownloaderExtension(launchArgs.contentDownloaderFilePath)) - { - return 0; - } - - return 1; + return 0; } Log_Info("Agent (%s; %s) starting.", ADUC_PLATFORM_LAYER, ADUC_VERSION); @@ -1111,6 +1478,10 @@ int main(int argc, char** argv) Log_Info("Git Info: %s", ADUC_GIT_INFO); #endif Log_Info("Agent built with handlers: %s.", ADUC_CONTENT_HANDLERS); + Log_Info( + "Supported Update Manifest version: min: %d, max: %d", + SUPPORTED_UPDATE_MANIFEST_VERSION_MIN, + SUPPORTED_UPDATE_MANIFEST_VERSION_MAX); _Bool healthy = HealthCheck(&launchArgs); if (launchArgs.healthCheckOnly || !healthy) @@ -1171,13 +1542,14 @@ int main(int argc, char** argv) } } + ADUC_D2C_Messaging_DoWork(); ClientHandle_DoWork(g_iotHubClientHandle); // NOTE: When using low level samples (iothub_ll_*), the IoTHubDeviceClient_LL_DoWork // function must be called regularly (eg. every 100 milliseconds) for the IoT device client to work properly. // See: https://github.com/Azure/azure-iot-sdk-c/tree/master/iothub_client/samples // NOTE: For this example the above has been wrapped to support module and device client methods using - // the clienty_handle_helper.h function ClientHandle_DoWork() + // the client_handle_helper.h function ClientHandle_DoWork() ThreadAPI_Sleep(100); }; diff --git a/src/agent_orchestration/CMakeLists.txt b/src/agent_orchestration/CMakeLists.txt new file mode 100644 index 000000000..67d39fe25 --- /dev/null +++ b/src/agent_orchestration/CMakeLists.txt @@ -0,0 +1,16 @@ +set (target_name agent_orchestration) + +include (agentRules) +compileasc99 () + +add_library (${target_name} STATIC "") +add_library (aduc::${target_name} ALIAS ${target_name}) + +target_include_directories (${target_name} PUBLIC inc ${ADUC_EXPORT_INCLUDES}) + +target_sources (${target_name} PRIVATE src/agent_orchestration.c) + +target_link_libraries ( + ${target_name} + PUBLIC aduc::adu_types + PRIVATE aduc::c_utils aduc::workflow_utils) diff --git a/src/agent/adu_core_interface/inc/aduc/agent_orchestration.h b/src/agent_orchestration/inc/aduc/agent_orchestration.h similarity index 87% rename from src/agent/adu_core_interface/inc/aduc/agent_orchestration.h rename to src/agent_orchestration/inc/aduc/agent_orchestration.h index c3b0c3450..2ef5db479 100644 --- a/src/agent/adu_core_interface/inc/aduc/agent_orchestration.h +++ b/src/agent_orchestration/inc/aduc/agent_orchestration.h @@ -17,16 +17,14 @@ * @param desiredUpdateAction The update action from the desired section of the twin * @return The mapped workflow step */ -ADUCITF_WorkflowStep AgentOrchestration_GetWorkflowStep( - const ADUCITF_UpdateAction desiredUpdateAction); +ADUCITF_WorkflowStep AgentOrchestration_GetWorkflowStep(const ADUCITF_UpdateAction desiredUpdateAction); /** * @brief Returns whether the workflow is complete. * @param entryAutoTransitionWorkflowStep The auto transition workflow step from the entry in the workflow handler map. * @return true if workflow is complete. */ -_Bool AgentOrchestration_IsWorkflowComplete( - ADUCITF_WorkflowStep entryAutoTransitionWorkflowStep); +_Bool AgentOrchestration_IsWorkflowComplete(ADUCITF_WorkflowStep entryAutoTransitionWorkflowStep); /** * @brief Returns whether reporting to the cloud should be done. diff --git a/src/agent/adu_core_interface/src/agent_orchestration.c b/src/agent_orchestration/src/agent_orchestration.c similarity index 71% rename from src/agent/adu_core_interface/src/agent_orchestration.c rename to src/agent_orchestration/src/agent_orchestration.c index a830dd6da..2ddd3fce4 100644 --- a/src/agent/adu_core_interface/src/agent_orchestration.c +++ b/src/agent_orchestration/src/agent_orchestration.c @@ -17,21 +17,20 @@ * @param desiredUpdateAction The update action from the desired section of the twin * @return The mapped workflow step */ -ADUCITF_WorkflowStep AgentOrchestration_GetWorkflowStep( - const ADUCITF_UpdateAction desiredUpdateAction) +ADUCITF_WorkflowStep AgentOrchestration_GetWorkflowStep(const ADUCITF_UpdateAction desiredUpdateAction) { switch (desiredUpdateAction) { - case ADUCITF_UpdateAction_ProcessDeployment: - return ADUCITF_WorkflowStep_ProcessDeployment; + case ADUCITF_UpdateAction_ProcessDeployment: + return ADUCITF_WorkflowStep_ProcessDeployment; - case ADUCITF_UpdateAction_Cancel: - // Should not get here as Cancel should have just signaled the cancel request - // to the current ongoing operation, or just went to idle. - return ADUCITF_WorkflowStep_Undefined; + case ADUCITF_UpdateAction_Cancel: + // Should not get here as Cancel should have just signaled the cancel request + // to the current ongoing operation, or just went to idle. + return ADUCITF_WorkflowStep_Undefined; - default: - return ADUCITF_WorkflowStep_Undefined; + default: + return ADUCITF_WorkflowStep_Undefined; } } @@ -40,8 +39,7 @@ ADUCITF_WorkflowStep AgentOrchestration_GetWorkflowStep( * @param entryAutoTransitionWorkflowStep The auto transition workflow step from the entry in the workflow handler map. * @return true if workflow is complete. */ -_Bool AgentOrchestration_IsWorkflowComplete( - ADUCITF_WorkflowStep entryAutoTransitionWorkflowStep) +_Bool AgentOrchestration_IsWorkflowComplete(ADUCITF_WorkflowStep entryAutoTransitionWorkflowStep) { return entryAutoTransitionWorkflowStep == ADUCITF_WorkflowStep_Undefined; } @@ -51,9 +49,9 @@ _Bool AgentOrchestration_IsWorkflowComplete( * @param updateState the current update state. * @return true if it should not report. */ -_Bool AgentOrchestration_ShouldNotReportToCloud(ADUCITF_State updateState) { - return updateState != ADUCITF_State_DeploymentInProgress - && updateState != ADUCITF_State_Idle +_Bool AgentOrchestration_ShouldNotReportToCloud(ADUCITF_State updateState) +{ + return updateState != ADUCITF_State_DeploymentInProgress && updateState != ADUCITF_State_Idle && updateState != ADUCITF_State_Failed; } diff --git a/src/communication_abstraction/inc/aduc/client_handle_helper.h b/src/communication_abstraction/inc/aduc/client_handle_helper.h index 6060f94f9..e372f7e89 100644 --- a/src/communication_abstraction/inc/aduc/client_handle_helper.h +++ b/src/communication_abstraction/inc/aduc/client_handle_helper.h @@ -107,6 +107,25 @@ MOCKABLE_FUNCTION( const void*, value); +/** + * @brief Wrapper for the device or model GetTwinAsync functions + * @details Uses either the device or module function depending on what the client type has been set to. + * @param iotHubClientHandle The clientHandle to be used for the operation + * @param deviceTwinCallback Callback for when the function completes + * @param userContextCallback A parameter to @p deviceTwinCallback + * @returns a value of IOTHUB_CLIENT_RESULT + */ +MOCKABLE_FUNCTION( + , + IOTHUB_CLIENT_RESULT, + ClientHandle_GetTwinAsync, + ADUC_ClientHandle, + iotHubClientHandle, + IOTHUB_CLIENT_DEVICE_TWIN_CALLBACK, + deviceTwinCallback, + void*, + userContextCallback) + /** * @brief Wrapper for the Device and Module SetClientTwinCallback functions * @details Uses either the device or module function depending on what the client type has been set to. diff --git a/src/communication_abstraction/src/client_handle_helper.c b/src/communication_abstraction/src/client_handle_helper.c index b7373894c..62e0a0bd5 100644 --- a/src/communication_abstraction/src/client_handle_helper.c +++ b/src/communication_abstraction/src/client_handle_helper.c @@ -229,6 +229,39 @@ ClientHandle_SetOption(ADUC_ClientHandle iotHubClientHandle, const char* optionN return result; } +/** + * @brief Wrapper for the device or model GetTwinAsync functions + * @details Uses either the device or module function depending on what the client type has been set to. + * @param iotHubClientHandle The clientHandle to be used for the operation + * @param deviceTwinCallback Callback for when the function completes + * @param userContextCallback A parameter to @p deviceTwinCallback + * @returns a value of IOTHUB_CLIENT_RESULT + */ +IOTHUB_CLIENT_RESULT +ClientHandle_GetTwinAsync( + ADUC_ClientHandle iotHubClientHandle, + IOTHUB_CLIENT_DEVICE_TWIN_CALLBACK deviceTwinCallback, + void* userContextCallback) +{ + IOTHUB_CLIENT_RESULT result = IOTHUB_CLIENT_INVALID_ARG; + if (g_ClientHandleType == ADUC_ConnType_Device) + { + result = IoTHubDeviceClient_LL_GetTwinAsync( + GetDeviceClientHandle(iotHubClientHandle), deviceTwinCallback, userContextCallback); + } + else if (g_ClientHandleType == ADUC_ConnType_Module) + { + result = IoTHubModuleClient_LL_GetTwinAsync( + GetModuleClientHandle(iotHubClientHandle), deviceTwinCallback, userContextCallback); + } + else + { + Log_Error("ClientHandle_SetOption before called ClientHandle_CreateFromConnectionString"); + } + + return result; +} + /** * @brief Wrapper for the Device and Module SetClientTwinCallback functions * @details Uses either the device or module function depending on what the client type has been set to. @@ -359,4 +392,6 @@ void ClientHandle_Destroy(ADUC_ClientHandle iotHubClientHandle) { Log_Error("ClientHandle_Destroy before called ClientHandle_CreateFromConnectionString"); } + + g_ClientHandleType = ADUC_ConnType_NotSet; } diff --git a/src/content_handlers/CMakeLists.txt b/src/content_handlers/CMakeLists.txt deleted file mode 100644 index 60a09f5cc..000000000 --- a/src/content_handlers/CMakeLists.txt +++ /dev/null @@ -1,43 +0,0 @@ -cmake_minimum_required (VERSION 3.5) - -project (content_handlers) - -add_library (${PROJECT_NAME} STATIC src/content_handler_factory.cpp) -add_library (aduc::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) - -target_include_directories (${PROJECT_NAME} PUBLIC inc ${ADUC_EXPORT_INCLUDES} - ${ADU_EXTENSION_INCLUDES}) -# -# Note: add ${CMAKE_DL_LIBS} for dynamic library loading support. -# -target_link_libraries ( - ${PROJECT_NAME} - PRIVATE aduc::c_utils - aduc::exception_utils - aduc::extension_utils - aduc::logging - aduc::string_utils - aduc::workflow_data_utils - ${CMAKE_DL_LIBS}) - -# -# Turn -fPIC on, in order to use this library in another shared library. -# -set_property (TARGET ${PROJECT_NAME} PROPERTY POSITION_INDEPENDENT_CODE ON) - -add_subdirectory (apt_handler) -add_subdirectory (script_handler) -add_subdirectory (simulator_handler) -add_subdirectory (steps_handler) -add_subdirectory (swupdate_handler) - -# -# Note: add more update content handler extensions here. -# -# e.g. -# add_subdirectory (my_handler) -# - -if (${ADUC_PLATFORM_LAYER} STREQUAL "simulator") - target_compile_definitions (${PROJECT_NAME} PUBLIC ADUC_SIMULATOR_MODE=1) -endif () diff --git a/src/content_handlers/inc/aduc/content_handler_factory.hpp b/src/content_handlers/inc/aduc/content_handler_factory.hpp deleted file mode 100644 index 56b158a56..000000000 --- a/src/content_handlers/inc/aduc/content_handler_factory.hpp +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @file content_handler_factory.hpp - * @brief Definition of the ContentHandlerFactory. - * - * @copyright Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ - -#ifndef ADUC_CONTENT_HANDLER_FACTORY_HPP -#define ADUC_CONTENT_HANDLER_FACTORY_HPP - -#include "aduc/result.h" -#include "aduc/logging.h" - -#include -#include -#include - -// Forward declaration. -class ContentHandler; - -typedef ContentHandler* (*UPDATE_CONTENT_HANDLER_CREATE_PROC)(ADUC_LOG_SEVERITY logLevel); - -class ContentHandlerFactory -{ -public: - static ADUC_Result LoadUpdateContentHandlerExtension(const std::string& updateType, ContentHandler** handler); - static void UnloadAllUpdateContentHandlers(); - static void UnloadAllExtensions(); - static void Uninit(); - -private: - static ADUC_Result LoadExtensionLibrary(const std::string& updateType, void** libHandle); - static std::unordered_map _libs; - static std::unordered_map _contentHandlers; - static pthread_mutex_t factoryMutex; -}; - -#endif // ADUC_CONTENT_HANDLER_FACTORY_HPP diff --git a/src/content_handlers/src/content_handler_factory.cpp b/src/content_handlers/src/content_handler_factory.cpp deleted file mode 100644 index c0e8ab741..000000000 --- a/src/content_handlers/src/content_handler_factory.cpp +++ /dev/null @@ -1,266 +0,0 @@ -/** - * @file content_handler_factory.cpp - * @brief Implementation of ContentHandlerFactory. - * - * @copyright Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ -#include "aduc/content_handler_factory.hpp" -#include "aduc/content_handler.hpp" - -#include "aduc/c_utils.h" -#include "aduc/exceptions.hpp" -#include "aduc/extension_utils.h" -#include "aduc/hash_utils.h" // for SHAversion -#include "aduc/logging.h" -#include "aduc/parser_utils.h" -#include "aduc/result.h" -#include "aduc/string_utils.hpp" - -#include -#include -#include - -// Note: this requires ${CMAKE_DL_LIBS} -#include - -std::unordered_map ContentHandlerFactory::_libs; -std::unordered_map ContentHandlerFactory::_contentHandlers; - -/** - * @brief Loads UpdateContentHandler extension. - * @param updateType An update type string. - * @param libHandle A buffer for storing output extension library handle. - * @return ADUC_Result contains result code and extended result code. - */ -ADUC_Result ContentHandlerFactory::LoadExtensionLibrary(const std::string& updateType, void** libHandle) -{ - ADUC_Result result{ ADUC_GeneralResult_Success }; - - Log_Info("Loading Update Content Handler for '%s'.", updateType.c_str()); - - if (libHandle == nullptr) - { - Log_Error("Invalid argument(s)."); - return { ADUC_GeneralResult_Failure, ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_INVALID_ARG }; - } - - // Try to find cached handle. - try - { - *libHandle = ContentHandlerFactory::_libs.at(updateType); - return { ADUC_GeneralResult_Success }; - } - catch (...) - { - Log_Debug("Cache not found. Will create a new one."); - } - - char* error = nullptr; - UPDATE_CONTENT_HANDLER_CREATE_PROC createUpdateContentHandlerExtension = nullptr; - ADUC_FileEntity entity; - memset(&entity, 0, sizeof(entity)); - - if (!GetUpdateContentHandlerFileEntity(updateType.c_str(), &entity)) - { - Log_Error("Update Content Handler for '%s' not found.", updateType.c_str()); - return { ADUC_GeneralResult_Failure, ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_NOT_FOUND }; - } - - // Validate file hash. - SHAversion algVersion; - if (!ADUC_HashUtils_GetShaVersionForTypeString( - ADUC_HashUtils_GetHashType(entity.Hash, entity.HashCount, 0), &algVersion)) - { - Log_Error( - "FileEntity for %s has unsupported hash type %s", - entity.TargetFilename, - ADUC_HashUtils_GetHashType(entity.Hash, entity.HashCount, 0)); - result = { ADUC_GeneralResult_Failure, ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_VALIDATE }; - goto done; - } - - if (!ADUC_HashUtils_IsValidFileHash( - entity.TargetFilename, - ADUC_HashUtils_GetHashValue(entity.Hash, entity.HashCount, 0), - algVersion, - false)) - { - Log_Error("Hash for %s is not valid", entity.TargetFilename); - result = { ADUC_GeneralResult_Failure, ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_VALIDATE }; - goto done; - } - - Log_Debug("Loading update content handler from '%s'.", entity.TargetFilename); - - *libHandle = dlopen(entity.TargetFilename, RTLD_LAZY); - - if (*libHandle == nullptr) - { - Log_Error("Cannot load content handler file %s. %s.", entity.TargetFilename, dlerror()); - result = { ADUC_GeneralResult_Failure, ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_LOAD }; - goto done; - } - - dlerror(); // Clear any existing error - - createUpdateContentHandlerExtension = - reinterpret_cast(dlsym(*libHandle, "CreateUpdateContentHandlerExtension")); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - - if (createUpdateContentHandlerExtension == nullptr) - { - Log_Error("The specified function doesn't exist. %s\n", dlerror()); - result = { ADUC_GeneralResult_Failure, ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_NO_SYMBOL }; - goto done; - } - - // Cache the loaded library. - try - { - ContentHandlerFactory::_libs.emplace(updateType, *libHandle); - } - catch (...) - { - Log_Warn("Failed to cache lib handle."); - } - -done: - if (IsAducResultCodeFailure(result.ResultCode)) - { - if (libHandle != nullptr) - { - dlclose(*libHandle); - *libHandle = nullptr; - } - } - - // Done with file entity. - ADUC_FileEntity_Uninit(&entity); - - return result; -} - -/** - * @brief Loads UpdateContentHandler for specified @p updateType - * @param updateType An update type string. - * @param handler A buffer for storing an output UpdateContentHandler object. - * @return ADUCResult contains result code and extended result code. - * */ -ADUC_Result -ContentHandlerFactory::LoadUpdateContentHandlerExtension(const std::string& updateType, ContentHandler** handler) -{ - UPDATE_CONTENT_HANDLER_CREATE_PROC createUpdateContentHandlerExtension = nullptr; - void* libHandle = nullptr; - ADUC_Result result = { ADUC_GeneralResult_Success }; - - Log_Info("Loading Update Content Handler for '%s'.", updateType.c_str()); - - if (handler == nullptr) - { - Log_Error("Invalid argument(s)."); - return { ADUC_GeneralResult_Failure, ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_INVALID_ARG }; - } - - // Try to find cached handler. - *handler = nullptr; - if (_contentHandlers.count(updateType) > 0) - { - try - { - *handler = _contentHandlers.at(updateType); - } - catch (const std::exception& ex) - { - Log_Debug("An exception occurred: %s", ex.what()); - } - catch (...) - { - Log_Debug("Unknown exception occurred while try to reuse a handler for '%s'", updateType.c_str()); - } - } - - if (IsAducResultCodeSuccess(result.ResultCode) && *handler != nullptr) - { - goto done; - } - - result = LoadExtensionLibrary(updateType, &libHandle); - if (IsAducResultCodeFailure(result.ResultCode)) - { - goto done; - } - - dlerror(); // Clear any existing error - - createUpdateContentHandlerExtension = - reinterpret_cast(dlsym(libHandle, "CreateUpdateContentHandlerExtension")); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - - if (createUpdateContentHandlerExtension == nullptr) - { - Log_Error("The specified function doesn't exist. %s\n", dlerror()); - result = { ADUC_GeneralResult_Failure, ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_NO_SYMBOL }; - goto done; - } - - try - { - *handler = createUpdateContentHandlerExtension(ADUC_Logging_GetLevel()); - } - catch (const std::exception& ex) - { - Log_Error("An exception occurred while laoding content handler for '%s': %s", updateType.c_str(), ex.what()); - } - catch (...) - { - Log_Error("An unknown exception occurred while laoding content handler for '%s'", updateType.c_str()); - } - - if (*handler == nullptr) - { - result = { ADUC_GeneralResult_Failure, ADUC_ERC_UPDATE_CONTENT_HANDLER_CREATE_FAILURE_CREATE }; - goto done; - } - - Log_Debug("Caching new content handler for '%s'.", updateType.c_str()); - _contentHandlers.emplace(updateType, *handler); - -done: - if (result.ResultCode == 0) - { - if (libHandle != nullptr) - { - dlclose(libHandle); - libHandle = nullptr; - } - } - - return result; -} - -void ContentHandlerFactory::UnloadAllUpdateContentHandlers() -{ - for (auto& contentHandler : _contentHandlers) - { - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory, cppcoreguidelines-no-malloc, hicpp-no-malloc) - delete (contentHandler.second); - } - - _contentHandlers.clear(); -} - -/** - * @brief This API unloads all handlers then unload all extension libraries. - * - */ -void ContentHandlerFactory::UnloadAllExtensions() -{ - // Make sure we unload every handlers first. - UnloadAllUpdateContentHandlers(); - - for (auto& lib : _libs) - { - dlclose(lib.second); - } - - _libs.clear(); -} diff --git a/src/deps/swupdate/.config b/src/deps/swupdate/.config new file mode 100644 index 000000000..9d7db1c1b --- /dev/null +++ b/src/deps/swupdate/.config @@ -0,0 +1,121 @@ +# +# Automatically generated file; DO NOT EDIT. +# SWUpdate Configuration +# + +# +# SWUpdate Settings +# + +# +# General Configuration +# +CONFIG_CURL=y +# CONFIG_CURL_SSL is not set +# CONFIG_DISKFORMAT is not set +# CONFIG_SYSTEMD is not set +CONFIG_DEFAULT_CONFIG_FILE="/usr/local/du/tests/swupdate.cfg" +CONFIG_SCRIPTS=y +CONFIG_HW_COMPATIBILITY=y +CONFIG_HW_COMPATIBILITY_FILE="/usr/local/du/tests/hwrevision" +CONFIG_SW_VERSIONS_FILE="/usr/local/du/tests/sw-versions" + +# +# Socket Paths +# +CONFIG_SOCKET_CTRL_PATH="" +CONFIG_SOCKET_PROGRESS_PATH="" +# CONFIG_MTD is not set +# CONFIG_LUA is not set +# CONFIG_FEATURE_SYSLOG is not set + +# +# Build Options +# +CONFIG_CROSS_COMPILE="" +CONFIG_SYSROOT="" +CONFIG_EXTRA_CFLAGS="-g" +CONFIG_EXTRA_LDFLAGS="" +CONFIG_EXTRA_LDLIBS="" + +# +# Debugging Options +# +CONFIG_DEBUG=y +CONFIG_DEBUG_PESSIMIZE=y +# CONFIG_WERROR is not set +CONFIG_NOCLEANUP=y + +# +# Bootloader support +# +# CONFIG_BOOTLOADER_EBG is not set +# CONFIG_UBOOT is not set +CONFIG_BOOTLOADER_NONE=y +# CONFIG_BOOTLOADER_GRUB is not set +CONFIG_UPDATE_STATE_CHOICE_NONE=y +# CONFIG_UPDATE_STATE_CHOICE_BOOTLOADER is not set + +# +# Interfaces +# +CONFIG_DOWNLOAD=y +# CONFIG_DOWNLOAD_SSL is not set +CONFIG_CHANNEL_CURL=y +# CONFIG_SURICATTA is not set +CONFIG_WEBSERVER=y +CONFIG_MONGOOSE=y +CONFIG_MONGOOSEIPV6=y +CONFIG_MONGOOSESSL=y + +# +# Security +# +# CONFIG_SSL_IMPL_NONE is not set +CONFIG_SSL_IMPL_OPENSSL=y +# CONFIG_SSL_IMPL_WOLFSSL is not set +# CONFIG_SSL_IMPL_MBEDTLS is not set +CONFIG_HASH_VERIFY=y +# CONFIG_SIGNED_IMAGES is not set +CONFIG_ENCRYPTED_IMAGES=y +# CONFIG_ENCRYPTED_SW_DESCRIPTION is not set +# CONFIG_PKCS11 is not set + +# +# Compressors (zlib always on) +# +CONFIG_GUNZIP=y +# CONFIG_ZSTD is not set + +# +# Parsers +# + +# +# Parser Features +# +CONFIG_LIBCONFIG=y +CONFIG_PARSERROOT="software" +# CONFIG_JSON is not set +# CONFIG_SETSWDESCRIPTION is not set + +# +# Handlers +# + +# +# Image Handlers +# +# CONFIG_ARCHIVE is not set +# CONFIG_BOOTLOADERHANDLER is not set +# CONFIG_DELTA is not set +# CONFIG_DISKPART is not set +# CONFIG_DISKFORMAT_HANDLER is not set +CONFIG_RAW=y +# CONFIG_RDIFFHANDLER is not set +# CONFIG_READBACKHANDLER is not set +# CONFIG_REMOTE_HANDLER is not set +CONFIG_SHELLSCRIPTHANDLER=y +# CONFIG_SWUFORWARDER_HANDLER is not set +# CONFIG_UCFWHANDLER is not set +# CONFIG_UNIQUEUUID is not set diff --git a/src/diagnostics_component/diagnostics_async_helper/CMakeLists.txt b/src/diagnostics_component/diagnostics_async_helper/CMakeLists.txt index d682c637b..b2988d5cd 100644 --- a/src/diagnostics_component/diagnostics_async_helper/CMakeLists.txt +++ b/src/diagnostics_component/diagnostics_async_helper/CMakeLists.txt @@ -2,11 +2,6 @@ cmake_minimum_required (VERSION 3.5) set (target_name diagnostics_async_helper) -include (find_curl_and_import_libcurl) - -# Used to find and include the CURL::libcurl imported libary -find_curl_and_import_libcurl () - add_library (${target_name} STATIC src/diagnostics_async_helper.cpp) add_library (diagnostics_component::${target_name} ALIAS ${target_name}) diff --git a/src/diagnostics_component/diagnostics_interface/CMakeLists.txt b/src/diagnostics_component/diagnostics_interface/CMakeLists.txt index e1d75bfca..b0b776425 100644 --- a/src/diagnostics_component/diagnostics_interface/CMakeLists.txt +++ b/src/diagnostics_component/diagnostics_interface/CMakeLists.txt @@ -6,9 +6,6 @@ include (agentRules) compileasc99 () -# Used to find and include the CURL::libcurl imported libary -find_curl_and_import_libcurl () - add_library (${target_name} STATIC src/diagnostics_interface.c) add_library (diagnostics_component::${target_name} ALIAS ${target_name}) @@ -25,6 +22,11 @@ target_link_libraries ( ${target_name} PUBLIC aduc::c_utils aduc::communication_abstraction + diagnostics_component::diagnostics_async_helper diagnostics_component::diagnostics_workflow + diagnostic_utils::diagnostics_config_utils Parson::parson - PRIVATE aduc::logging aduc::pnp_helper IotHubClient::iothub_client) + PRIVATE aduc::d2c_messaging + aduc::logging + aduc::pnp_helper + IotHubClient::iothub_client) diff --git a/src/diagnostics_component/diagnostics_interface/inc/diagnostics_interface.h b/src/diagnostics_component/diagnostics_interface/inc/diagnostics_interface.h index cc246e2b8..9ea23869c 100644 --- a/src/diagnostics_component/diagnostics_interface/inc/diagnostics_interface.h +++ b/src/diagnostics_component/diagnostics_interface/inc/diagnostics_interface.h @@ -8,10 +8,11 @@ #ifndef DIAGNOSTICS_INTERFACE_H #define DIAGNOSTICS_INTERFACE_H +#include #include #include #include -#include +#include #include #include @@ -82,7 +83,12 @@ void DiagnosticsInterface_Destroy(void** componentContext); * @brief A callback for the diagnostic component's property update events. */ void DiagnosticsInterface_PropertyUpdateCallback( - ADUC_ClientHandle clientHandle, const char* propertyName, JSON_Value* propertyValue, int version, void* context); + ADUC_ClientHandle clientHandle, + const char* propertyName, + JSON_Value* propertyValue, + int version, + ADUC_PnPComponentClient_PropertyUpdate_Context* sourceContext, + void* context); // // Reporting diff --git a/src/diagnostics_component/diagnostics_interface/src/diagnostics_interface.c b/src/diagnostics_component/diagnostics_interface/src/diagnostics_interface.c index e23552b3f..27bc13e47 100644 --- a/src/diagnostics_component/diagnostics_interface/src/diagnostics_interface.c +++ b/src/diagnostics_component/diagnostics_interface/src/diagnostics_interface.c @@ -9,10 +9,12 @@ #include "aduc/c_utils.h" #include "aduc/client_handle_helper.h" +#include "aduc/d2c_messaging.h" #include "aduc/logging.h" #include "aduc/string_c_utils.h" // atoint64t #include // isalnum -#include +#include // for DiagnosticsWorkflow_DiscoverAndUploadLogsAsync +#include // for DiagnosticsWorkflowData, DiagnosticsWorkflow_InitFromFile #include #include @@ -61,7 +63,7 @@ _Bool DiagnosticsInterface_Create(void** componentContext, int argc, char** argv goto done; } - if (!DiagnosticsWorkflow_InitFromFile(workflowData, DIAGNOSTICS_CONFIG_FILE_PATH)) + if (!DiagnosticsConfigUtils_InitFromFile(workflowData, DIAGNOSTICS_CONFIG_FILE_PATH)) { Log_Error("Unable to initialize the diagnostic workflow data."); goto done; @@ -73,7 +75,7 @@ _Bool DiagnosticsInterface_Create(void** componentContext, int argc, char** argv if (!succeeded) { - DiagnosticsWorkflow_UnInit(workflowData); + DiagnosticsConfigUtils_UnInit(workflowData); free(workflowData); workflowData = NULL; } @@ -105,65 +107,32 @@ void DiagnosticsInterface_Destroy(void** componentContext) DiagnosticsWorkflowData* workflowData = (DiagnosticsWorkflowData*)*componentContext; - DiagnosticsWorkflow_UnInit(workflowData); + DiagnosticsConfigUtils_UnInit(workflowData); free(workflowData); *componentContext = NULL; } -void DiagnosticsClientReportedStateCallback(int statusCode, void* context) -{ - UNREFERENCED_PARAMETER(context); - - if (statusCode < 200 || statusCode >= 300) - { - Log_Error( - "Failed to report ADU agent's state, error: %d, %s", - statusCode, - MU_ENUM_TO_STRING(IOTHUB_CLIENT_RESULT, statusCode)); - } -} - /** - * @brief Sends status message to IotHub - * @param clientHandle handle for the IotHub client handle to send the message to - * @param msgToSend message to send to IotHub - * @param msgSize size of @p msgToSend - * @returns the result of the IotHub message send action + * @brief This function is called when the message is no longer being process. + * + * @param context The ADUC_D2C_Message object + * @param status The message status. */ -static IOTHUB_CLIENT_RESULT -SendMessageToIotHub(ADUC_ClientHandle clientHandle, const char* msgToSend, const size_t msgSize) +static void OnDiagnosticsD2CMessageCompleted(void* context, ADUC_D2C_Message_Status status) { - IOTHUB_CLIENT_RESULT iothubClientResult = IOTHUB_CLIENT_ERROR; - - if (g_iotHubClientHandleForDiagnosticsComponent == NULL) - { - Log_Error("SendMessageToIotHub called before registration. Can't report!"); - goto done; - } - - iothubClientResult = ClientHandle_SendReportedState( - clientHandle, (const unsigned char*)msgToSend, msgSize, DiagnosticsClientReportedStateCallback, NULL); - - if (iothubClientResult != IOTHUB_CLIENT_OK) - { - goto done; - } - -done: - - return iothubClientResult; + UNREFERENCED_PARAMETER(context); + Log_Debug("Send message completed (status:%d)", status); } /** * @brief Function for sending a PnP message to the IotHub * @param clientHandle handle for the IotHub client handle to send the message to * @param jsonString message to send to the iothub - * @returns the result of the IotHub message send action + * @returns True if success. */ -static IOTHUB_CLIENT_RESULT SendPnPMessageToIotHub(ADUC_ClientHandle clientHandle, const char* jsonString) +static _Bool SendPnPMessageToIotHub(ADUC_ClientHandle clientHandle, const char* jsonString) { - IOTHUB_CLIENT_RESULT iothubClientResult = IOTHUB_CLIENT_ERROR; - + _Bool success = false; // Reporting just a message STRING_HANDLE jsonToSend = PnP_CreateReportedProperty( g_diagnosticsPnPComponentName, g_diagnosticsPnPComponentAgentPropertyName, jsonString); @@ -174,21 +143,25 @@ static IOTHUB_CLIENT_RESULT SendPnPMessageToIotHub(ADUC_ClientHandle clientHandl goto done; } - const char* jsonToSendStr = STRING_c_str(jsonToSend); - size_t jsonToSendStrLen = STRING_length(jsonToSend); - - iothubClientResult = SendMessageToIotHub(clientHandle, jsonToSendStr, jsonToSendStrLen); - - if (iothubClientResult != IOTHUB_CLIENT_OK) + if (!ADUC_D2C_Message_SendAsync( + ADUC_D2C_Message_Type_Diagnostics, + &g_iotHubClientHandleForDiagnosticsComponent, + STRING_c_str(jsonToSend), + NULL /* responseCallback */, + OnDiagnosticsD2CMessageCompleted, + NULL /* statusChangedCallback */, + NULL /* userData */)) { + Log_Error("Unable to send diagnostics message."); goto done; } + success = true; done: STRING_delete(jsonToSend); - return iothubClientResult; + return success; } /** @@ -197,13 +170,13 @@ static IOTHUB_CLIENT_RESULT SendPnPMessageToIotHub(ADUC_ClientHandle clientHandl * @param jsonString message to send to the iothub * @param status value to set as the status to send up to the iothub * @param propertyVersion value for the version to send up to the iothub - * @returns the result of the IotHub message send action + * @returns True if success. */ -static IOTHUB_CLIENT_RESULT SendPnPMessageToIotHubWithStatus( +static _Bool SendPnPMessageToIotHubWithStatus( ADUC_ClientHandle clientHandle, const char* jsonString, int status, int propertyVersion) { STRING_HANDLE jsonToSend = NULL; - IOTHUB_CLIENT_RESULT iothubClientResult = IOTHUB_CLIENT_ERROR; + _Bool success = false; if (g_iotHubClientHandleForDiagnosticsComponent == NULL) { @@ -221,24 +194,30 @@ static IOTHUB_CLIENT_RESULT SendPnPMessageToIotHubWithStatus( if (jsonToSend == NULL) { - Log_Error("Unable to serialize JSON passed to SendPnPMessagetoIotHub"); + Log_Error("Unable to serialize JSON passed to SendPnPMessageToIotHub"); goto done; } - const char* jsonToSendStr = STRING_c_str(jsonToSend); - const size_t jsonToSendStrLen = STRING_length(jsonToSend); - - iothubClientResult = SendMessageToIotHub(clientHandle, jsonToSendStr, jsonToSendStrLen); - if (iothubClientResult != IOTHUB_CLIENT_OK) + if (!ADUC_D2C_Message_SendAsync( + ADUC_D2C_Message_Type_Diagnostics_ACK, + &g_iotHubClientHandleForDiagnosticsComponent, + STRING_c_str(jsonToSend), + NULL /* responseCallback */, + OnDiagnosticsD2CMessageCompleted, + NULL /* statusChangedCallback */, + NULL /* userData */)) { + Log_Error("Unable to send diagnostic ACK message."); goto done; } + success = true; + done: STRING_delete(jsonToSend); - return iothubClientResult; + return success; } void DiagnosticsOrchestratorUpdateCallback( @@ -256,16 +235,11 @@ void DiagnosticsOrchestratorUpdateCallback( DiagnosticsWorkflow_DiscoverAndUploadLogsAsync(context, jsonString); - // Ack the request - IOTHUB_CLIENT_RESULT iothubClientResult = - SendPnPMessageToIotHubWithStatus(clientHandle, jsonString, PNP_STATUS_SUCCESS, propertyVersion); - - if (iothubClientResult != IOTHUB_CLIENT_OK) + // Ack the request! + if (!SendPnPMessageToIotHubWithStatus(clientHandle, jsonString, PNP_STATUS_SUCCESS, propertyVersion)) { Log_Error( - "Unable to send acknowledgement of property to IoT Hub for component=%s, error=%d", - g_diagnosticsPnPComponentName, - iothubClientResult); + "Unable to send acknowledgement of property to IoT Hub for component=%s", g_diagnosticsPnPComponentName); goto done; } @@ -278,8 +252,14 @@ void DiagnosticsOrchestratorUpdateCallback( * @brief A callback for the diagnostic component's property update events. */ void DiagnosticsInterface_PropertyUpdateCallback( - ADUC_ClientHandle clientHandle, const char* propertyName, JSON_Value* propertyValue, int version, void* context) + ADUC_ClientHandle clientHandle, + const char* propertyName, + JSON_Value* propertyValue, + int version, + ADUC_PnPComponentClient_PropertyUpdate_Context* sourceContext, + void* context) { + UNREFERENCED_PARAMETER(sourceContext); if (strcmp(propertyName, g_diagnosticsPnPComponentOrchestratorPropertyName) == 0) { DiagnosticsOrchestratorUpdateCallback(clientHandle, propertyValue, version, context); @@ -326,16 +306,9 @@ void DiagnosticsInterface_ReportStateAndResultAsync(const Diagnostics_Result res goto done; } - IOTHUB_CLIENT_RESULT iotHubResult = - SendPnPMessageToIotHub(g_iotHubClientHandleForDiagnosticsComponent, jsonString); - - if (iotHubResult != IOTHUB_CLIENT_OK) + if (!SendPnPMessageToIotHub(g_iotHubClientHandleForDiagnosticsComponent, jsonString)) { - Log_Error( - "Diagnostics Interface unable to report state, %s, error: %d, %s", - jsonString, - iotHubResult, - MU_ENUM_TO_STRING(IOTHUB_CLIENT_RESULT, iotHubResult)); + Log_Error("Diagnostics Interface unable to report state, %s", jsonString); goto done; } diff --git a/src/diagnostics_component/diagnostics_workflow/CMakeLists.txt b/src/diagnostics_component/diagnostics_workflow/CMakeLists.txt index e31f1493d..a481ef72d 100644 --- a/src/diagnostics_component/diagnostics_workflow/CMakeLists.txt +++ b/src/diagnostics_component/diagnostics_workflow/CMakeLists.txt @@ -3,13 +3,9 @@ cmake_minimum_required (VERSION 3.5) set (target_name diagnostics_workflow) include (agentRules) -include (find_curl_and_import_libcurl) compileasc99 () -# Used to find and include the CURL::libcurl imported libary -find_curl_and_import_libcurl() - add_library (${target_name} STATIC src/diagnostics_workflow.c src/diagnostics_result.c) add_library (diagnostics_component::${target_name} ALIAS ${target_name}) @@ -24,6 +20,7 @@ find_package (Parson REQUIRED) target_link_libraries ( ${target_name} PUBLIC aduc::c_utils diagnostics_component::diagnostics_async_helper + diagnostic_utils::diagnostics_config_utils PRIVATE aduc::logging aduc::system_utils aziotsharedutil @@ -35,7 +32,3 @@ target_link_libraries ( diagnostic_utils::operation_id_utils Parson::parson parson_json_utils) - -if (ADUC_BUILD_UNIT_TESTS) - add_subdirectory (tests) -endif () diff --git a/src/diagnostics_component/diagnostics_workflow/inc/diagnostics_workflow.h b/src/diagnostics_component/diagnostics_workflow/inc/diagnostics_workflow.h index 7c887a117..7bf7e0563 100644 --- a/src/diagnostics_component/diagnostics_workflow/inc/diagnostics_workflow.h +++ b/src/diagnostics_component/diagnostics_workflow/inc/diagnostics_workflow.h @@ -9,69 +9,10 @@ #define DIAGNOSTICS_WORKFLOW_H #include -#include -#include -#include -#include -#include -#include +#include EXTERN_C_BEGIN -/** - * @brief Forward declaration of JSON_Value used in Parson - */ -typedef struct json_value_t JSON_Value; - -/** - * @brief Describes each component for which to collect logs. - */ -typedef struct tagDiagnosticLogComponent -{ - STRING_HANDLE componentName; //!< Name of the component for which to collect logs - STRING_HANDLE logPath; //!< Absolute path to the directory where the logs are stored -} DiagnosticsLogComponent; - -/** - * @brief Data structure representing the data needed for the Diagnostics Workflow - */ -typedef struct tagDiagnosticWorkflowData -{ - VECTOR_HANDLE components; //!< Vector of DiagnosticLogComponent pointers for which to collect logs - unsigned int maxBytesToUploadPerLogPath; //!< The maximum number of bytes to upload per log file path -} DiagnosticsWorkflowData; - -/** - * @brief Initializes @p workflowData with the contents of @p filePath - * @param[out] workflowData the workflowData structure to be intialized. - * @param[in] filePath path to the diagnostics configuration file - * @returns true on successful configuration, false on failure - */ -_Bool DiagnosticsWorkflow_InitFromFile(DiagnosticsWorkflowData* workflowData, const char* filePath); - -/** - * @brief Initializes @p workflowData with the contents of @p fileJsonValue - * @param[out] workflowData the structure to be initialized - * @param[in] fileJsonValue a JSON_Value representation of a diagnostics-config.json file - * @returns true on successful configuration; false on failures - */ -_Bool DiagnosticsWorkflow_InitFromJSON(DiagnosticsWorkflowData* workflowData, JSON_Value* fileJsonValue); - -/** - * @brief UnInitializes @p workflowData's data members - * @param workflowData the workflowData structure to be unintialized. - */ -void DiagnosticsWorkflow_UnInit(DiagnosticsWorkflowData* workflowData); - -/** - * @brief Returns the DiagnosticsLogComponent object @p index within @p workflowData - * @param workflowData the DiagnosticsWorkflowData vector from which to get the DiagnosticsComponent - * @param index the index from which to retrieve the value - * @returns NULL if index is out of range; the DiagnosticsComponent otherwise - */ -const DiagnosticsLogComponent* -DiagnosticsWorkflow_GetLogComponentElem(const DiagnosticsWorkflowData* workflowData, unsigned int index); - /** * @brief Uploads the diagnostic logs described by @p workflowData * @param workflowData the workflowData structure describing the log components diff --git a/src/diagnostics_component/diagnostics_workflow/src/diagnostics_workflow.c b/src/diagnostics_component/diagnostics_workflow/src/diagnostics_workflow.c index 93c5c4735..243793ebb 100644 --- a/src/diagnostics_component/diagnostics_workflow/src/diagnostics_workflow.c +++ b/src/diagnostics_component/diagnostics_workflow/src/diagnostics_workflow.c @@ -10,8 +10,8 @@ #include #include -#include #include +#include #include #include #include @@ -20,83 +20,6 @@ #include #include -/** - * Expected Diagnostics Config file format: - * { - "logComponents":[ - { - "componentName":"DU", - "logPath":"/var/logs/adu/" - }, - { - "componentName":"DO", - "logPath":"/var/cache/do/" - }, - ... - ], - "maxKilobytesToUploadPerLogPath":5 - } - */ - -/** - * @brief Fieldname for the array of log components in the Diagnostics JSON Config File - */ -#define DIAGNOSTICS_CONFIG_FILE_LOG_COMPONENTS_FIELDNAME "logComponents" - -/** - * @brief Fieldname for the name of the component having its logs collected in the Diagnostics JSON Config File - */ -#define DIAGNOSTICS_CONFIG_FILE_COMPONENT_FIELDNAME_COMPONENTNAME "componentName" - -/** - * @brief Fieldname for the location of logs on the device for a component in the Diagnostics JSON Config File - */ -#define DIAGNOSTICS_CONFIG_FILE_COMPONENT_FIELDNAME_LOGPATH "logPath" - -/** - * @brief Fieldname for the maximum number of bytes to upload per diagnostics workflow - */ -#define DIAGNOSTICS_CONFIG_FILE_FIELDNAME_MAXKILOBYTESTOUPLOADPERLOGPATH "maxKilobytesToUploadPerLogPath" - -/** - * @brief Maximum number of kilobytes allowed to be uploaded per log path - */ -#define DIAGNOSTICS_MAX_KILOBYTES_PER_LOG_PATH 100000 /* 1000 KB or 100 MB */ - -/** - * @brief Returns the DiagnosticsLogComponent object @p index within @p workflowData - * @param workflowData the DiagnosticsWorkflowData vector from which to get the DiagnosticsComponent - * @param index the index from which to retrieve the value - * @returns NULL if index is out of range; the DiagnosticsComponent otherwise - */ -const DiagnosticsLogComponent* -DiagnosticsWorkflow_GetLogComponentElem(const DiagnosticsWorkflowData* workflowData, unsigned int index) -{ - if (workflowData == NULL || index >= VECTOR_size(workflowData->components)) - { - return NULL; - } - - return (const DiagnosticsLogComponent*)VECTOR_element(workflowData->components, index); -} -/** - * @brief Uninitializes a logComponent pointer - * @param logComponent pointer to the logComponent whose members will be freed - */ -void DiagnosticsWorkflow_LogComponentUninit(DiagnosticsLogComponent* logComponent) -{ - if (logComponent == NULL) - { - return; - } - - STRING_delete(logComponent->componentName); - logComponent->componentName = NULL; - - STRING_delete(logComponent->logPath); - logComponent->logPath = NULL; -} - /** * @brief Sets the memory in @p memory which points to the storage location in @p sasCredential to 0 before calling STRING_delete() on sasCredential * @param sasCredential credential to be deleted @@ -155,222 +78,6 @@ STRING_HANDLE DiagnosticsComponent_CreateSasCredential(const char* sasCredential return handle; } -/** - * @brief initializes a log component using a JSON_Object representing the component in the config file - * @param component the componentObj to intitialize - * @param componentObj JSON Object with the associated components for the log component - * @returns true on success; false on failure - */ -_Bool DiagnosticsComponent_LogComponentInitFromObj(DiagnosticsLogComponent* component, JSON_Object* componentObj) -{ - if (componentObj == NULL || component == NULL) - { - return false; - } - - memset(component, 0, sizeof(*component)); - - _Bool succeeded = false; - char* componentName = NULL; - char* logPath = NULL; - - if (!ADUC_JSON_GetStringFieldFromObj( - componentObj, DIAGNOSTICS_CONFIG_FILE_COMPONENT_FIELDNAME_COMPONENTNAME, &componentName)) - { - goto done; - } - - if (!ADUC_JSON_GetStringFieldFromObj(componentObj, DIAGNOSTICS_CONFIG_FILE_COMPONENT_FIELDNAME_LOGPATH, &logPath)) - { - goto done; - } - - component->componentName = STRING_construct(componentName); - - if (component->componentName == NULL) - { - goto done; - } - - component->logPath = STRING_construct(logPath); - - if (component->logPath == NULL) - { - goto done; - } - - succeeded = true; - -done: - - if (!succeeded) - { - DiagnosticsWorkflow_LogComponentUninit(component); - } - - free(componentName); - free(logPath); - - return succeeded; -} - -/** - * @brief Initializes @p workflowData with the contents of @p filePath - * @param workflowData the workflowData structure to be intialized. - * @param filePath path to the diagnostics configuration file - * @returns true on successful configuration, false on failure - */ -_Bool DiagnosticsWorkflow_InitFromFile(DiagnosticsWorkflowData* workflowData, const char* filePath) -{ - JSON_Value* fileJsonValue = NULL; - - if (workflowData == NULL || filePath == NULL) - { - return false; - } - - fileJsonValue = json_parse_file(filePath); - - if (fileJsonValue == NULL) - { - return false; - } - - _Bool succeeded = DiagnosticsWorkflow_InitFromJSON(workflowData, fileJsonValue); - - json_value_free(fileJsonValue); - - return succeeded; -} - -/** - * @brief Initializes @p workflowData with the contents of @p fileJsonValue - * @param workflowData the structure to be initialized - * @param fileJsonValue a JSON_Value representation of a diagnostics-config.json file - * @returns true on successful configuration; false on failures - */ -_Bool DiagnosticsWorkflow_InitFromJSON(DiagnosticsWorkflowData* workflowData, JSON_Value* fileJsonValue) -{ - if (workflowData == NULL || fileJsonValue == NULL) - { - return false; - } - - _Bool succeeded = false; - - memset(workflowData, 0, sizeof(*workflowData)); - - JSON_Object* fileJsonObj = json_value_get_object(fileJsonValue); - - if (fileJsonObj == NULL) - { - goto done; - } - - unsigned int maxKilobytesToUploadPerLogPath = 0; - - if (!ADUC_JSON_GetUnsignedIntegerField( - fileJsonValue, - DIAGNOSTICS_CONFIG_FILE_FIELDNAME_MAXKILOBYTESTOUPLOADPERLOGPATH, - &maxKilobytesToUploadPerLogPath)) - { - Log_Warn("DiagnosticsWorkflow_Init failed to retrieve maxKilobytesPerLogPath from diagnostics config"); - goto done; - } - - if (maxKilobytesToUploadPerLogPath < 1) - { - Log_Warn( - "DiagnosticsWorkflow_Init maxKilobytesPerLogPath config set to invalid value: %i", - maxKilobytesToUploadPerLogPath); - goto done; - } - - if (maxKilobytesToUploadPerLogPath > DIAGNOSTICS_MAX_KILOBYTES_PER_LOG_PATH) - { - maxKilobytesToUploadPerLogPath = DIAGNOSTICS_MAX_KILOBYTES_PER_LOG_PATH; - } - - workflowData->maxBytesToUploadPerLogPath = maxKilobytesToUploadPerLogPath * 1024; - - JSON_Array* componentArray = json_object_get_array(fileJsonObj, DIAGNOSTICS_CONFIG_FILE_LOG_COMPONENTS_FIELDNAME); - - if (componentArray == NULL) - { - goto done; - } - - const size_t numComponents = json_array_get_count(componentArray); - - if (numComponents == 0) - { - goto done; - } - - workflowData->components = VECTOR_create(sizeof(DiagnosticsLogComponent)); - - if (workflowData->components == NULL) - { - goto done; - } - - for (size_t i = 0; i < numComponents; ++i) - { - JSON_Object* componentObj = json_array_get_object(componentArray, i); - - DiagnosticsLogComponent logComponent = {}; - - if (!DiagnosticsComponent_LogComponentInitFromObj(&logComponent, componentObj)) - { - goto done; - } - - if (VECTOR_push_back(workflowData->components, &logComponent, 1) != 0) - { - goto done; - } - } - - succeeded = true; -done: - - if (!succeeded) - { - Log_Error("DiagnosticsWorkflow_Init failed"); - DiagnosticsWorkflow_UnInit(workflowData); - } - - return succeeded; -} - -/** - * @brief UnInitializes @p workflowData's data members - * @param workflowData the workflowData structure to be unintialized. - */ -void DiagnosticsWorkflow_UnInit(DiagnosticsWorkflowData* workflowData) -{ - if (workflowData == NULL) - { - return; - } - - if (workflowData->components != NULL) - { - const size_t numComponents = VECTOR_size(workflowData->components); - - for (size_t i = 0; i < numComponents; i++) - { - DiagnosticsLogComponent* logComponent = - (DiagnosticsLogComponent*)VECTOR_element(workflowData->components, i); - - DiagnosticsWorkflow_LogComponentUninit(logComponent); - } - - VECTOR_destroy(workflowData->components); - } - - memset(workflowData, 0, sizeof(*workflowData)); -} /** * @brief Discovers the logs described by @p logComponent and stores them in @p fileNames @@ -605,7 +312,7 @@ void DiagnosticsWorkflow_DiscoverAndUploadLogs(const DiagnosticsWorkflowData* wo // for (size_t i = 0; i < numComponents; ++i) { - const DiagnosticsLogComponent* logComponent = DiagnosticsWorkflow_GetLogComponentElem(workflowData, i); + const DiagnosticsLogComponent* logComponent = DiagnosticsConfigUtils_GetLogComponentElem(workflowData, i); if (logComponent == NULL || logComponent->componentName == NULL || logComponent->logPath == NULL) { @@ -633,7 +340,7 @@ void DiagnosticsWorkflow_DiscoverAndUploadLogs(const DiagnosticsWorkflowData* wo // for (size_t i = 0; i < numComponents; ++i) { - const DiagnosticsLogComponent* logComponent = DiagnosticsWorkflow_GetLogComponentElem(workflowData, i); + const DiagnosticsLogComponent* logComponent = DiagnosticsConfigUtils_GetLogComponentElem(workflowData, i); if (logComponent == NULL || logComponent->componentName == NULL || logComponent->logPath == NULL) { diff --git a/src/diagnostics_component/utils/CMakeLists.txt b/src/diagnostics_component/utils/CMakeLists.txt index 9bda7de10..d6fbdd4b4 100644 --- a/src/diagnostics_component/utils/CMakeLists.txt +++ b/src/diagnostics_component/utils/CMakeLists.txt @@ -1,4 +1,5 @@ cmake_minimum_required (VERSION 3.5) +add_subdirectory (config_utils) add_subdirectory (file_info_utils) add_subdirectory (operation_id_utils) diff --git a/src/diagnostics_component/utils/config_utils/CMakeLists.txt b/src/diagnostics_component/utils/config_utils/CMakeLists.txt new file mode 100644 index 000000000..51c0f7975 --- /dev/null +++ b/src/diagnostics_component/utils/config_utils/CMakeLists.txt @@ -0,0 +1,19 @@ +set (target_name diagnostics_config_utils) + +add_library (${target_name} STATIC) +add_library (diagnostic_utils::${target_name} ALIAS ${target_name}) + +target_include_directories (${target_name} PUBLIC inc) +target_sources (${target_name} PRIVATE src/diagnostics_config_utils.c) + +find_package (azure_c_shared_utility REQUIRED) +find_package (Parson REQUIRED) + +target_link_libraries ( + ${target_name} + PUBLIC aduc::c_utils aziotsharedutil Parson::parson + PRIVATE aduc::parson_json_utils aduc::logging) + +if (ADUC_BUILD_UNIT_TESTS) + add_subdirectory (tests) +endif () diff --git a/src/diagnostics_component/utils/config_utils/inc/diagnostics_config_utils.h b/src/diagnostics_component/utils/config_utils/inc/diagnostics_config_utils.h new file mode 100644 index 000000000..680c2d4b3 --- /dev/null +++ b/src/diagnostics_component/utils/config_utils/inc/diagnostics_config_utils.h @@ -0,0 +1,73 @@ +#ifndef DIAGNOSTICS_CONFIG_UTILS_H +#define DIAGNOSTICS_CONFIG_UTILS_H + +#include +#include +#include +#include + +EXTERN_C_BEGIN + +/** + * @brief Forward declaration of JSON_Value used in Parson + */ +typedef struct json_value_t JSON_Value; + +/** + * @brief Describes each component for which to collect logs. + */ +typedef struct tagDiagnosticLogComponent +{ + STRING_HANDLE componentName; //!< Name of the component for which to collect logs + STRING_HANDLE logPath; //!< Absolute path to the directory where the logs are stored +} DiagnosticsLogComponent; + +/** + * @brief Data structure representing the data needed for the Diagnostics Workflow + */ +typedef struct tagDiagnosticWorkflowData +{ + VECTOR_HANDLE components; //!< Vector of DiagnosticLogComponent pointers for which to collect logs + unsigned int maxBytesToUploadPerLogPath; //!< The maximum number of bytes to upload per log file path +} DiagnosticsWorkflowData; + +/** + * @brief Initializes @p workflowData with the contents of @p fileJsonValue + * @param workflowData the structure to be initialized + * @param fileJsonValue a JSON_Value representation of a diagnostics-config.json file + * @returns true on successful configuration; false on failures + */ +_Bool DiagnosticsConfigUtils_InitFromJSON(DiagnosticsWorkflowData* workflowData, JSON_Value* fileJsonValue); + +/** + * @brief Initializes @p workflowData with the contents of @p filePath + * @param workflowData the workflowData structure to be intialized. + * @param filePath path to the diagnostics configuration file + * @returns true on successful configuration, false on failure + */ +_Bool DiagnosticsConfigUtils_InitFromFile(DiagnosticsWorkflowData* workflowData, const char* filePath); + +/** + * @brief UnInitializes @p workflowData's data members + * @param workflowData the workflowData structure to be unintialized. + */ +void DiagnosticsConfigUtils_UnInit(DiagnosticsWorkflowData* workflowData); + +/** + * @brief Returns the DiagnosticsLogComponent object @p index within @p workflowData + * @param workflowData the DiagnosticsWorkflowData vector from which to get the DiagnosticsComponent + * @param index the index from which to retrieve the value + * @returns NULL if index is out of range; the DiagnosticsComponent otherwise + */ +const DiagnosticsLogComponent* +DiagnosticsConfigUtils_GetLogComponentElem(const DiagnosticsWorkflowData* workflowData, unsigned int index); + +/** + * @brief Uninitializes a logComponent pointer + * @param logComponent pointer to the logComponent whose members will be freed + */ +void DiagnosticsConfigUtils_LogComponentUninit(DiagnosticsLogComponent* logComponent); + +EXTERN_C_END + +#endif // #define DIAGNOSTICS_CONFIG_UTILS_H diff --git a/src/diagnostics_component/utils/config_utils/src/diagnostics_config_utils.c b/src/diagnostics_component/utils/config_utils/src/diagnostics_config_utils.c new file mode 100644 index 000000000..a9b71dd4b --- /dev/null +++ b/src/diagnostics_component/utils/config_utils/src/diagnostics_config_utils.c @@ -0,0 +1,308 @@ +/** + * @file diagnostics_config_utils.c + * @brief Implementation for functions handling the Diagnostic config + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include "diagnostics_config_utils.h" +#include +#include // for ADUC_JSON_GetUnsignedIntegerField +#include // for free + +/** + * @brief Fieldname for the array of log components in the Diagnostics JSON Config File + */ +#define DIAGNOSTICS_CONFIG_FILE_LOG_COMPONENTS_FIELDNAME "logComponents" + +/** + * @brief Fieldname for the name of the component having its logs collected in the Diagnostics JSON Config File + */ +#define DIAGNOSTICS_CONFIG_FILE_COMPONENT_FIELDNAME_COMPONENTNAME "componentName" + +/** + * @brief Fieldname for the location of logs on the device for a component in the Diagnostics JSON Config File + */ +#define DIAGNOSTICS_CONFIG_FILE_COMPONENT_FIELDNAME_LOGPATH "logPath" + +/** + * @brief Fieldname for the maximum number of bytes to upload per diagnostics workflow + */ +#define DIAGNOSTICS_CONFIG_FILE_FIELDNAME_MAXKILOBYTESTOUPLOADPERLOGPATH "maxKilobytesToUploadPerLogPath" + +/** + * @brief Maximum number of kilobytes allowed to be uploaded per log path + */ +#define DIAGNOSTICS_MAX_KILOBYTES_PER_LOG_PATH 100000 /* 1000 KB or 100 MB */ + +/** + * Expected Diagnostics Config file format: + * { + "logComponents":[ + { + "componentName":"DU", + "logPath":"/var/logs/adu/" + }, + { + "componentName":"DO", + "logPath":"/var/cache/do/" + }, + ... + ], + "maxKilobytesToUploadPerLogPath":5 + } + */ + +/** + * @brief initializes a log component using a JSON_Object representing the component in the config file + * @param component the componentObj to intitialize + * @param componentObj JSON Object with the associated components for the log component + * @returns true on success; false on failure + */ +static _Bool +DiagnosticsConfigUtils_LogComponentInitFromObj(DiagnosticsLogComponent* component, JSON_Object* componentObj) +{ + if (componentObj == NULL || component == NULL) + { + return false; + } + + memset(component, 0, sizeof(*component)); + + _Bool succeeded = false; + char* componentName = NULL; + char* logPath = NULL; + + if (!ADUC_JSON_GetStringFieldFromObj( + componentObj, DIAGNOSTICS_CONFIG_FILE_COMPONENT_FIELDNAME_COMPONENTNAME, &componentName)) + { + goto done; + } + + if (!ADUC_JSON_GetStringFieldFromObj(componentObj, DIAGNOSTICS_CONFIG_FILE_COMPONENT_FIELDNAME_LOGPATH, &logPath)) + { + goto done; + } + + component->componentName = STRING_construct(componentName); + + if (component->componentName == NULL) + { + goto done; + } + + component->logPath = STRING_construct(logPath); + + if (component->logPath == NULL) + { + goto done; + } + + succeeded = true; + +done: + + if (!succeeded) + { + DiagnosticsConfigUtils_LogComponentUninit(component); + } + + free(componentName); + free(logPath); + + return succeeded; +} + +/** + * @brief Initializes @p workflowData with the contents of @p fileJsonValue + * @param workflowData the structure to be initialized + * @param fileJsonValue a JSON_Value representation of a diagnostics-config.json file + * @returns true on successful configuration; false on failures + */ +_Bool DiagnosticsConfigUtils_InitFromJSON(DiagnosticsWorkflowData* workflowData, JSON_Value* fileJsonValue) +{ + if (workflowData == NULL || fileJsonValue == NULL) + { + return false; + } + + _Bool succeeded = false; + + memset(workflowData, 0, sizeof(*workflowData)); + + JSON_Object* fileJsonObj = json_value_get_object(fileJsonValue); + + if (fileJsonObj == NULL) + { + goto done; + } + + unsigned int maxKilobytesToUploadPerLogPath = 0; + + if (!ADUC_JSON_GetUnsignedIntegerField( + fileJsonValue, + DIAGNOSTICS_CONFIG_FILE_FIELDNAME_MAXKILOBYTESTOUPLOADPERLOGPATH, + &maxKilobytesToUploadPerLogPath)) + { + Log_Warn("DiagnosticsConfigUtils_Init failed to retrieve maxKilobytesPerLogPath from diagnostics config"); + goto done; + } + + if (maxKilobytesToUploadPerLogPath < 1) + { + Log_Warn( + "DiagnosticsConfigUtils_Init maxKilobytesPerLogPath config set to invalid value: %i", + maxKilobytesToUploadPerLogPath); + goto done; + } + + if (maxKilobytesToUploadPerLogPath > DIAGNOSTICS_MAX_KILOBYTES_PER_LOG_PATH) + { + maxKilobytesToUploadPerLogPath = DIAGNOSTICS_MAX_KILOBYTES_PER_LOG_PATH; + } + + workflowData->maxBytesToUploadPerLogPath = maxKilobytesToUploadPerLogPath * 1024; + + JSON_Array* componentArray = json_object_get_array(fileJsonObj, DIAGNOSTICS_CONFIG_FILE_LOG_COMPONENTS_FIELDNAME); + + if (componentArray == NULL) + { + goto done; + } + + const size_t numComponents = json_array_get_count(componentArray); + + if (numComponents == 0) + { + goto done; + } + + workflowData->components = VECTOR_create(sizeof(DiagnosticsLogComponent)); + + if (workflowData->components == NULL) + { + goto done; + } + + for (size_t i = 0; i < numComponents; ++i) + { + JSON_Object* componentObj = json_array_get_object(componentArray, i); + + DiagnosticsLogComponent logComponent = {}; + + if (!DiagnosticsConfigUtils_LogComponentInitFromObj(&logComponent, componentObj)) + { + goto done; + } + + if (VECTOR_push_back(workflowData->components, &logComponent, 1) != 0) + { + goto done; + } + } + + succeeded = true; +done: + + if (!succeeded) + { + Log_Error("DiagnosticsConfigUtils_Init failed"); + DiagnosticsConfigUtils_UnInit(workflowData); + } + + return succeeded; +} + +/** + * @brief Initializes @p workflowData with the contents of @p filePath + * @param workflowData the workflowData structure to be intialized. + * @param filePath path to the diagnostics configuration file + * @returns true on successful configuration, false on failure + */ +_Bool DiagnosticsConfigUtils_InitFromFile(DiagnosticsWorkflowData* workflowData, const char* filePath) +{ + JSON_Value* fileJsonValue = NULL; + + if (workflowData == NULL || filePath == NULL) + { + return false; + } + + fileJsonValue = json_parse_file(filePath); + + if (fileJsonValue == NULL) + { + return false; + } + + _Bool succeeded = DiagnosticsConfigUtils_InitFromJSON(workflowData, fileJsonValue); + + json_value_free(fileJsonValue); + + return succeeded; +} + +/** + * @brief Returns the DiagnosticsLogComponent object @p index within @p workflowData + * @param workflowData the DiagnosticsWorkflowData vector from which to get the DiagnosticsComponent + * @param index the index from which to retrieve the value + * @returns NULL if index is out of range; the DiagnosticsComponent otherwise + */ +const DiagnosticsLogComponent* +DiagnosticsConfigUtils_GetLogComponentElem(const DiagnosticsWorkflowData* workflowData, unsigned int index) +{ + if (workflowData == NULL || index >= VECTOR_size(workflowData->components)) + { + return NULL; + } + + return (const DiagnosticsLogComponent*)VECTOR_element(workflowData->components, index); +} + +/** + * @brief Uninitializes a logComponent pointer + * @param logComponent pointer to the logComponent whose members will be freed + */ +void DiagnosticsConfigUtils_LogComponentUninit(DiagnosticsLogComponent* logComponent) +{ + if (logComponent == NULL) + { + return; + } + + STRING_delete(logComponent->componentName); + logComponent->componentName = NULL; + + STRING_delete(logComponent->logPath); + logComponent->logPath = NULL; +} + +/** + * @brief UnInitializes @p workflowData's data members + * @param workflowData the workflowData structure to be unintialized. + */ +void DiagnosticsConfigUtils_UnInit(DiagnosticsWorkflowData* workflowData) +{ + if (workflowData == NULL) + { + return; + } + + if (workflowData->components != NULL) + { + const size_t numComponents = VECTOR_size(workflowData->components); + + for (size_t i = 0; i < numComponents; i++) + { + DiagnosticsLogComponent* logComponent = + (DiagnosticsLogComponent*)VECTOR_element(workflowData->components, i); + + DiagnosticsConfigUtils_LogComponentUninit(logComponent); + } + + VECTOR_destroy(workflowData->components); + } + + memset(workflowData, 0, sizeof(*workflowData)); +} diff --git a/src/diagnostics_component/diagnostics_workflow/tests/CMakeLists.txt b/src/diagnostics_component/utils/config_utils/tests/CMakeLists.txt similarity index 61% rename from src/diagnostics_component/diagnostics_workflow/tests/CMakeLists.txt rename to src/diagnostics_component/utils/config_utils/tests/CMakeLists.txt index 32617d177..3572ceb57 100644 --- a/src/diagnostics_component/diagnostics_workflow/tests/CMakeLists.txt +++ b/src/diagnostics_component/utils/config_utils/tests/CMakeLists.txt @@ -1,13 +1,11 @@ -cmake_minimum_required (VERSION 3.5) - -project (diagnostics_workflow_ut) +project (diagnostics_config_utils_ut) include (agentRules) compileasc99 () disablertti () -set (sources main.cpp diagnostics_workflow_ut.cpp) +set (sources main.cpp diagnostics_config_utils_ut.cpp) find_package (azure_c_shared_utility REQUIRED) find_package (Catch2 REQUIRED) @@ -18,10 +16,10 @@ add_executable (${PROJECT_NAME} ${sources}) target_link_libraries ( ${PROJECT_NAME} - PRIVATE Catch2::Catch2 - diagnostics_component::diagnostics_workflow - Parson::parson - aziotsharedutil) + PRIVATE aziotsharedutil + diagnostic_utils::diagnostics_config_utils + Catch2::Catch2 + Parson::parson) include (CTest) include (Catch) diff --git a/src/diagnostics_component/diagnostics_workflow/tests/diagnostics_workflow_ut.cpp b/src/diagnostics_component/utils/config_utils/tests/diagnostics_config_utils_ut.cpp similarity index 68% rename from src/diagnostics_component/diagnostics_workflow/tests/diagnostics_workflow_ut.cpp rename to src/diagnostics_component/utils/config_utils/tests/diagnostics_config_utils_ut.cpp index 3f166469a..9609b3280 100644 --- a/src/diagnostics_component/diagnostics_workflow/tests/diagnostics_workflow_ut.cpp +++ b/src/diagnostics_component/utils/config_utils/tests/diagnostics_config_utils_ut.cpp @@ -1,12 +1,12 @@ /** - * @file diagnostics_workflow_ut.cpp - * @brief Unit Tests for the Diagnostic Workflow + * @file diagnostics_config_utils_ut.cpp + * @brief Unit Tests for the Diagnostic Config utils. * * @copyright Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ -#include "diagnostics_workflow.h" +#include "diagnostics_config_utils.h" #include #include @@ -16,13 +16,13 @@ #include #include -class DiagnosticWorkflowUnitTestHelper +class DiagnosticConfigUtilsUnitTestHelper { public: JSON_Value* jsonValue = nullptr; DiagnosticsWorkflowData workflowData = {}; - explicit DiagnosticWorkflowUnitTestHelper(const char* jsonString) + explicit DiagnosticConfigUtilsUnitTestHelper(const char* jsonString) { jsonValue = json_parse_string(jsonString); @@ -31,21 +31,21 @@ class DiagnosticWorkflowUnitTestHelper throw std::runtime_error("json could not be parsed"); } } - DiagnosticWorkflowUnitTestHelper(const DiagnosticWorkflowUnitTestHelper&) = delete; - DiagnosticWorkflowUnitTestHelper(DiagnosticWorkflowUnitTestHelper&&) = delete; - DiagnosticWorkflowUnitTestHelper& operator=(const DiagnosticWorkflowUnitTestHelper&) = delete; - DiagnosticWorkflowUnitTestHelper& operator=(DiagnosticWorkflowUnitTestHelper&&) = delete; + DiagnosticConfigUtilsUnitTestHelper(const DiagnosticConfigUtilsUnitTestHelper&) = delete; + DiagnosticConfigUtilsUnitTestHelper(DiagnosticConfigUtilsUnitTestHelper&&) = delete; + DiagnosticConfigUtilsUnitTestHelper& operator=(const DiagnosticConfigUtilsUnitTestHelper&) = delete; + DiagnosticConfigUtilsUnitTestHelper& operator=(DiagnosticConfigUtilsUnitTestHelper&&) = delete; - ~DiagnosticWorkflowUnitTestHelper() + ~DiagnosticConfigUtilsUnitTestHelper() { json_value_free(jsonValue); - DiagnosticsWorkflow_UnInit(&workflowData); + DiagnosticsConfigUtils_UnInit(&workflowData); } }; -TEST_CASE("DiagnosticsWorkflow_Init") +TEST_CASE("DiagnosticsConfigUtils_Init") { - SECTION("DiagnosticsWorkflow_Init- Positive Test Case") + SECTION("DiagnosticsConfigUtils_Init- Positive Test Case") { unsigned int maxKilobytesToUploadPerLogPath = 5; @@ -66,9 +66,9 @@ TEST_CASE("DiagnosticsWorkflow_Init") R"("maxKilobytesToUploadPerLogPath":)" << maxKilobytesToUploadPerLogPath << R"(})"; // clang-format on - DiagnosticWorkflowUnitTestHelper testHelper(goodConfigJsonStream.str().c_str()); + DiagnosticConfigUtilsUnitTestHelper testHelper(goodConfigJsonStream.str().c_str()); - CHECK(DiagnosticsWorkflow_InitFromJSON(&testHelper.workflowData, testHelper.jsonValue)); + CHECK(DiagnosticsConfigUtils_InitFromJSON(&testHelper.workflowData, testHelper.jsonValue)); CHECK_FALSE(testHelper.workflowData.components == nullptr); const size_t logComponentLength = VECTOR_size(testHelper.workflowData.components); @@ -76,14 +76,14 @@ TEST_CASE("DiagnosticsWorkflow_Init") CHECK(logComponentLength == 2); const DiagnosticsLogComponent* firstLogComponent = - DiagnosticsWorkflow_GetLogComponentElem(&testHelper.workflowData, 0); + DiagnosticsConfigUtils_GetLogComponentElem(&testHelper.workflowData, 0); CHECK(firstLogComponent != nullptr); CHECK(strcmp(STRING_c_str(firstLogComponent->componentName), "DU") == 0); CHECK(strcmp(STRING_c_str(firstLogComponent->logPath), ADUC_LOG_FOLDER) == 0); const DiagnosticsLogComponent* secondLogComponent = - DiagnosticsWorkflow_GetLogComponentElem(&testHelper.workflowData, 1); + DiagnosticsConfigUtils_GetLogComponentElem(&testHelper.workflowData, 1); CHECK(secondLogComponent != nullptr); CHECK(strcmp(STRING_c_str(secondLogComponent->componentName), "DO") == 0); @@ -92,7 +92,7 @@ TEST_CASE("DiagnosticsWorkflow_Init") CHECK(testHelper.workflowData.maxBytesToUploadPerLogPath == (maxKilobytesToUploadPerLogPath * 1024)); } - SECTION("DiagnosticsWorkflow_Init- No logComponents") + SECTION("DiagnosticsConfigUtils_Init- No logComponents") { // clang-format off std::string noLogComponents = R"({)" @@ -100,14 +100,14 @@ TEST_CASE("DiagnosticsWorkflow_Init") R"(})"; // clang-format on - DiagnosticWorkflowUnitTestHelper testHelper(noLogComponents.c_str()); + DiagnosticConfigUtilsUnitTestHelper testHelper(noLogComponents.c_str()); - CHECK_FALSE(DiagnosticsWorkflow_InitFromJSON(&testHelper.workflowData, testHelper.jsonValue)); + CHECK_FALSE(DiagnosticsConfigUtils_InitFromJSON(&testHelper.workflowData, testHelper.jsonValue)); CHECK(testHelper.workflowData.components == nullptr); } - SECTION("DiagnosticsWorkflow_Init- No Upload Limit") + SECTION("DiagnosticsConfigUtils_Init- No Upload Limit") { // clang-format off std::string noUploadLimit = R"({)" @@ -124,9 +124,9 @@ TEST_CASE("DiagnosticsWorkflow_Init") R"(})"; // clang-format on - DiagnosticWorkflowUnitTestHelper testHelper(noUploadLimit.c_str()); + DiagnosticConfigUtilsUnitTestHelper testHelper(noUploadLimit.c_str()); - CHECK_FALSE(DiagnosticsWorkflow_InitFromJSON(&testHelper.workflowData, testHelper.jsonValue)); + CHECK_FALSE(DiagnosticsConfigUtils_InitFromJSON(&testHelper.workflowData, testHelper.jsonValue)); CHECK(testHelper.workflowData.components == nullptr); } diff --git a/src/diagnostics_component/utils/config_utils/tests/main.cpp b/src/diagnostics_component/utils/config_utils/tests/main.cpp new file mode 100644 index 000000000..4ceb06a5d --- /dev/null +++ b/src/diagnostics_component/utils/config_utils/tests/main.cpp @@ -0,0 +1,9 @@ +/** + * @file main.cpp + * @brief diagnostics_config_utils_ut tests main entry point. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ +#define CATCH_CONFIG_MAIN +#include diff --git a/src/diagnostics_component/utils/file_info_utils/src/file_info_utils.c b/src/diagnostics_component/utils/file_info_utils/src/file_info_utils.c index bd85fe89d..7f42fbfc8 100644 --- a/src/diagnostics_component/utils/file_info_utils/src/file_info_utils.c +++ b/src/diagnostics_component/utils/file_info_utils/src/file_info_utils.c @@ -229,8 +229,8 @@ _Bool FileInfoUtils_GetNewestFilesInDirUnderSize( // No more files to add break; } - ++fileIndex; currentFileMaxCount += discoveredFiles[fileIndex].fileSize; + ++fileIndex; } // Only log file found is larger than our maxFileSize diff --git a/src/extensions/CMakeLists.txt b/src/extensions/CMakeLists.txt index b677db07e..99c2b2f37 100644 --- a/src/extensions/CMakeLists.txt +++ b/src/extensions/CMakeLists.txt @@ -1,5 +1,7 @@ -set (ADUC_EXTENSION_INCLUDES ${CMAKE_CURRENT_SOURCE_DIR}/inc ${CMAKE_CURRENT_SOURCE_DIR}/inc) - +add_subdirectory (content_downloaders) +add_subdirectory (component_enumerators/examples/contoso_component_enumerator) +add_subdirectory (download_handlers) add_subdirectory (extension_manager) -add_subdirectory (content-downloaders) -add_subdirectory (component-enumerators/examples/contoso-component-enumerator) +add_subdirectory (step_handlers) +add_subdirectory (update_manifest_handlers) +add_subdirectory (shared_lib) diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/CMakeLists.txt b/src/extensions/component-enumerators/examples/contoso-component-enumerator/CMakeLists.txt deleted file mode 100644 index cbbb6b0a2..000000000 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -project (contoso-component-enumerator) - -include (agentRules) - -compileasc99 () - -add_library ( - ${PROJECT_NAME} SHARED - contoso-component-enumerator.cpp -) - -find_package (Parson REQUIRED) - -add_library (aduc::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) - -target_include_directories (${PROJECT_NAME} PUBLIC ${ADUC_EXTENSION_INCLUDES} ${ADUC_EXPORT_INCLUDES}) - -target_link_libraries ( - ${PROJECT_NAME} - PUBLIC Parson::parson) - -install (TARGETS ${PROJECT_NAME} LIBRARY DESTINATION ${ADUC_EXTENSIONS_INSTALL_FOLDER}) diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/README.md b/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/README.md deleted file mode 100644 index feffb7544..000000000 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# Device Update for Azure IoT Hub tutorial using the Device Update agent to do Proxy Updates - -## Generate Example Updates - -Open PowerShell terminal, go [[project-root]/tools/AduCmdlets](../../../../../../tools/AduCmdlets/) directory, then import AduUpdate.psm1 module: - -```powershell -Import-Module ./AduUpdate.psm1 -``` - -Next, go to [sample-updates](./sample-updates/) directory, then run following commands to generate **all** example updates: - -> NOTE | You can choose to generate only updates you want to try. - -```powershell -./CreateSampleMSOEUpdate-1.x.ps1 -./CreateSampleMSOEUpdate-2.x.ps1 -./CreateSampleMSOEUpdate-3.x.ps1 -./CreateSampleMSOEUpdate-4.x.ps1 -./CreateSampleMSOEUpdate-5.x.ps1 -./CreateSampleMSOEUpdate-6.x.ps1 -./CreateSampleMSOEUpdate-7.x.ps1 -./CreateSampleMSOEUpdate-8.x.ps1 -./CreateSampleMSOEUpdate-10.x.ps1 -``` - -## Setup Test Device - -### Prerequisites - -- Ubuntu 18.04 LTS Server - -### Install the Device Update Agent and Dependencies - -- Register packages.microsoft.com in APT package repository - -```sh -curl https://packages.microsoft.com/config/ubuntu/18.04/multiarch/prod.list > ~/microsoft-prod.list - -sudo cp ~/microsoft-prod.list /etc/apt/sources.list.d/ - -curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > ~/microsoft.gpg - -sudo cp ~/microsoft.gpg /etc/apt/trusted.gpg.d/ - -sudo apt-get update -``` - -- Install test **deviceupdate-agent-[version].deb** package on the test device. -e.g. - - ```sh - sudo apt-get install ./deviceupdate-agent-0.8.0-public-preview-refresh.deb - ``` - -Note: this will automatically install the delivery-optimization-agent package from packages.microsoft.com - -- Specify a connection string in /etc/adu/du-config.json - ->NOTE | The current example only compatible with device with "contoso" manufacturer, and "virtual-vacuum-v1" model. These values must be correctly set inside `agents` entry in du-config.json - -- Ensure that /etc/adu/du-diagnostics-config.json contain correct settings. - e.g. - -```sh -{ - "logComponents":[ - { - "componentName":"adu", - "logPath":"/var/log/adu/" - }, - { - "componentName":"do", - "logPath":"/var/log/deliveryoptimization-agent/" - } - ], - "maxKilobytesToUploadPerLogPath":50 -} -``` - -- Restart `adu-agent` service - -```sh -sudo systemctl restart adu-agent -``` - -### Setup Mock Components - -For testing and demonstration purposes, we'll be creating following mock components on the device: - -- 3 motors -- 2 cameras -- hostfs -- rootfs - -**IMPORTANT** -This components configuration depends on the implementation of an example Component Enumerator extension called libcontoso-component-enumerator.so, which required a mock component inventory data file `/usr/local/contoso-devices/components-inventory.json` - -> Tip: you can copy [demo](../demo) folder to your home directory on the test VM and run `~/demo/tools/reset-demo-components.sh` to copy required files to the right locations. - -#### Add /usr/local/contoso-devices/components-inventory.json - -- Copy [components-inventory.json](./demo-devices/contoso-devices/components-inventory.json) to **/usr/local/contoso-devices** folder - -#### Register Contoso Components Enumerator extension - -- Copy [libcontoso-component-enumerator.so](./sample-components-enumerator/libcontoso-component-enumerator.so) to /var/lib/adu/extensions/sources folder -- Register the extension - -```sh -sudo /usr/bin/AducIotAgent -E /var/lib/adu/extensions/sources/libcontoso-component-enumerator.so -``` - -**Congratulations!** Your VM should now support Proxy Updates! - -## Next Step - -Try [Tutorial #1 - Install 'tree' Debian package on the Contoso Virtual Vacuum devices, and update their motors' firmware to version 1.1](./tutorial1.md) diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-6.x.ps1 b/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-6.x.ps1 deleted file mode 100644 index 5d28ab892..000000000 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-6.x.ps1 +++ /dev/null @@ -1,191 +0,0 @@ -# -# Device Update for IoT Hub -# Sample PowerShell script for creating import manifests and payloads files for testing purposes. -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# - -#Requires -Version 5.0 - -<# - .SYNOPSIS - Create a sample updates for testing purposes. - - .EXAMPLE - PS > CreateSampleMSOEUpdate-6.x.ps1 -UpdateProvider "Contoso" ` - -UpdateName "Virtual-Vacuum" ` - -DeviceManufacturer "contoso" ` - -DeviceModel "virtual-vacuum-v1" ` - -OutputManifestPath . -#> -[CmdletBinding()] -Param( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] $OutputManifestPath, - - # Update Provider - [ValidateNotNullOrEmpty()] - [string] $UpdateProvider = "Contoso", - - # Update Provider - [ValidateNotNullOrEmpty()] - [string] $UpdateName = "Virtual-Vacuum", - - # Device Manufacturer - [ValidateNotNullOrEmpty()] - [string] $DeviceManufacturer = "contoso", - - # Device Model - [ValidateNotNullOrEmpty()] - [string] $DeviceModel = "virtual-vacuum-v1" -) - -######################################################################## -# Update : 6.0 -# -# Create a parent update that updates an optional 'steamers' components -######################################################################## - -$UpdateVersion = '6.0' - -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel -$parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion -$parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" - -$RefUpdateNamePrefix = $UpdateName - -Write-Host "Preparing update $parentUpdateIdStr ..." - - - - # ----------------------------------------------------------- - # Create a child update for an optional 'steamer' component - # that currently not connected to the host device. - # ----------------------------------------------------------- - - $RefUpdateManufacturer = "contoso" - $RefUpdateName = "$RefUpdateNamePrefix-virtual-steamers" - $RefUpdateVersion = "3.0" - - $steamersFirmwareVersion = "2.0" - - Write-Host " Preparing child update ($RefUpdateManufacturer/$RefUpdateName/$RefUpdateVersion)..." - - $steamersUpdateId = New-AduUpdateId -Provider $RefUpdateManufacturer -Name $RefUpdateName -Version $RefUpdateVersion - - # This components update only apply to 'steamers' group. - $steamersSelector = @{ group = 'steamers' } - $steamersCompat = New-AduUpdateCompatibility -Properties $steamersSelector - - $steamerScriptFile = "$PSScriptRoot\scripts\contoso-steamer-installscript.sh" - $steamerFirmwareFile = "$PSScriptRoot\data-files\steamer-firmware-$steamersFirmwareVersion.json" - - #------------ - # ADD STEP(S) - # - - # This update contains 3 steps. - $steamersInstallSteps = @() - - # Step #1 - simulating a success pre-install task. - $steamersInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` - -Files $steamerScriptFile ` - -HandlerProperties @{ ` - 'scriptFileName'='contoso-steamer-installscript.sh'; ` - 'installedCriteria'="$steamersFirmwareVersion"; ` - 'arguments'="--pre-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` - } ` - ` - -Description 'Steamers Update pre-install step' - - # Step #2 - install a mock firmware version 1.0 onto steamer component. - $steamersInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` - -Files $steamerScriptFile, $steamerFirmwareFile ` - -HandlerProperties @{ ` - 'scriptFileName'='contoso-steamer-installscript.sh'; ` - 'installedCriteria'="$steamersFirmwareVersion"; ` - "arguments"="--firmware-file steamer-firmware-$steamersFirmwareVersion.json --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` - } ` - ` - -Description 'Steamers Update - firmware installation' - - # Step #3 - simulating a success post-install task. - $steamersInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` - -Files $steamerScriptFile ` - -HandlerProperties @{ ` - 'scriptFileName'='contoso-steamer-installscript.sh'; ` - 'installedCriteria'="$steamersFirmwareVersion"; ` - "arguments"="--post-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` - } ` - ` - -Description 'Motors Update post-install step' - - # ------------------------------ - # Create child update manifest - # ------------------------------ - - $childUpdateId = $steamersUpdateId - $childUpdateIdStr = "$($childUpdateId.Provider).$($childUpdateId.Name).$($childUpdateId.Version)" - $childPayloadFiles = $steamerScriptFile, $steamerFirmwareFile - $childCompat = $steamersCompat - $childSteps = $steamersInstallSteps - - Write-Host " Preparing child update manifest $childUpdateIdStr ..." - - $childManifest = New-AduImportManifest -UpdateId $childUpdateId -IsDeployable $false ` - -Compatibility $childCompat ` - -InstallationSteps $childSteps ` - -ErrorAction Stop - - # Create folder for manifest files and payload. - $outputPath = "$OutputManifestPath\$parentUpdateIdStr" - Write-Host " Saving child manifest files and payload to $outputPath..." - New-Item $outputPath -ItemType Directory -ErrorAction SilentlyContinue | Out-Null - - # Generate manifest files. - $childManifest | Out-File "$outputPath\$childUpdateIdStr.importmanifest.json" -Encoding utf8 - - # Copy all payloads (if used) - Copy-Item -Path $childPayloadFiles -Destination $outputPath -Force - - Write-Host " " - -# ---------------------------- -# Create the parent update -# ---------------------------- -Write-Host " Preparing parent update $parentUpdateIdStr ..." - -$payloadFiles = -$parentSteps = @() - - #------------ - # ADD STEP(s) - - $parentSteps += New-AduInstallationStep -UpdateId $steamersUpdateId -Description "Steamers Firmware Version $steamersFirmwareVersion" - - -# ------------------------------ -# Create parent update manifest -# ------------------------------ - -Write-Host " Generating an import manifest $parentUpdateIdStr..." - -$parentManifest = New-AduImportManifest -UpdateId $parentUpdateId ` - -IsDeployable $true ` - -Compatibility $parentCompat ` - -InstallationSteps $parentSteps ` - -ErrorAction Stop - -# Create folder for manifest files and payload. -$outputPath = "$OutputManifestPath\$parentUpdateIdStr" -Write-Host " Saving parent manifest file and payload(s) to $outputPath..." -New-Item $outputPath -ItemType Directory -ErrorAction SilentlyContinue | Out-Null - -# Generate manifest file. -$parentManifest | Out-File "$outputPath\$parentUpdateIdStr.importmanifest.json" -Encoding utf8 - -# Copy all payloads (if used) -if ($payloadFiles) { Copy-Item -Path $payloadFiles -Destination $outputPath -Force } - -Write-Host " " diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-8.x.ps1 b/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-8.x.ps1 deleted file mode 100644 index a2742a4a7..000000000 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-8.x.ps1 +++ /dev/null @@ -1,327 +0,0 @@ -# -# Device Update for IoT Hub -# Sample PowerShell script for creating import manifests and payloads files for testing purposes. -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# - -#Requires -Version 5.0 - -<# - .SYNOPSIS - Create a sample updates for testing purposes. - - .EXAMPLE - PS > CreateSampleMSOEUpdate-8.x.ps1 -UpdateProvider "Contoso" ` - -UpdateName "Virtual-Vacuum" ` - -DeviceManufacturer "contoso" ` - -DeviceModel "virtual-vacuum-v1" ` - -OutputManifestPath . -#> -[CmdletBinding()] -Param( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] $OutputManifestPath, - - # Update Provider - [ValidateNotNullOrEmpty()] - [string] $UpdateProvider = "Contoso", - - # Update Provider - [ValidateNotNullOrEmpty()] - [string] $UpdateName = "Virtual-Vacuum", - - # Device Manufacturer - [ValidateNotNullOrEmpty()] - [string] $DeviceManufacturer = "contoso", - - # Device Model - [ValidateNotNullOrEmpty()] - [string] $DeviceModel = "virtual-vacuum-v1" -) - -############################################################## -# Update : 8.0 -# -# Create a parent update that updates 'hostfw' component, reboot a device, then update 'motors' and 'cameras' group respectively. -# -############################################################## - -$UpdateVersion = '8.0' - -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel -$parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion -$parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" - -$RefUpdateNamePrefix = $UpdateName - -Write-Host "Preparing update $parentUpdateIdStr ..." - # ------------------------------------------------- - # Create a child update for hostfw - # ------------------------------------------------- - $RefUpdateManufacturer = "contoso" - $RefUpdateName = "$RefUpdateNamePrefix-hostfw" - $RefUpdateVersion = "1.1" - - $hostfwFirmwareVersion = "1.1" - - Write-Host "Preparing child update ($RefUpdateManufacturer/$RefUpdateName/$RefUpdateVersion)..." - - $hostfwUpdateId = New-AduUpdateId -Provider $RefUpdateManufacturer -Name $RefUpdateName -Version $RefUpdateVersion - - # This components update only apply to a component named 'hostfw'. - $hostfwSelector = @{ name = 'hostfw' } - $hostfwCompat = New-AduUpdateCompatibility -Properties $hostfwSelector - - $hostfwScriptFile = "$PSScriptRoot\scripts\contoso-firmware-installscript.sh" - $hostfwFirmwareFile = "$PSScriptRoot\data-files\host-firmware-$hostfwFirmwareVersion.json" - - #------------ - # ADD STEP(S) - # - - $hostfwInstallSteps = @() - - # Step #0 - install host firmware and reboot - $hostfwInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` - -Files $hostfwScriptFile, $hostfwFirmwareFile ` - -HandlerProperties @{ ` - 'scriptFileName'='contoso-firmware-installscript.sh'; ` - 'installedCriteria'="$hostfwFirmwareVersion"; ` - 'arguments'="--restart-to-apply --firmware-file host-firmware-$hostfwFirmwareVersion.json --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` - } ` - ` - -Description 'Update Host Firmware then restart a device' - - # ------------------------------ - # Create child update manifest - # ------------------------------ - - $childUpdateId = $hostfwUpdateId - $childUpdateIdStr = "$($childUpdateId.Provider).$($childUpdateId.Name).$($childUpdateId.Version)" - $childPayloadFiles = $hostfwScriptFile, $hostfwFirmwareFile - $childCompat = $hostfwCompat - $childSteps = $hostfwInstallSteps - - Write-Host " Preparing child update manifest $childUpdateIdStr ..." - - $childManifest = New-AduImportManifest -UpdateId $childUpdateId -IsDeployable $false ` - -Compatibility $childCompat ` - -InstallationSteps $childSteps ` - -ErrorAction Stop - - # Create folder for manifest files and payload. - $outputPath = "$OutputManifestPath\$parentUpdateIdStr" - Write-Host " Saving child manifest files and payload to $outputPath..." - New-Item $outputPath -ItemType Directory -ErrorAction SilentlyContinue | Out-Null - - # Generate manifest files. - $childManifest | Out-File "$outputPath\$childUpdateIdStr.importmanifest.json" -Encoding utf8 - - # Copy all payloads (if used) - Copy-Item -Path $childPayloadFiles -Destination $outputPath -Force - - Write-Host " " - - # ------------------------------------------------- - # Create a child update for every component in a 'motors' group. - # ------------------------------------------------- - - $RefUpdateManufacturer = "contoso" - $RefUpdateName = "$RefUpdateNamePrefix-virtual-motors" - $RefUpdateVersion = "8.0" - - $motorsFirmwareVersion = "3.1" - - Write-Host "Preparing child update ($RefUpdateManufacturer/$RefUpdateName/$RefUpdateVersion)..." - - $motorsUpdateId = New-AduUpdateId -Provider $RefUpdateManufacturer -Name $RefUpdateName -Version $RefUpdateVersion - - # This components update only apply to 'motors' group. - $motorsSelector = @{ group = 'motors' } - $motorsCompat = New-AduUpdateCompatibility -Properties $motorsSelector - - $motorScriptFile = "$PSScriptRoot\scripts\contoso-motor-installscript.sh" - $motorFirmwareFile = "$PSScriptRoot\data-files\motor-firmware-$motorsFirmwareVersion.json" - - #------------ - # ADD STEP(S) - # - - $motorsInstallSteps = @() - - # Step #0 - simulating a success pre-install task. - $motorsInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` - -Files $motorScriptFile ` - -HandlerProperties @{ ` - 'scriptFileName'='contoso-motor-installscript.sh'; ` - 'installedCriteria'="$motorsFirmwareVersion"; ` - 'arguments'="--pre-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` - } ` - ` - -Description 'Motors Update pre-install step' - - # Step #1 - install a mock firmware version 1.2 onto motor component. - $motorsInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` - -Files $motorScriptFile, $motorFirmwareFile ` - -HandlerProperties @{ ` - 'scriptFileName'='contoso-motor-installscript.sh'; ` - 'installedCriteria'="$motorsFirmwareVersion"; ` - "arguments"="--firmware-file motor-firmware-$motorsFirmwareVersion.json --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` - } ` - ` - -Description 'Motors Update - firmware installation' - - - # ------------------------------ - # Create child update manifest - # ------------------------------ - - $childUpdateId = $motorsUpdateId - $childUpdateIdStr = "$($childUpdateId.Provider).$($childUpdateId.Name).$($childUpdateId.Version)" - $childPayloadFiles = $motorScriptFile, $motorFirmwareFile - $childCompat = $motorsCompat - $childSteps = $motorsInstallSteps - - Write-Host " Preparing child update manifest $childUpdateIdStr ..." - - $childManifest = New-AduImportManifest -UpdateId $childUpdateId -IsDeployable $false ` - -Compatibility $childCompat ` - -InstallationSteps $childSteps ` - -ErrorAction Stop - - # Create folder for manifest files and payload. - $outputPath = "$OutputManifestPath\$parentUpdateIdStr" - Write-Host " Saving child manifest files and payload to $outputPath..." - New-Item $outputPath -ItemType Directory -ErrorAction SilentlyContinue | Out-Null - - # Generate manifest files. - $childManifest | Out-File "$outputPath\$childUpdateIdStr.importmanifest.json" -Encoding utf8 - - # Copy all payloads (if used) - Copy-Item -Path $childPayloadFiles -Destination $outputPath -Force - - Write-Host " " - - - # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - # ------------------------------------------------- - # Create a child update for every component in a 'cameras' group - # ------------------------------------------------- - - $RefUpdateManufacturer = "contoso" - $RefUpdateName = "$RefUpdateNamePrefix-virtual-cameras" - $RefUpdateVersion = "8.0" - - $camerasFirmwareVersion = "3.0" - - Write-Host "Preparing child update ($RefUpdateManufacturer/$RefUpdateName/$RefUpdateVersion)..." - - $camerasUpdateId = New-AduUpdateId -Provider $RefUpdateManufacturer -Name $RefUpdateName -Version $RefUpdateVersion - - # This components update only apply to 'cameras' group. - $camerasSelector = @{ group = 'cameras' } - $camerasCompat = New-AduUpdateCompatibility -Properties $camerasSelector - - $cameraScriptFile = "$PSScriptRoot\scripts\contoso-camera-installscript.sh" - $cameraFirmwareFile = "$PSScriptRoot\data-files\camera-firmware-$camerasFirmwareVersion.json" - - #------------ - # ADD STEP(S) - # - - $camerasInstallSteps = @() - - # Step #0 - simulating a success pre-install task. - $camerasInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` - -Files $cameraScriptFile ` - -HandlerProperties @{ ` - 'scriptFileName'='contoso-camera-installscript.sh'; ` - 'installedCriteria'="$camerasFirmwareVersion"; ` - 'arguments'="--pre-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` - } ` - ` - -Description 'Cameras Update pre-install step' - - # Step #1 - install a mock firmware version 2.1 onto camera component. - $camerasInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` - -Files $cameraScriptFile, $cameraFirmwareFile ` - -HandlerProperties @{ ` - 'scriptFileName'='contoso-camera-installscript.sh'; ` - 'installedCriteria'="$camerasFirmwareVersion"; ` - "arguments"="--firmware-file camera-firmware-$camerasFirmwareVersion.json --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` - } ` - ` - -Description 'Cameras Update - firmware installation' - - # ------------------------------ - # Create child update manifest - # ------------------------------ - - $childUpdateId = $camerasUpdateId - $childUpdateIdStr = "$($childUpdateId.Provider).$($childUpdateId.Name).$($childUpdateId.Version)" - $childPayloadFiles = $cameraScriptFile, $cameraFirmwareFile - $childCompat = $camerasCompat - $childSteps = $camerasInstallSteps - - Write-Host " Preparing child update manifest $childUpdateIdStr ..." - - $childManifest = New-AduImportManifest -UpdateId $childUpdateId -IsDeployable $false ` - -Compatibility $childCompat ` - -InstallationSteps $childSteps ` - -ErrorAction Stop - - # Create folder for manifest files and payload. - $outputPath = "$OutputManifestPath\$parentUpdateIdStr" - Write-Host " Saving child manifest files and payload to $outputPath..." - New-Item $outputPath -ItemType Directory -ErrorAction SilentlyContinue | Out-Null - - # Generate manifest files. - $childManifest | Out-File "$outputPath\$childUpdateIdStr.importmanifest.json" -Encoding utf8 - - # Copy all payloads (if used) - Copy-Item -Path $childPayloadFiles -Destination $outputPath -Force - - Write-Host " " - -# ---------------------------- -# Create the parent update -# ---------------------------- -Write-Host " Preparing parent update $parentUpdateIdStr ..." - -$payloadFiles = -$parentSteps = @() - - #------------ - # ADD STEP(s) - - $parentSteps += New-AduInstallationStep -UpdateId $hostfwUpdateId -Description "Host Firmware Version $hostfwFirmwareVersion" - $parentSteps += New-AduInstallationStep -UpdateId $motorsUpdateId -Description "Motors Firmware Version $motorsFirmwareVersion" - $parentSteps += New-AduInstallationStep -UpdateId $camerasUpdateId -Description "Cameras Firmware Version $camerasFirmwareVersion" - -# ------------------------------ -# Create parent update manifest -# ------------------------------ - -Write-Host " Generating an import manifest $parentUpdateIdStr..." - -$parentManifest = New-AduImportManifest -UpdateId $parentUpdateId ` - -IsDeployable $true ` - -Compatibility $parentCompat ` - -InstallationSteps $parentSteps ` - -ErrorAction Stop - -# Create folder for manifest files and payload. -$outputPath = "$OutputManifestPath\$parentUpdateIdStr" -Write-Host " Saving parent manifest file and payload(s) to $outputPath..." -New-Item $outputPath -ItemType Directory -ErrorAction SilentlyContinue | Out-Null - -# Generate manifest file. -$parentManifest | Out-File "$outputPath\$parentUpdateIdStr.importmanifest.json" -Encoding utf8 - -# Copy all payloads (if used) -if ($payloadFiles) { Copy-Item -Path $payloadFiles -Destination $outputPath -Force } - -Write-Host " " diff --git a/src/extensions/component_enumerators/examples/contoso_component_enumerator/CMakeLists.txt b/src/extensions/component_enumerators/examples/contoso_component_enumerator/CMakeLists.txt new file mode 100644 index 000000000..e4f949aa5 --- /dev/null +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/CMakeLists.txt @@ -0,0 +1,17 @@ +project (contoso_component_enumerator) + +include (agentRules) + +compileasc99 () + +add_library (${PROJECT_NAME} SHARED contoso_component_enumerator.cpp) + +find_package (Parson REQUIRED) + +add_library (aduc::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +target_include_directories (${PROJECT_NAME} PUBLIC ${ADU_EXTENSION_INCLUDES} ${ADU_EXPORT_INCLUDES}) + +target_link_libraries (${PROJECT_NAME} PUBLIC Parson::parson aduc::contract_utils) + +install (TARGETS ${PROJECT_NAME} LIBRARY DESTINATION ${ADUC_EXTENSIONS_INSTALL_FOLDER}) diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/README.md b/src/extensions/component_enumerators/examples/contoso_component_enumerator/README.md similarity index 97% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/README.md rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/README.md index a19e6533b..118d1d8c9 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/README.md +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/README.md @@ -26,7 +26,7 @@ For this demonstration, the Contoso Virtual Vacuum consists of five 'logical' co **Figure 1** - Contoso Virtual Vacuum Components Diagram -![Contoso Virtual-Vacuum Components Diagram](../contoso-component-enumerator/assets/contoso-virtual-vacuum-components-diagram.svg) +![Contoso Virtual-Vacuum Components Diagram](../contoso_component_enumerator/assets/contoso-virtual-vacuum-components-diagram.svg) We use the following directory structure to simulates those components mentioned above. @@ -68,7 +68,7 @@ After doing this the Update Content Handler can install and apply the update to **Figure 2** - Proxy Update Flow Diagram -![Contoso Virtual-Vacuum Update Flow](../contoso-component-enumerator/assets/contoso-virtual-vacuum-update-flow.svg) +![Contoso Virtual-Vacuum Update Flow](../contoso_component_enumerator/assets/contoso-virtual-vacuum-update-flow.svg) - **Device Builder responsibilities** - Design and build the device. @@ -88,7 +88,7 @@ After doing this the Update Content Handler can install and apply the update to - Collects all update results from Parent and Child Update(s) and reports it to the Azure IoT Hub. - Child **Steps Handler** - Iterate through a list of **instances of component** that are compatible with the **Child Update** content. - - See [Steps Handler](../../../../content_handlers/steps_handler/README.md) for more information. + - See [Steps Handler](../../../../extensions/update_manifest_handlers/steps_handler/README.md) for more information. - **SWUpdate Installer** and **Motors Firmware Installer** - See [How To Implement Custom Update Content Handler](../../../../../docs/agent-reference/how-to-implement-custom-update-handler.md) for more details. @@ -454,8 +454,8 @@ For example, for `hostfw`, the value of property `properties.version` will be po This example is written in `C++`. You can choose to use `C` if desired. -- [CMakeLists.txt](../contoso-component-enumerator/CMakeLists.txt) -- [contoso-component-enumerator.cpp](../contoso-component-enumerator/contoso-component-enumerator.cpp) +- [CMakeLists.txt](../contoso_component_enumerator/CMakeLists.txt) +- [contoso_component_enumerator.cpp](../contoso_component_enumerator/contoso_component_enumerator.cpp) - [inc/aduc/component_enumerator_extension.hpp](../../inc/aduc/../../../inc/aduc/component_enumerator_extension.hpp) ### Example Proxy Update diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/assets/contoso-virtual-vacuum-components-diagram.svg b/src/extensions/component_enumerators/examples/contoso_component_enumerator/assets/contoso-virtual-vacuum-components-diagram.svg similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/assets/contoso-virtual-vacuum-components-diagram.svg rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/assets/contoso-virtual-vacuum-components-diagram.svg diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/assets/contoso-virtual-vacuum-update-flow.svg b/src/extensions/component_enumerators/examples/contoso_component_enumerator/assets/contoso-virtual-vacuum-update-flow.svg similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/assets/contoso-virtual-vacuum-update-flow.svg rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/assets/contoso-virtual-vacuum-update-flow.svg diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/contoso-component-enumerator.cpp b/src/extensions/component_enumerators/examples/contoso_component_enumerator/contoso_component_enumerator.cpp similarity index 52% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/contoso-component-enumerator.cpp rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/contoso_component_enumerator.cpp index 1b792f8fe..5ea3ae6b9 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/contoso-component-enumerator.cpp +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/contoso_component_enumerator.cpp @@ -7,6 +7,7 @@ */ #include "aduc/component_enumerator_extension.hpp" #include "parson.h" +#include #include #include #include @@ -43,7 +44,7 @@ // const char* g_contosoComponentInventoryFilePath = "/usr/local/contoso-devices/components-inventory.json"; -JSON_Value* _GetAllComponentsFromFile(const char* configFilepath) +static JSON_Value* _GetAllComponentsFromFile(const char* configFilepath) { // Read config file. JSON_Value* rootValue = json_parse_file(configFilepath); @@ -134,101 +135,130 @@ JSON_Value* _GetAllComponentsFromFile(const char* configFilepath) return rootValue; } -extern "C" +static bool _json_object_contains_named_value(JSON_Object* jsonObject, const char* name, const char* value) { - bool _json_object_contains_named_value(JSON_Object* jsonObject, const char* name, const char* value) + if (!(jsonObject != nullptr && name != nullptr && *name != 0 && value != nullptr && *value != 0)) { - if (!(jsonObject != nullptr && name != nullptr && *name != 0 && value != nullptr && *value != 0)) - { - return false; - } - - return ( - json_object_has_value_of_type(jsonObject, name, JSONString) != 0 - && strcmp(json_object_get_string(jsonObject, name), value) == 0); + return false; } - /** - * @brief Select component(s) that contain property or properties matching specified in @p selectorJson string. - * - * Example input json: - * - Select all components belong to a 'Motors' group - * "{\"group\":\"Motors\"}" - * - * - Select a component with name equals 'left-motor' - * "{\"name\":\"left-motor\"}" - * - * - Select components matching specified class (manufature/model) - * "{\"manufacturer\":\"Contoso\",\"model\":\"USB-Motor-0001\"}" - * - * @param selectorJson A stringified json containing one or more properties use for components selection. - * @return Returns a serialized json data containing components information. - * Caller must call FreeString function when done with the returned string. - */ - char* SelectComponents(const char* selectorJson) - { - char* outputString = nullptr; + return ( + json_object_has_value_of_type(jsonObject, name, JSONString) != 0 + && strcmp(json_object_get_string(jsonObject, name), value) == 0); +} - JSON_Value* allComponentsValue = nullptr; - JSON_Array* componentsArray = nullptr; +EXTERN_C_BEGIN - JSON_Value* selectorValue = json_parse_string(selectorJson); - JSON_Object* selector = json_object(selectorValue); - if (selector == nullptr) - { - goto done; - } +///////////////////////////////////////////////////////////////////////////// +// BEGIN Shared Library Export Functions +// +// These are the function symbols that the device update agent will +// lookup and call. +// - // NOTE: For demonstration purposes, we're popoulating components data by reading from - // the specified 'component inventory' file. - allComponentsValue = _GetAllComponentsFromFile(g_contosoComponentInventoryFilePath); - componentsArray = json_object_get_array(json_object(allComponentsValue), "components"); - if (componentsArray == nullptr) - { - goto done; - } +/** + * @brief Select component(s) that contain property or properties matching specified in @p selectorJson string. + * + * Example input json: + * - Select all components belong to a 'Motors' group + * "{\"group\":\"Motors\"}" + * + * - Select a component with name equals 'left-motor' + * "{\"name\":\"left-motor\"}" + * + * - Select components matching specified class (manufature/model) + * "{\"manufacturer\":\"Contoso\",\"model\":\"USB-Motor-0001\"}" + * + * @param selectorJson A stringified json containing one or more properties use for components selection. + * @return Returns a serialized json data containing components information. + * Caller must call FreeString function when done with the returned string. + */ +char* SelectComponents(const char* selectorJson) +{ + char* outputString = nullptr; + + JSON_Value* allComponentsValue = nullptr; + JSON_Array* componentsArray = nullptr; - // Keep only components that contain all properties (name & value) specified in the selector. - for (int i = json_array_get_count(componentsArray) - 1; i >= 0; i--) + JSON_Value* selectorValue = json_parse_string(selectorJson); + JSON_Object* selector = json_object(selectorValue); + if (selector == nullptr) + { + goto done; + } + + // NOTE: For demonstration purposes, we're popoulating components data by reading from + // the specified 'component inventory' file. + allComponentsValue = _GetAllComponentsFromFile(g_contosoComponentInventoryFilePath); + componentsArray = json_object_get_array(json_object(allComponentsValue), "components"); + if (componentsArray == nullptr) + { + goto done; + } + + // Keep only components that contain all properties (name & value) specified in the selector. + for (int i = json_array_get_count(componentsArray) - 1; i >= 0; i--) + { + JSON_Object* component = json_array_get_object(componentsArray, i); + for (int s = json_object_get_count(selector) - 1; s >= 0; s--) { - JSON_Object* component = json_array_get_object(componentsArray, i); - for (int s = json_object_get_count(selector) - 1; s >= 0; s--) + bool matched = _json_object_contains_named_value( + component, json_object_get_name(selector, s), json_string(json_object_get_value_at(selector, s))); + if (!matched) { - bool matched = _json_object_contains_named_value( - component, json_object_get_name(selector, s), json_string(json_object_get_value_at(selector, s))); - if (!matched) - { - json_array_remove(componentsArray, (size_t)i); - } + json_array_remove(componentsArray, (size_t)i); } } + } - outputString = json_serialize_to_string_pretty(allComponentsValue); + outputString = json_serialize_to_string_pretty(allComponentsValue); - done: - json_value_free(selectorValue); - json_value_free(allComponentsValue); +done: + json_value_free(selectorValue); + json_value_free(allComponentsValue); - return outputString; - } + return outputString; +} - /** - * @brief Returns all components information in JSON format. - * @param includeProperties Indicates whether to include optional component's properties in the output string. - * @return Returns a serialized json data contains components information. Caller must call FreeComponentsDataString function - * when done with the returned string. - */ - char* GetAllComponents() - { - JSON_Value* val = _GetAllComponentsFromFile(g_contosoComponentInventoryFilePath); - char* returnString = json_serialize_to_string_pretty(val); - json_value_free(val); - return returnString; - } +/** + * @brief Returns all components information in JSON format. + * @param includeProperties Indicates whether to include optional component's properties in the output string. + * @return Returns a serialized json data contains components information. Caller must call FreeComponentsDataString function + * when done with the returned string. + */ +char* GetAllComponents() +{ + JSON_Value* val = _GetAllComponentsFromFile(g_contosoComponentInventoryFilePath); + char* returnString = json_serialize_to_string_pretty(val); + json_value_free(val); + return returnString; +} - void FreeComponentsDataString(char* string) - { - json_free_serialized_string(string); - } +/** + * @brief Frees the components data string allocated by GetAllComponents. + * + * @param string The string previously returned by GetAllComponents. + */ +void FreeComponentsDataString(char* string) +{ + json_free_serialized_string(string); +} + +/** + * @brief Gets the extension contract info. + * + * @param[out] contractInfo The extension contract info. + * @return ADUC_Result The result. + */ +ADUC_Result GetContractInfo(ADUC_ExtensionContractInfo* contractInfo) +{ + contractInfo->majorVer = ADUC_V1_CONTRACT_MAJOR_VER; + contractInfo->majorVer = ADUC_V1_CONTRACT_MINOR_VER; + return ADUC_Result{ ADUC_GeneralResult_Success, 0 }; +} + +// +// END Shared Library Export Functions +///////////////////////////////////////////////////////////////////////////// -} // extern "c" +EXTERN_C_END diff --git a/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/README.md b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/README.md new file mode 100644 index 000000000..fddc1829a --- /dev/null +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/README.md @@ -0,0 +1,437 @@ +# Device Update for Azure IoT Hub Tutorials + +Tutorials +- [Tutorial 1](#tutorial-1---single-step-update) - Single Step Update +- [Tutorial 2](#tutorial-2---multi-step-update) - Multi Step Update +- [Tutorial 3](#tutorial-3---bundle-update-and-multi-component-update) - Bundle Update and Multi Component Update + +--- +## Tutorial 1 - Single Inline Step Update + +As described in [Multi Step Ordered Execution]() document, a step can be either `inline` or `reference`. + +This example update (Contoso.Virtual-Vacuum.1.0) demonstrates how to create an update with a single `inline` step that will install an APT package ('tree 10.7.0-5') onto the target device. + +Follow this [instruction](#generate-example-updates) to generate the example update using `CreateSampleMSOEUpdate-1.x.ps1` PowerShell script. + +### Example Import Manifest - Contoso.Virtual-Vacuum.1.0 + +```json +{ + "updateId": { + "provider": "Contoso", + "name": "Virtual-Vacuum", + "version": "1.0" + }, + "isDeployable": true, + "compatibility": [ + { + "model": "virtual-vacuum-v1", + "manufacturer": "contoso" + } + ], + "instructions": { + "steps": [ + { + "type": "inline", + "description": "APT update that installs libcurl4-doc package.", + "handler": "microsoft/apt:1", + "files": [ + "apt-manifest-1.0.json" + ], + "handlerProperties": { + "installedCriteria": "apt-update-test-1.1" + } + } + ] + }, + "files": [ + { + "filename": "apt-manifest-1.0.json", + "sizeInBytes": 132, + "hashes": { + "sha256": "nACI0Obg4Px/f2f85oOko3bHE0HJ8Vlq3CMafx9g2Bo=" + } + } + ], + "createdDateTime": "2022-05-13T21:57:08.9036596Z", + "manifestVersion": "4.0" +} + +``` + +From the example import manifest above, you will notice the following: +- The `instructions.steps` property (array) contains only 1 step. +- The `type` property indicates that this is an inline step. +- A `handler` property informs the Device Update agent which Content Handler to be used to process this step. +- The `files` property contains a single file entity. This file is called an APT manifest. The current implementation of APT Content Handler requires one APT manifest. (See [apt-manifest-1.0.json](./sample-updates/data-files/APT/apt-manifest-1.0.json)) +- An `handlerProperties.installedCriteria` is a string used by APT Content Handler to determine whether this 'step' has been installed (or processed) on the target device. + +--- +## Tutorial 2 - Multi Step Update + +A **Multi Step Update** is an update that contains at least two steps. If at least one of those steps is a `reference` step, the update will be called `Bundle Update`. + +This example update (Contoso.Virtual-Vacuum.2.2) demonstrates how to create an update with two `inline` steps. +- Step 0 - Install an APT package ('libcurl4-doc') onto the target device. +- Step 1 - Install an APT package ('tree 10.7.0-5') onto the target device. + +> NOTE | This demonstrates that you can use two steps to install two APT packages. Alternatively, you can install two packages in a single step by adding both packages' information in a single APT manifest. + +Follow this [instruction](#generate-example-updates) to generate the example update using `CreateSampleMSOEUpdate-2.x.ps1` PowerShell script. + +### Example Import Manifest - Contoso.Virtual-Vacuum.2.2 + +```json +{ + "updateId": { + "provider": "Contoso", + "name": "Virtual-Vacuum", + "version": "2.2" + }, + "isDeployable": true, + "compatibility": [ + { + "model": "virtual-vacuum-v1", + "manufacturer": "contoso" + } + ], + "instructions": { + "steps": [ + { + "type": "inline", + "description": "Install libcurl4-doc on host device", + "handler": "microsoft/apt:1", + "files": [ + "apt-manifest-1.0.json" + ], + "handlerProperties": { + "installedCriteria": "apt-update-test-2.2" + } + }, + { + "type": "inline", + "description": "Install tree on host device", + "handler": "microsoft/apt:1", + "files": [ + "apt-manifest-tree-1.0.json" + ], + "handlerProperties": { + "installedCriteria": "apt-update-test-tree-2.2" + } + } + ] + }, + "files": [ + { + "filename": "apt-manifest-1.0.json", + "sizeInBytes": 124, + "hashes": { + "sha256": "cjJdOa7v4bYyKUrCtF7cSTuPyhfWt8BYkujGtkRDjJ8=" + } + }, + { + "filename": "apt-manifest-tree-1.0.json", + "sizeInBytes": 138, + "hashes": { + "sha256": "8vEB0TLIWS6PFkQhJqlcY8ZGiEZGoZALLF9Yx6wP2XA=" + } + } + ], + "createdDateTime": "2022-05-13T21:47:36.3189977Z", + "manifestVersion": "4.0" +} +``` + +This import manifest contains steps that use 'microsoft/apt:1' handler type. Each step requires a corresponding APT manifest containing information about the package to be installed. + +--- + +## Tutorial 3 - Bundle Update and Multi Component Update + +A **Bundle Update** is an update that contains at least one reference step. + +A **Multi Component Update** is a Bundle Update intended for a device with a registered `Component Enumerator Extension`. + +> If the target device doesn't have a registered component enumerator, the Device Update Agent will assume that every step is intended to be installed onto the host device and will not pass any 'Component Context' to a Content Handler when processing steps. +> +> See [Steps Handler](../../../../../extensions/update_manifest_handlers/steps_handler/) for more information. + +Follow this [instruction](#generate-example-updates) to generate the example update using `CreateSampleMSOEUpdate-3.x.ps1` PowerShell script. + +**Important** - follow this [instruction](#register-contoso-components-enumerator-extension) to register the example Contoso Component Enumerator and Components Inventory file. + +### Example Import Manifest - Contoso.Virtual-Vacuum.3.3 + +This import manifest contains a single reference step that requires the [contoso Virtual-Vacuum-virtual-camera 1.4](#example-import-manifest-for-reference-update-virtual-vacuum-virtual-camera-14) update to be imported simultaneously. + +```json +{ + "updateId": { + "provider": "Contoso", + "name": "Virtual-Vacuum", + "version": "3.3" + }, + "isDeployable": true, + "compatibility": [ + { + "model": "virtual-vacuum-v1", + "manufacturer": "contoso" + } + ], + "instructions": { + "steps": [ + { + "type": "reference", + "description": "Cameras Firmware Update", + "updateId": { + "provider": "contoso", + "name": "Virtual-Vacuum-virtual-camera", + "version": "1.4" + } + } + ] + }, + "createdDateTime": "2022-09-19T04:02:43.9745720Z", + "manifestVersion": "4.0" +} + +``` + +#### Example Import Manifest for the reference update Virtual-vacuum-virtual-camera 1.4 + +```json +{ + "updateId": { + "provider": "contoso", + "name": "Virtual-Vacuum-virtual-camera", + "version": "1.4" + }, + "isDeployable": false, + "compatibility": [ + { + "group": "cameras" + } + ], + "instructions": { + "steps": [ + { + "type": "inline", + "description": "Cameras Update - pre-install step (failure - missing file)", + "handler": "microsoft/script:1", + "files": [ + "contoso-camera-installscript.sh" + ], + "handlerProperties": { + "scriptFileName": "contoso-camera-installscript.sh", + "arguments": "--pre-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path", + "installedCriteria": "1.1" + } + }, + { + "type": "inline", + "description": "Cameras Update - firmware installation", + "handler": "microsoft/script:1", + "files": [ + "contoso-camera-installscript.sh", + "camera-firmware-1.1.json" + ], + "handlerProperties": { + "scriptFileName": "contoso-camera-installscript.sh", + "arguments": "--firmware-file camera-firmware-1.1.json --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path", + "installedCriteria": "1.1" + } + }, + { + "type": "inline", + "description": "Cameras Update - post-install script", + "handler": "microsoft/script:1", + "files": [ + "contoso-camera-installscript.sh" + ], + "handlerProperties": { + "scriptFileName": "contoso-camera-installscript.sh", + "arguments": "--post-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path", + "installedCriteria": "1.1" + } + } + ] + }, + "files": [ + { + "filename": "contoso-camera-installscript.sh", + "sizeInBytes": 26884, + "hashes": { + "sha256": "K6F2TUmY2JPtp6vWI8bNl6sVceacBFTHXIyhOOmS1x8=" + } + }, + { + "filename": "camera-firmware-1.1.json", + "sizeInBytes": 124, + "hashes": { + "sha256": "6jst7Roi7dbeb3BNokQo6Gea5qsPPu2KDJ6GotaOQi4=" + } + } + ], + "createdDateTime": "2022-09-19T04:02:43.9465570Z", + "manifestVersion": "4.0" +} + +``` + +### Example Import Manifest - Contoso.Virtual-Vacuum-virtual-camera.1.4 + + ```json +{ + "updateId": { + "provider": "contoso", + "name": "Virtual-Vacuum-virtual-camera", + "version": "1.4" + }, + "isDeployable": false, + "compatibility": [ + { + "group": "cameras" + } + ], + "instructions": { + "steps": [ + { + "type": "inline", + "description": "Cameras Update - firmware installation", + "handler": "microsoft/script:1", + "files": [ + "contoso-camera-installscript.sh", + "camera-firmware-1.1.json" + ], + "handlerProperties": { + "scriptFileName": "contoso-camera-installscript.sh", + "arguments": "--firmware-file camera-firmware-1.1.json --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path", + "installedCriteria": "1.4" + } + } + ] + }, + "files": [ + { + "filename": "contoso-camera-installscript.sh", + "sizeInBytes": 27849, + "hashes": { + "sha256": "aj2H25P9GnRyAQg4wfiixJToUkDOsch3YQ0fNgHn4as=" + } + }, + { + "filename": "camera-firmware-1.1.json", + "sizeInBytes": 130, + "hashes": { + "sha256": "6lYoRUnOVymga/7YH6qSKHv0osG4MFpfSW+naOq7/4M=" + } + } + ], + "createdDateTime": "2022-05-13T22:49:03.4040790Z", + "manifestVersion": "4.0" +} +``` + +The above update contains a single inline step that installs `camera firmware v1.1` onto every compatible `component` that is currently connected to a host device (as reported by the registered Component Enumerator extension). + +When the Component Enumerator extension is registered, the Device Update agent will **select** the compatible `component` by invoking a `SelectComponents` api and passing the referenced-update's compatibility property as a `selector`. For this example, the component enumerator will return every component with a `group` property value that equals `cameras`. + +When processing this step, the Agent will iterate through the list of returned compatible components and perform the step's actions (download, install, apply) on each component with each component's context accordingly. +See [Steps Handler](../../../../../extensions/step_handlers/steps_handler/) and [Component Enumerator Extension](../../contoso_component_enumerator/README.md) for more details. + +--- +## Appendix + +### Setup Test Device + +Prerequisites + +- A test device with Ubuntu 18.04 LTS (or Virtual Machine) +- Connection to the internet +#### Install the Device Update Agent and Dependencies + +- Register packages.microsoft.com in APT package repository + + ```sh + curl https://packages.microsoft.com/config/ubuntu/18.04/multiarch/prod.list > ~/microsoft-prod.list + + sudo cp ~/microsoft-prod.list /etc/apt/sources.list.d/ + + curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > ~/microsoft.gpg + + sudo cp ~/microsoft.gpg /etc/apt/trusted.gpg.d/ + + sudo apt-get update + ``` + +- Install test **deviceupdate-agent-[version].deb** package on the test device. +e.g. + + ```sh + sudo apt-get install ./deviceupdate-agent-0.8.0-public-preview-refresh.deb + ``` + +Note: this will automatically install the delivery-optimization-agent package from packages.microsoft.com + +- Specify a connection string in /etc/adu/du-config.json + +>NOTE | The current example only compatible with device with "contoso" manufacturer, and "virtual-vacuum-v1" model. These values must be correctly set inside `agents` entry in du-config.json + +- Restart `deviceupdate-agent` service + +```sh +sudo systemctl restart deviceupdate-agent +``` + +### Optional Steps + +The following optional steps are required for Multi Component Update (a.k.a. Proxy Update). + +#### Register Contoso Components Enumerator extension + +Run following command on your test device + +```sh +sudo /usr/bin/AducIotAgent -E /var/lib/adu/extensions/sources/libcontoso_component_enumerator.so +``` + +#### Setup Mock Components + +For testing and demonstration purposes, we'll be creating following mock components on the device: + +- 3 motors +- 2 cameras +- hostfs +- rootfs + +**IMPORTANT** +This components configuration depends on the implementation of an example Component Enumerator extension called libcontoso_component_enumerator.so, which required a mock component inventory data file `/usr/local/contoso-devices/components-inventory.json` + +> Tip: you can copy [demo](../demo) folder to your home directory on the test VM and run `sudo ~/demo/tools/reset-demo-components.sh` to copy required files to the right locations. + +#### Add /usr/local/contoso-devices/components-inventory.json + +- Copy [components-inventory.json](./demo-devices/contoso-devices/components-inventory.json) to **/usr/local/contoso-devices** folder + +### Generate Example Updates + +Open PowerShell terminal, go [[project-root]/tools/AduCmdlets](../../../../../../tools/AduCmdlets/) directory, then import AduUpdate.psm1 module: + +```powershell +Import-Module ./AduUpdate.psm1 +``` + +Next, go to [sample-updates](./sample-updates/) directory, then run following commands to generate **all** example updates: + +> NOTE | You can choose to generate only updates you want to try. + +```powershell +./CreateSampleMSOEUpdate-1.x.ps1 +./CreateSampleMSOEUpdate-2.x.ps1 +./CreateSampleMSOEUpdate-3.x.ps1 +./CreateSampleMSOEUpdate-4.x.ps1 +./CreateSampleMSOEUpdate-5.x.ps1 +./CreateSampleMSOEUpdate-6.x.ps1 +./CreateSampleMSOEUpdate-7.x.ps1 +``` diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/contoso-components-config.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/components-inventory-with-steamers.json similarity index 88% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/contoso-components-config.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/components-inventory-with-steamers.json index 21d22c5da..814936e10 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/contoso-components-config.json +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/components-inventory-with-steamers.json @@ -87,6 +87,17 @@ "path": "\/usr\/local\/contoso-devices\/vacuum-1\/cameras\/contoso-camera-serial-00001", "firmwareDataFile": "firmware.json" } + }, + { + "id": "contoso-steamer-serial-00000", + "name": "main-steamer", + "group": "steamers", + "manufacturer": "contoso", + "model": "virtual-steamer", + "properties": { + "path": "\/usr\/local\/contoso-devices\/vacuum-1\/steamers\/contoso-steamer-serial-00000", + "firmwareDataFile": "firmware.json" + } } ] } diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/components-inventory.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/components-inventory.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/components-inventory.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/components-inventory.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/bootfs/diskimage.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/bootfs/diskimage.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/bootfs/diskimage.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/bootfs/diskimage.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/cameras/contoso-camera-serial-00000/firmware.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/cameras/contoso-camera-serial-00000/firmware.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/cameras/contoso-camera-serial-00000/firmware.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/cameras/contoso-camera-serial-00000/firmware.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/cameras/contoso-camera-serial-00001/firmware.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/cameras/contoso-camera-serial-00001/firmware.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/cameras/contoso-camera-serial-00001/firmware.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/cameras/contoso-camera-serial-00001/firmware.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/hostfw/firmware.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/hostfw/firmware.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/hostfw/firmware.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/hostfw/firmware.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/motors/contoso-motor-serial-00000/firmware.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/motors/contoso-motor-serial-00000/firmware.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/motors/contoso-motor-serial-00000/firmware.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/motors/contoso-motor-serial-00000/firmware.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/motors/contoso-motor-serial-00001/firmware.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/motors/contoso-motor-serial-00001/firmware.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/motors/contoso-motor-serial-00001/firmware.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/motors/contoso-motor-serial-00001/firmware.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/motors/contoso-motor-serial-00002/firmware.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/motors/contoso-motor-serial-00002/firmware.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/motors/contoso-motor-serial-00002/firmware.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/motors/contoso-motor-serial-00002/firmware.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/rootfs/diskimage.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/rootfs/diskimage.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/demo-devices/contoso-devices/vacuum-1/rootfs/diskimage.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/rootfs/diskimage.json diff --git a/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/unplugged-streamers/contoso-steamer-serial-00000/firmware.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/unplugged-streamers/contoso-steamer-serial-00000/firmware.json new file mode 100644 index 000000000..9533716df --- /dev/null +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/demo-devices/contoso-devices/vacuum-1/unplugged-streamers/contoso-steamer-serial-00000/firmware.json @@ -0,0 +1,4 @@ +{ + "version": "0.1", + "description": "This component is generated for testing purposes." +} diff --git a/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/AduUpdate.psm1 b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/AduUpdate.psm1 new file mode 100644 index 000000000..860aabf83 --- /dev/null +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/AduUpdate.psm1 @@ -0,0 +1,399 @@ +# +# Device Update for IoT Hub +# PowerShell module for creating import manifest for Device Update for IoT Hub (ADU). +# Copyright (c) Microsoft Corporation. +# + +#Requires -Version 5.0 + +# ------------------------------------------------- +# CLASSES +# ------------------------------------------------- + +class UpdateId +{ + [string] $Provider + [string] $Name + [string] $Version + + UpdateId($provider, $name, $version) + { + $this.Provider = $provider + $this.Name = $name + $this.Version = $version + } +} + +# ------------------------------------------------- +# INTERNAL METHODS +# ------------------------------------------------- + +function Get-UpdateId +{ + Param( + [ValidateNotNullOrEmpty()] + [string] $Provider = $(throw "'Provider' parameter is required."), + + [ValidateNotNullOrEmpty()] + [string] $Name = $(throw "'Name' parameter is required."), + + [ValidateNotNullOrEmpty()] + [version] $Version = $(throw "'Version' parameter is required.") + ) + + # Server will accept any order; preserving order for aesthetics only. + $updateId = [ordered] @{ + 'provider' = $Provider + 'name' = $Name + 'version' = "$Version" + } + + return $updateId +} + +function Get-FileMetadatas +{ + Param( + [ValidateCount(0, 5)] + [string[]] $FilePaths = $(throw "'FilePaths' parameter is required.") + ) + + $files = @() + + foreach ($filePath in $FilePaths) + { + if (!(Test-Path $filePath)) + { + throw "$filePath could not be found." + } + + $file = Get-Item $filePath + $fileHashes = Get-AduFileHashes $filePath + + # Server will accept any order; preserving order for aesthetics only. + $fileMap = [ordered] @{ + 'filename' = $file.Name + 'sizeInBytes' = $file.Length + 'hashes' = $fileHashes + } + + $files += $fileMap + } + + return $files +} + +# ------------------------------------------------- +# EXPORTED METHODS +# ------------------------------------------------- + +function Get-AduFileHashes +{ +<# + .SYNOPSIS + Get file hashes in a format required by ADU. + + .EXAMPLE + PS > Get-AduFileHashes -FilePath .\payload.bin +#> + [CmdletBinding()] + Param( + # Full path to the file. + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] $FilePath + ) + + if (!(Test-Path $filePath)) + { + throw "$FilePath could not be found." + } + + $FilePath = Resolve-Path $FilePath + + $fs = [System.IO.File]::OpenRead($FilePath) + $sha256 = New-Object System.Security.Cryptography.SHA256Managed + $bytes = $sha256.ComputeHash($fs) + $sha256.Dispose() + $fs.Close() + $fileHash = [System.Convert]::ToBase64String($bytes) + + $hashes = [pscustomobject]@{ + 'sha256' = $fileHash + } + + return $hashes +} + +function New-AduUpdateId +{ +<# + .SYNOPSIS + Create a new ADU update identity. + + .EXAMPLE + PS > New-AduUpdateId -Provider Contoso -Name Toaster -Version 1.0 +#> + [CmdletBinding()] + Param( + # Update provider. + [Parameter(Mandatory=$true)] + [ValidateLength(1, 64)] + [ValidatePattern("^[a-zA-Z0-9.-]+$")] + [string] $Provider, + + # Update name. + [Parameter(Mandatory=$true)] + [ValidateLength(1, 64)] + [ValidatePattern("^[a-zA-Z0-9.-]+$")] + [string] $Name, + + # Update version. + [Parameter(Mandatory=$true)] + [version] $Version + ) + + return [UpdateId]::New($Provider, $Name, $Version) +} + +function New-AduUpdateCompatibility +{ +<# + .SYNOPSIS + Create a new ADU update compatibility info. + + .EXAMPLE + PS > New-AduUpdateCompatibility -Manufacturer Contoso -Model Toaster +#> + [CmdletBinding()] + Param( + # Compatibility properties. + [Parameter(ParameterSetName='CustomProperties', Mandatory=$true)] + [hashtable] $Properties, + + # Device manufacturer. + [Parameter(ParameterSetName='BackwardCompat', Mandatory=$true)] + [ValidateLength(1, 64)] + [string] $Manufacturer, + + # Device model. + [Parameter(ParameterSetName='BackwardCompat', Mandatory=$true)] + [ValidateLength(1, 64)] + [string] $Model + ) + + switch ($PSCmdlet.ParameterSetName) + { + 'CustomProperties' { + return $Properties + } + + 'BackwardCompat' { + return @{ + manufacturer = $Manufacturer + model = $Model + } + } + } +} + +function New-AduInstallationStep +{ +<# + .SYNOPSIS + Create a new ADU installation step. + + .EXAMPLE + PS > New-AduInstallationStep -Handler 'microsoft/swupdate:1' -Files '.\file1.json', '.\file2.zip' +#> + [CmdLetBinding()] + Param( + # Step handler type in form of "{provider}/{handlerType}:{handlerTypeVersion}". + # This parameter is forwarded to client device during deployment. + # For example, reference ADU agent uses the following: + # - "microsoft/swupdate:1" for SwUpdate image-based installation step. + # - "microsoft/apt:1" for APT package-based installation step. + [Parameter(ParameterSetName = 'inline', Mandatory=$true)] + [ValidateLength(1, 32)] + [ValidatePattern("^\S+\/\S+:\d{1,5}$")] + [string] $Handler, + + # The payload filenames used for this step. + [Parameter(ParameterSetName = 'inline', Mandatory=$true)] + [ValidateCount(1, 10)] + [string[]] $Files, + + # Optional JSON object argument to step handler. + [Parameter(ParameterSetName = 'inline')] + [hashtable] $HandlerProperties, + + # Identity of child update to install for this step. + [Parameter(ParameterSetName = 'reference', Mandatory=$true)] + [UpdateId] $UpdateId, + + # Optional step description. + [ValidateLength(0, 64)] + [string] $Description + ) + + switch ($PsCmdlet.ParameterSetName) + { + 'inline' { + $step = [ordered] @{ + type = 'inline' + } + + if ($Description.Length -gt 0) + { + $step.description = $Description + } + + $step.handler = $Handler + $step.files = $Files + + if ($HandlerProperties -ne $null) + { + $step.handlerProperties = $HandlerProperties + } + + return $step + } + + 'reference' { + $step = [ordered] @{ + type = 'reference' + } + + if ($Description.Length -gt 0) + { + $step.description = $Description + } + + $step.updateId = Get-UpdateId -Provider $UpdateId.Provider -Name $UpdateId.Name -Version $UpdateId.Version + + return $step + } + } +} + +function New-AduImportManifest +{ +<# + .SYNOPSIS + Create a new ADU update import manifest. + + .EXAMPLE + PS > $updateId = New-AduUpdateId -Provider Fabrikam -Name Toaster -Version 2.0 + PS > $compatInfo1 = New-AduUpdateCompatibility -Manufacturer Fabrikam -Model Toaster + PS > $compatInfo2 = New-AduUpdateCompatibility -Properties @{ OS = "Linux"; Manufacturer = "Fabrikam" } + PS > $step = New-AduInstallationStep -Handler 'microsoft/swupdate:1' -Files '.\file1.json', '.\file2.zip' + PS > + PS > New-AduImportManifest -UpdateId $updateId -Compatibility $compatInfo1, $compatInfo2 -InstallationSteps $step +#> + [CmdletBinding()] + Param( + # Update identity created using New-AduUpdateId. + [Parameter(ParameterSetName='UpdateId', Mandatory=$true)] + [ValidateNotNull()] + [UpdateId] $UpdateId, + + # Update provider. + [Parameter(ParameterSetName='BackwardCompat', Mandatory=$true)] + [ValidateLength(1, 64)] + [ValidatePattern("^[a-zA-Z0-9.-]+$")] + [string] $Provider, + + # Update name. + [Parameter(ParameterSetName='BackwardCompat', Mandatory=$true)] + [ValidateLength(1, 64)] + [ValidatePattern("^[a-zA-Z0-9.-]+$")] + [string] $Name, + + # Update version. + [Parameter(ParameterSetName='BackwardCompat', Mandatory=$true)] + [version] $Version, + + # Optional friendly update description + [ValidateLength(0, 512)] + [string] $Description, + + # List of compatibility information of devices this update is compatible with, created using New-AduUpdateCompatibility. + [Parameter(Mandatory=$true)] + [ValidateCount(1, 10)] + [hashtable[]] $Compatibility, + + # List of update installation steps. + [Parameter(Mandatory=$true)] + [ValidateCount(1, 10)] + [System.Collections.Specialized.OrderedDictionary[]] $InstallationSteps, + + # Whether the update can be deployed on its own to a device. Must be false for a referenced update. + [bool] $IsDeployable = $true + ) + + switch ($PSCmdlet.ParameterSetName) + { + 'UpdateId' { + $id = Get-UpdateId -Provider $UpdateId.Provider -Name $UpdateId.Name -Version $UpdateId.Version + } + + 'BackwardCompat' { + $id = Get-UpdateId -Provider $Provider -Name $Name -Version $Version + } + } + + $fileMetadatas = @() + + foreach ($step in $InstallationSteps) + { + if ($step.type -eq 'inline') + { + for ($iFile = 0; $iFile -lt $step.files.Count; $iFile++) + { + $meta = Get-FileMetadatas $step.files[$iFile] + + # in case multiple steps are sharing a payload file. + if ($fileMetadatas.filename -notcontains $meta.filename) + { + $fileMetadatas += $meta + } + + # inline step requires only payload file name, convert full path to filename. + $step.files[$iFile] = $meta.filename + } + } + } + + if ($fileMetadatas.Count -gt 10) + { + Write-Error 'Update cannot have more than 10 payload files.' + } + + # Server will accept any order; preserving order for aesthetics only. + $importManifest = [ordered] @{ + updateId = $id + } + + if ($Description.Length -gt 0) + { + $importManifest.description = $Description + } + + $importManifest.isDeployable = $IsDeployable + $importManifest.compatibility = [array] $Compatibility + $importManifest.instructions = [ordered] @{ + steps = [array] $InstallationSteps + } + + if ($fileMetadatas.Length -gt 0) + { + $importManifest.files = [array] $fileMetadatas + } + + $importManifest.createdDateTime = (Get-Date).ToUniversalTime().ToString('o') # ISO8601 + $importManifest.manifestVersion = '4.0' + + ConvertTo-Json -InputObject $importManifest -Depth 20 +} + +Export-ModuleMember -Function New-AduImportManifest, New-AduUpdateId, New-AduUpdateCompatibility, New-AduInstallationStep, Get-AduFileHashes diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-1.x.ps1 b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-1.x.ps1 similarity index 87% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-1.x.ps1 rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-1.x.ps1 index fc5a321f8..c1865dfd8 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-1.x.ps1 +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-1.x.ps1 @@ -14,8 +14,8 @@ .EXAMPLE PS > CreateSampleMSOEUpdate-1.x.ps1 -UpdateProvider "Contoso" ` -UpdateName "Virtual-Vacuum" ` - -DeviceManufacturer "contoso" ` - -DeviceModel "virtual-vacuum-v1" ` + -Manufacturer "contoso" ` + -Model "virtual-vacuum-v1" ` -OutputManifestPath . #> [CmdletBinding()] @@ -31,23 +31,23 @@ Param( # Update Provider [ValidateNotNullOrEmpty()] [string] $UpdateName = "Virtual-Vacuum", - + # Device Manufacturer [ValidateNotNullOrEmpty()] - [string] $DeviceManufacturer = "contoso", + [string] $Manufacturer = "contoso", # Device Model [ValidateNotNullOrEmpty()] - [string] $DeviceModel = "virtual-vacuum-v1" + [string] $Model = "virtual-vacuum-v1" ) # ------------------------------------------------------------ -# Update : 1.0 +# Update : 0.1 # Create the parent update with bad APT Manifest file. # ------------------------------------------------------------ -$UpdateVersion = '1.0' +$UpdateVersion = '0.1' -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model $parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" $parentSteps = @() @@ -59,8 +59,9 @@ $parentFile0 = "$PSScriptRoot\data-files\APT\bad-apt-manifest-1.0.json" # ----------- # ADD STEP(S) - - # Step 1 - Simulating a failure during an install task. + + # Step 0 - Simulating a failure during an install task (for testing purposes) + # by intentionally using mal-formed APT manifest file. $parentSteps += New-AduInstallationStep ` -Handler 'microsoft/apt:1' ` -Files $parentFile0 ` @@ -93,13 +94,14 @@ Write-Host " " # --------------------------------------------------------------- -# Update 1.1 +# Update 1.0 +# # Create the parent update with good APT Manifest file. # This update installs libcurl4-doc package on the target device. # --------------------------------------------------------------- -$UpdateVersion = '1.1' +$UpdateVersion = '1.0' -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model $parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" $parentSteps = @() @@ -107,11 +109,11 @@ $parentSteps = @() Write-Host "Preparing parent update $UpdateVersion ..." $parentFile1 = "$PSScriptRoot\data-files\APT\apt-manifest-1.0.json" - + # ----------- # ADD STEP(S) - # Step 1 - APT update that installs libcurl4-doc debian package. + # Step 0 - APT update that installs 'tree'' debian package. $parentSteps += New-AduInstallationStep ` -Handler 'microsoft/apt:1' ` -Files $parentFile1 ` diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-2.x.ps1 b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-2.x.ps1 similarity index 93% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-2.x.ps1 rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-2.x.ps1 index f52c6702d..5be9768a3 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-2.x.ps1 +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-2.x.ps1 @@ -14,8 +14,8 @@ .EXAMPLE PS > CreateSampleMSOEUpdate-2.x.ps1 -UpdateProvider "Contoso" ` -UpdateName "Virtual-Vacuum" ` - -DeviceManufacturer "contoso" ` - -DeviceModel "virtual-vacuum-v1" ` + -Manufacturer "contoso" ` + -Model "virtual-vacuum-v1" ` -OutputManifestPath . #> [CmdletBinding()] @@ -31,14 +31,14 @@ Param( # Update Provider [ValidateNotNullOrEmpty()] [string] $UpdateName = "Virtual-Vacuum", - + # Device Manufacturer [ValidateNotNullOrEmpty()] - [string] $DeviceManufacturer = "contoso", + [string] $Manufacturer = "contoso", # Device Model [ValidateNotNullOrEmpty()] - [string] $DeviceModel = "virtual-vacuum-v1" + [string] $Model = "virtual-vacuum-v1" ) # ------------------------------------------------------------ @@ -47,7 +47,7 @@ Param( # ------------------------------------------------------------ $UpdateVersion = '2.0' -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model $parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" @@ -107,7 +107,7 @@ Write-Host " " # ------------------------------------------------------------ $UpdateVersion = '2.1' -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model $parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" @@ -167,7 +167,7 @@ Write-Host " " # ------------------------------------------------------------ $UpdateVersion = '2.2' -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model $parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" $parentSteps = @() diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-3.x.ps1 b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-3.x.ps1 similarity index 97% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-3.x.ps1 rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-3.x.ps1 index 904d0e88c..a81ad190d 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-3.x.ps1 +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-3.x.ps1 @@ -14,8 +14,8 @@ .EXAMPLE PS > CreateSampleMSOEUpdate-3.x.ps1 -UpdateProvider "Contoso" ` -UpdateName "Virtual-Vacuum" ` - -DeviceManufacturer "contoso" ` - -DeviceModel "virtual-vacuum-v1" ` + -Manufacturer "contoso" ` + -Model "virtual-vacuum-v1" ` -OutputManifestPath . #> [CmdletBinding()] @@ -34,11 +34,11 @@ Param( # Device Manufacturer [ValidateNotNullOrEmpty()] - [string] $DeviceManufacturer = "contoso", + [string] $Manufacturer = "contoso", # Device Model [ValidateNotNullOrEmpty()] - [string] $DeviceModel = "virtual-vacuum-v1" + [string] $Model = "virtual-vacuum-v1" ) ############################################################## @@ -50,7 +50,7 @@ Param( $UpdateVersion = '3.0' -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model $parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" @@ -199,7 +199,7 @@ Write-Host " " ############################################################## $UpdateVersion = '3.1' -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model $parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" @@ -346,7 +346,7 @@ Write-Host " " ############################################################## $UpdateVersion = '3.2' -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model $parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" @@ -494,7 +494,7 @@ Write-Host " " ############################################################## $UpdateVersion = '3.3' -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model $parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-4.x.ps1 b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-4.x.ps1 similarity index 95% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-4.x.ps1 rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-4.x.ps1 index cb45a7a68..bb40e2fb0 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-4.x.ps1 +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-4.x.ps1 @@ -14,8 +14,8 @@ .EXAMPLE PS > CreateSampleMSOEUpdate-4.x.ps1 -UpdateProvider "Contoso" ` -UpdateName "Virtual-Vacuum" ` - -DeviceManufacturer "contoso" ` - -DeviceModel "virtual-vacuum-v1" ` + -Manufacturer "contoso" ` + -Model "virtual-vacuum-v1" ` -OutputManifestPath . #> [CmdletBinding()] @@ -34,11 +34,11 @@ Param( # Device Manufacturer [ValidateNotNullOrEmpty()] - [string] $DeviceManufacturer = "contoso", + [string] $Manufacturer = "contoso", # Device Model [ValidateNotNullOrEmpty()] - [string] $DeviceModel = "virtual-vacuum-v1" + [string] $Model = "virtual-vacuum-v1" ) ############################################################## @@ -51,7 +51,7 @@ Param( $UpdateVersion = '4.0' -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model $parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-5.x.ps1 b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-5.x.ps1 similarity index 97% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-5.x.ps1 rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-5.x.ps1 index 513ab2928..c21ea4338 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-5.x.ps1 +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-5.x.ps1 @@ -14,8 +14,8 @@ .EXAMPLE PS > CreateSampleMSOEUpdate-5.x.ps1 -UpdateProvider "Contoso" ` -UpdateName "Virtual-Vacuum" ` - -DeviceManufacturer "contoso" ` - -DeviceModel "virtual-vacuum-v1" ` + -Manufacturer "contoso" ` + -Model "virtual-vacuum-v1" ` -OutputManifestPath . #> [CmdletBinding()] @@ -34,11 +34,11 @@ Param( # Device Manufacturer [ValidateNotNullOrEmpty()] - [string] $DeviceManufacturer = "contoso", + [string] $Manufacturer = "contoso", # Device Model [ValidateNotNullOrEmpty()] - [string] $DeviceModel = "virtual-vacuum-v1" + [string] $Model = "virtual-vacuum-v1" ) ############################################################## @@ -52,7 +52,7 @@ Param( $UpdateVersion = '5.0' -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model $parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-7.x.ps1 b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-6.x.ps1 similarity index 80% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-7.x.ps1 rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-6.x.ps1 index 9e055a96e..560fe21b2 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-7.x.ps1 +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-6.x.ps1 @@ -12,10 +12,10 @@ Create a sample updates for testing purposes. .EXAMPLE - PS > CreateSampleMSOEUpdate-7.x.ps1 -UpdateProvider "Contoso" ` + PS > CreateSampleMSOEUpdate-6.x.ps1 -UpdateProvider "Contoso" ` -UpdateName "Virtual-Vacuum" ` - -DeviceManufacturer "contoso" ` - -DeviceModel "virtual-vacuum-v1" ` + -Manufacturer "contoso" ` + -Model "virtual-vacuum-v1" ` -OutputManifestPath . #> [CmdletBinding()] @@ -34,64 +34,65 @@ Param( # Device Manufacturer [ValidateNotNullOrEmpty()] - [string] $DeviceManufacturer = "contoso", + [string] $Manufacturer = "contoso", # Device Model [ValidateNotNullOrEmpty()] - [string] $DeviceModel = "virtual-vacuum-v1" + [string] $Model = "virtual-vacuum-v1" ) ############################################################## -# Update : 7.0 +# Update : 8.0 # # Create a parent update that updates 'motors', 'cameras' # - Each child update contains 3 inline steps. ############################################################## -$UpdateVersion = '7.0' +$UpdateVersion = '8.0' -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model $parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" $RefUpdateNamePrefix = $UpdateName Write-Host "Preparing update $parentUpdateIdStr ..." - # ------------------------------------------------- - # Create a child update for all 'motor' components + # Create a child update for hostfw # ------------------------------------------------- - $RefUpdateManufacturer = "contoso" - $RefUpdateName = "$RefUpdateNamePrefix-virtual-motors" - $RefUpdateVersion = "7.0" + $RefUpdateName = "$RefUpdateNamePrefix-hostfw" + $RefUpdateVersion = "1.1" - $motorsFirmwareVersion = "1.2" + $hostfwFirmwareVersion = "1.1" Write-Host "Preparing child update ($RefUpdateManufacturer/$RefUpdateName/$RefUpdateVersion)..." - $motorsUpdateId = New-AduUpdateId -Provider $RefUpdateManufacturer -Name $RefUpdateName -Version $RefUpdateVersion + $hostfwUpdateId = New-AduUpdateId -Provider $RefUpdateManufacturer -Name $RefUpdateName -Version $RefUpdateVersion # This components update only apply to 'motors' group. - $motorsSelector = @{ group = 'motors' } - $motorsCompat = New-AduUpdateCompatibility -Properties $motorsSelector + $hostfwSelector = @{ group = 'hostfw' } + $hostfwCompat = New-AduUpdateCompatibility -Properties $hostfwSelector - $motorScriptFile = "$PSScriptRoot\scripts\contoso-motor-installscript.sh" - $motorFirmwareFile = "$PSScriptRoot\data-files\motor-firmware-$motorsFirmwareVersion.json" + $hostfwScriptFile = "$PSScriptRoot\scripts\contoso-firmware-installscript.sh" + $hostfwFirmwareFile = "$PSScriptRoot\data-files\host-firmware-$hostfwFirmwareVersion.json" + + $steamerScriptFile = "$PSScriptRoot\scripts\contoso-steamer-installscript.sh" + $steamerFirmwareFile = "$PSScriptRoot\data-files\steamer-firmware-$steamersFirmwareVersion.json" #------------ # ADD STEP(S) # # This update contains 3 steps. - $motorsInstallSteps = @() + $hostfwInstallSteps = @() - # Step #1 - simulating a success pre-install task. - $motorsInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` - -Files $motorScriptFile ` + # Step #1 - install host firmware and reboot + $hostfwInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` + -Files $hostfwScriptFile, $hostfwFirmwareFile ` -HandlerProperties @{ ` - 'scriptFileName'='contoso-motor-installscript.sh'; ` - 'installedCriteria'="$motorsFirmwareVersion"; ` + 'scriptFileName'='contoso-steamer-installscript.sh'; ` + 'installedCriteria'="$steamersFirmwareVersion"; ` 'arguments'="--pre-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` } ` ` @@ -101,9 +102,9 @@ Write-Host "Preparing update $parentUpdateIdStr ..." $motorsInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` -Files $motorScriptFile, $motorFirmwareFile ` -HandlerProperties @{ ` - 'scriptFileName'='contoso-motor-installscript.sh'; ` - 'installedCriteria'="$motorsFirmwareVersion"; ` - "arguments"="--firmware-file motor-firmware-$motorsFirmwareVersion.json --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` + 'scriptFileName'='contoso-steamer-installscript.sh'; ` + 'installedCriteria'="$steamersFirmwareVersion"; ` + "arguments"="--firmware-file steamer-firmware-$steamersFirmwareVersion.json --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` } ` ` -Description 'Motors Update - firmware installation' @@ -112,8 +113,8 @@ Write-Host "Preparing update $parentUpdateIdStr ..." $motorsInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` -Files $motorScriptFile ` -HandlerProperties @{ ` - 'scriptFileName'='contoso-motor-installscript.sh'; ` - 'installedCriteria'="$motorsFirmwareVersion"; ` + 'scriptFileName'='contoso-steamer-installscript.sh'; ` + 'installedCriteria'="$steamersFirmwareVersion"; ` "arguments"="--post-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` } ` ` @@ -158,9 +159,9 @@ Write-Host "Preparing update $parentUpdateIdStr ..." $RefUpdateManufacturer = "contoso" $RefUpdateName = "$RefUpdateNamePrefix-virtual-cameras" - $RefUpdateVersion = "7.0" + $RefUpdateVersion = "8.0" - $camerasFirmwareVersion = "2.1" + $camerasFirmwareVersion = "3.0" Write-Host "Preparing child update ($RefUpdateManufacturer/$RefUpdateName/$RefUpdateVersion)..." @@ -185,7 +186,7 @@ Write-Host "Preparing update $parentUpdateIdStr ..." -Files $cameraScriptFile ` -HandlerProperties @{ ` 'scriptFileName'='contoso-camera-installscript.sh'; ` - 'installedCriteria'="$camerasFirmwareVersion"; ` + 'installedCriteria'="$RefUpdateVersion"; ` 'arguments'="--pre-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` } ` ` @@ -196,7 +197,7 @@ Write-Host "Preparing update $parentUpdateIdStr ..." -Files $cameraScriptFile, $cameraFirmwareFile ` -HandlerProperties @{ ` 'scriptFileName'='contoso-camera-installscript.sh'; ` - 'installedCriteria'="$camerasFirmwareVersion"; ` + 'installedCriteria'="$RefUpdateVersion"; ` "arguments"="--firmware-file camera-firmware-$camerasFirmwareVersion.json --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` } ` ` @@ -207,13 +208,13 @@ Write-Host "Preparing update $parentUpdateIdStr ..." -Files $cameraScriptFile ` -HandlerProperties @{ ` 'scriptFileName'='contoso-camera-installscript.sh'; ` - 'installedCriteria'="$camerasFirmwareVersion"; ` + 'installedCriteria'="$RefUpdateVersion"; ` "arguments"="--post-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` } ` ` -Description 'Cameras Update post-install step' - # ------------------------------ + # ------------------------------ # Create child update manifest # ------------------------------ @@ -254,6 +255,7 @@ $parentSteps = @() #------------ # ADD STEP(s) + $parentSteps += New-AduInstallationStep -UpdateId $hostfwUpdateId -Description "Host Firmware Version $hostfwFirmwareVersion" $parentSteps += New-AduInstallationStep -UpdateId $motorsUpdateId -Description "Motors Firmware Version $motorsFirmwareVersion" $parentSteps += New-AduInstallationStep -UpdateId $camerasUpdateId -Description "Cameras Firmware Version $camerasFirmwareVersion" diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-10.x.ps1 b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-7.x.ps1 similarity index 63% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-10.x.ps1 rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-7.x.ps1 index 0881bdff0..37b004715 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/CreateSampleMSOEUpdate-10.x.ps1 +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/CreateSampleMSOEUpdate-7.x.ps1 @@ -12,10 +12,10 @@ Create a sample updates for testing purposes. .EXAMPLE - PS > CreateSampleMSOEUpdate-1.x.ps1 -UpdateProvider "Contoso" ` + PS > CreateSampleMSOEUpdate-7.x.ps1 -UpdateProvider "Contoso" ` -UpdateName "Virtual-Vacuum" ` - -DeviceManufacturer "contoso" ` - -DeviceModel "virtual-vacuum-v1" ` + -Manufacturer "contoso" ` + -Model "virtual-vacuum-v1" ` -OutputManifestPath . #> [CmdletBinding()] @@ -31,26 +31,222 @@ Param( # Update Provider [ValidateNotNullOrEmpty()] [string] $UpdateName = "Virtual-Vacuum", - + # Device Manufacturer [ValidateNotNullOrEmpty()] - [string] $DeviceManufacturer = "contoso", + [string] $Manufacturer = "contoso", # Device Model [ValidateNotNullOrEmpty()] - [string] $DeviceModel = "virtual-vacuum-v1" + [string] $Model = "virtual-vacuum-v1" ) # ------------------------------------------------------------------------- -# Update : 10.0 +# Update : 7.0 # Create the parent update with very large content. # The update manifest will be delivered using detached update manifest file. # -------------------------------------------------------------------------- -$UpdateVersion = '10.0' +$UpdateVersion = '7.0' -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model $parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" + +$RefUpdateNamePrefix = $UpdateName + +Write-Host "Preparing update $parentUpdateIdStr ..." + + # ------------------------------------------------- + # Create a child update for all 'motor' components + # ------------------------------------------------- + + $RefUpdateManufacturer = "contoso" + $RefUpdateName = "$RefUpdateNamePrefix-virtual-motors" + $RefUpdateVersion = "7.0" + + $motorsFirmwareVersion = "1.2" + + Write-Host "Preparing child update ($RefUpdateManufacturer/$RefUpdateName/$RefUpdateVersion)..." + + $motorsUpdateId = New-AduUpdateId -Provider $RefUpdateManufacturer -Name $RefUpdateName -Version $RefUpdateVersion + + # This components update only apply to 'motors' group. + $motorsSelector = @{ group = 'motors' } + $motorsCompat = New-AduUpdateCompatibility -Properties $motorsSelector + + $motorScriptFile = "$PSScriptRoot\scripts\contoso-motor-installscript.sh" + $motorFirmwareFile = "$PSScriptRoot\data-files\motor-firmware-$motorsFirmwareVersion.json" + + #------------ + # ADD STEP(S) + # + + # This update contains 3 steps. + $motorsInstallSteps = @() + + # Step #1 - simulating a success pre-install task. + $motorsInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` + -Files $motorScriptFile ` + -HandlerProperties @{ ` + 'scriptFileName'='contoso-motor-installscript.sh'; ` + 'installedCriteria'="$motorsFirmwareVersion"; ` + 'arguments'="--pre-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` + } ` + ` + -Description 'Motors Update pre-install step' + + # Step #2 - install a mock firmware version 1.2 onto motor component. + $motorsInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` + -Files $motorScriptFile, $motorFirmwareFile ` + -HandlerProperties @{ ` + 'scriptFileName'='contoso-motor-installscript.sh'; ` + 'installedCriteria'="$motorsFirmwareVersion"; ` + "arguments"="--firmware-file motor-firmware-$motorsFirmwareVersion.json --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` + } ` + ` + -Description 'Motors Update - firmware installation' + + # Step #3 - simulating a success post-install task. + $motorsInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` + -Files $motorScriptFile ` + -HandlerProperties @{ ` + 'scriptFileName'='contoso-motor-installscript.sh'; ` + 'installedCriteria'="$motorsFirmwareVersion"; ` + "arguments"="--post-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` + } ` + ` + -Description 'Motors Update post-install step' + + # ------------------------------ + # Create child update manifest + # ------------------------------ + + $childUpdateId = $motorsUpdateId + $childUpdateIdStr = "$($childUpdateId.Provider).$($childUpdateId.Name).$($childUpdateId.Version)" + $childPayloadFiles = $motorScriptFile, $motorFirmwareFile + $childCompat = $motorsCompat + $childSteps = $motorsInstallSteps + + Write-Host " Preparing child update manifest $childUpdateIdStr ..." + + $childManifest = New-AduImportManifest -UpdateId $childUpdateId -IsDeployable $false ` + -Compatibility $childCompat ` + -InstallationSteps $childSteps ` + -ErrorAction Stop + + # Create folder for manifest files and payload. + $outputPath = "$OutputManifestPath\$parentUpdateIdStr" + Write-Host " Saving child manifest files and payload to $outputPath..." + New-Item $outputPath -ItemType Directory -ErrorAction SilentlyContinue | Out-Null + + # Generate manifest files. + $childManifest | Out-File "$outputPath\$childUpdateIdStr.importmanifest.json" -Encoding utf8 + + # Copy all payloads (if used) + Copy-Item -Path $childPayloadFiles -Destination $outputPath -Force + + Write-Host " " + + + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + # ------------------------------------------------- + # Create a child update for all 'camera' components + # ------------------------------------------------- + + $RefUpdateManufacturer = "contoso" + $RefUpdateName = "$RefUpdateNamePrefix-virtual-cameras" + $RefUpdateVersion = "7.0" + + $camerasFirmwareVersion = "2.1" + + Write-Host "Preparing child update ($RefUpdateManufacturer/$RefUpdateName/$RefUpdateVersion)..." + + $camerasUpdateId = New-AduUpdateId -Provider $RefUpdateManufacturer -Name $RefUpdateName -Version $RefUpdateVersion + + # This components update only apply to 'cameras' group. + $camerasSelector = @{ group = 'cameras' } + $camerasCompat = New-AduUpdateCompatibility -Properties $camerasSelector + + $cameraScriptFile = "$PSScriptRoot\scripts\contoso-camera-installscript.sh" + $cameraFirmwareFile = "$PSScriptRoot\data-files\camera-firmware-$camerasFirmwareVersion.json" + + #------------ + # ADD STEP(S) + # + + # This update contains 3 steps. + $camerasInstallSteps = @() + + # Step #1 - simulating a success pre-install task. + $camerasInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` + -Files $cameraScriptFile ` + -HandlerProperties @{ ` + 'scriptFileName'='contoso-camera-installscript.sh'; ` + 'installedCriteria'="$camerasFirmwareVersion"; ` + 'arguments'="--pre-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` + } ` + ` + -Description 'Cameras Update pre-install step' + + # Step #2 - install a mock firmware version 2.1 onto camera component. + $camerasInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` + -Files $cameraScriptFile, $cameraFirmwareFile ` + -HandlerProperties @{ ` + 'scriptFileName'='contoso-camera-installscript.sh'; ` + 'installedCriteria'="$camerasFirmwareVersion"; ` + "arguments"="--firmware-file camera-firmware-$camerasFirmwareVersion.json --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` + } ` + ` + -Description 'Cameras Update - firmware installation' + + # Step #3 - simulating a success post-install task. + $camerasInstallSteps += New-AduInstallationStep -Handler 'microsoft/script:1' ` + -Files $cameraScriptFile ` + -HandlerProperties @{ ` + 'scriptFileName'='contoso-camera-installscript.sh'; ` + 'installedCriteria'="$camerasFirmwareVersion"; ` + "arguments"="--post-install-sim-success --component-name --component-name-val --component-group --component-group-val --component-prop path --component-prop-val path" ` + } ` + ` + -Description 'Cameras Update post-install step' + + # ------------------------------ + # Create child update manifest + # ------------------------------ + + $childUpdateId = $camerasUpdateId + $childUpdateIdStr = "$($childUpdateId.Provider).$($childUpdateId.Name).$($childUpdateId.Version)" + $childPayloadFiles = $cameraScriptFile, $cameraFirmwareFile + $childCompat = $camerasCompat + $childSteps = $camerasInstallSteps + + Write-Host " Preparing child update manifest $childUpdateIdStr ..." + + $childManifest = New-AduImportManifest -UpdateId $childUpdateId -IsDeployable $false ` + -Compatibility $childCompat ` + -InstallationSteps $childSteps ` + -ErrorAction Stop + + # Create folder for manifest files and payload. + $outputPath = "$OutputManifestPath\$parentUpdateIdStr" + Write-Host " Saving child manifest files and payload to $outputPath..." + New-Item $outputPath -ItemType Directory -ErrorAction SilentlyContinue | Out-Null + + # Generate manifest files. + $childManifest | Out-File "$outputPath\$childUpdateIdStr.importmanifest.json" -Encoding utf8 + + # Copy all payloads (if used) + Copy-Item -Path $childPayloadFiles -Destination $outputPath -Force + + Write-Host " " + +# ---------------------------- +# Create the parent update +# ---------------------------- +Write-Host " Preparing parent update $parentUpdateIdStr ..." + +$payloadFiles = $parentSteps = @() Write-Host "Preparing parent update $UpdateVersion ..." @@ -60,7 +256,7 @@ $parentFile0 = "$PSScriptRoot\data-files\APT\apt-manifest-1.0.json" # ----------- # ADD STEP(S) - + # Step 1 - Simulating a failure during an install task. $parentSteps += New-AduInstallationStep ` -Handler 'microsoft/apt:1' ` @@ -99,7 +295,7 @@ $parentFile0 = "$PSScriptRoot\data-files\APT\apt-manifest-1.0.json" 'data30'='Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' ` } ` ` - -Description 'Update with deteached Update Manifest file.' + -Description 'Update with detached Update Manifest file.' # ------------------------------ # Create parent update manifest Write-Host " Generating an import manifest $parentUpdateIdStr..." diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/APT/apt-manifest-1.0.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/APT/apt-manifest-1.0.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/APT/apt-manifest-1.0.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/APT/apt-manifest-1.0.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/APT/apt-manifest-4.0.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/APT/apt-manifest-4.0.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/APT/apt-manifest-4.0.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/APT/apt-manifest-4.0.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/APT/apt-manifest-tree-1.0.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/APT/apt-manifest-tree-1.0.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/APT/apt-manifest-tree-1.0.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/APT/apt-manifest-tree-1.0.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/APT/bad-apt-manifest-1.0.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/APT/bad-apt-manifest-1.0.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/APT/bad-apt-manifest-1.0.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/APT/bad-apt-manifest-1.0.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/camera-firmware-1.1.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/camera-firmware-1.1.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/camera-firmware-1.1.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/camera-firmware-1.1.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/camera-firmware-2.1.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/camera-firmware-2.1.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/camera-firmware-2.1.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/camera-firmware-2.1.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/camera-firmware-2.2.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/camera-firmware-2.2.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/camera-firmware-2.2.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/camera-firmware-2.2.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/camera-firmware-3.0.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/camera-firmware-3.0.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/camera-firmware-3.0.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/camera-firmware-3.0.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/contoso-vacuum-apt-1.0.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/contoso-vacuum-apt-1.0.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/contoso-vacuum-apt-1.0.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/contoso-vacuum-apt-1.0.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/host-firmware-1.1.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/host-firmware-1.1.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/host-firmware-1.1.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/host-firmware-1.1.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/host-firmware-1.2.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/host-firmware-1.2.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/host-firmware-1.2.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/host-firmware-1.2.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/host-firmware-1.3.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/host-firmware-1.3.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/host-firmware-1.3.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/host-firmware-1.3.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/motor-firmware-1.1.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/motor-firmware-1.1.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/motor-firmware-1.1.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/motor-firmware-1.1.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/motor-firmware-1.2.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/motor-firmware-1.2.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/motor-firmware-1.2.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/motor-firmware-1.2.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/motor-firmware-1.3.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/motor-firmware-1.3.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/motor-firmware-1.3.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/motor-firmware-1.3.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/motor-firmware-1.4.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/motor-firmware-1.4.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/motor-firmware-1.4.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/motor-firmware-1.4.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/motor-firmware-1.5.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/motor-firmware-1.5.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/motor-firmware-1.5.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/motor-firmware-1.5.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/motor-firmware-3.1.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/motor-firmware-3.1.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/motor-firmware-3.1.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/motor-firmware-3.1.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/steamer-firmware-1.0.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/steamer-firmware-1.0.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/steamer-firmware-1.0.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/steamer-firmware-1.0.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/steamer-firmware-2.0.json b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/steamer-firmware-2.0.json similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/data-files/steamer-firmware-2.0.json rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/data-files/steamer-firmware-2.0.json diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-camera-installscript.sh b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-camera-installscript.sh similarity index 92% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-camera-installscript.sh rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-camera-installscript.sh index 48176f1e5..ab08c69ab 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-camera-installscript.sh +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-camera-installscript.sh @@ -10,9 +10,9 @@ ret_val=0 # Ensure we dont end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi # Output formatting. @@ -106,61 +106,60 @@ PARAMS= # _timestamp= -update_timestamp() -{ +update_timestamp() { # See https://man7.org/linux/man-pages/man1/date.1.html _timestamp="$(date +'%Y/%m/%d:%H%M%S')" } -log_debug(){ - if [ $log_level -gt 0 ]; then +log_debug() { + if [ $log_level -gt 0 ]; then return fi log "$log_debug_pref" "$@" } -log_info(){ - if [ $log_level -gt 1 ]; then +log_info() { + if [ $log_level -gt 1 ]; then return fi log "$log_info_pref" "$@" } -log_warn(){ - if [ $log_level -gt 2 ]; then +log_warn() { + if [ $log_level -gt 2 ]; then return fi log "$log_warn_pref" "$@" } -log_error(){ - if [ $log_level -gt 3 ]; then +log_error() { + if [ $log_level -gt 3 ]; then return fi log "$log_error_pref" "$@" } -log(){ +log() { update_timestamp - if [ -z $log_file ]; then + if [ -z "$log_file" ]; then echo -e "[$_timestamp]" "$@" >&1 else - echo "[$_timestamp]" "$@" >> $log_file + echo "[$_timestamp]" "$@" >> "$log_file" fi } -output(){ +output() { update_timestamp - if [ -z $output_file ]; then + if [ -z "$output_file" ]; then echo "[$_timestamp]" "$@" >&1 else echo "[$_timestamp]" "$@" >> "$output_file" fi } -result(){ - # NOTE: dont' insert timestamp in result file. - if [ -z $result_file ]; then +result() { + # NOTE: don't insert timestamp in result file. + if [ -z "$result_file" ]; then echo "$@" >&1 else echo "$@" > "$result_file" @@ -188,7 +187,7 @@ print_help() { echo "Device Update reserved argument" echo "===============================" echo "" - echo "--action-isinstalled Perform 'is-installed' check." + echo "--action-is-installed Perform 'is-installed' check." echo " Check whether the selected component [or primary device] current states" echo " satisfies specified 'installedCriteria' data." echo "--installed-criteria Specify the Installed-Criteria string." @@ -201,7 +200,7 @@ print_help() { echo "--restart-to-apply Request the host device to restart when applying update to this component." echo "--restart-agent-to-apply Request the DU Agent to restart when applying update to this component." echo "" - echo "--install-error-policy Indicates how to proceed when an error occurs. Default \"abort\"" + echo '--install-error-policy Indicates how to proceed when an error occurs. Default "abort"' echo " options: abort, continue" echo "--install-reboot-policy Indicates whether a device reboot is required, after install completed successfully." echo " Optios:" @@ -434,7 +433,7 @@ while [[ $1 != "" ]]; do error "--firmware-file parameter is mandatory." $ret 1 fi - firmware_file="$1"; + firmware_file="$1" echo "firmware file: $firmware_file" shift ;; @@ -445,7 +444,7 @@ while [[ $1 != "" ]]; do error "--work-folder parameter is mandatory." $ret 1 fi - workfolder="$1"; + workfolder="$1" shift ;; @@ -460,7 +459,7 @@ while [[ $1 != "" ]]; do error "--out-file parameter is mandatory." $ret 1 fi - output_file="$1"; + output_file="$1" # #Create output file path. @@ -481,7 +480,7 @@ while [[ $1 != "" ]]; do error "--result-file parameter is mandatory." $ret 1 fi - result_file="$1"; + result_file="$1" # #Create result file path. # @@ -500,7 +499,7 @@ while [[ $1 != "" ]]; do error "--log-file parameter is mandatory." $ret 1 fi - log_file="$1"; + log_file="$1" shift ;; --log-level) @@ -554,7 +553,7 @@ done # Example implementation of 'IsInstalled' function, for contoso-motor component. # # Design Goal: -# Determine whether the spcifified 'installedCriteria' (parameter $1) is met. +# Determine whether the specified 'installedCriteria' (parameter $1) is met. # # 'installedCriteria' is a version number of a motor in a mock component's data file # located at "$component_props['path']/firmware.json". @@ -577,7 +576,7 @@ done # ADUC_Result_IsInstalled_Installed = 900, /**< Succeeded and content is installed. */ # ADUC_Result_IsInstalled_NotInstalled = 901, /**< Succeeded and content is not installed */ # -IsInstalled(){ +IsInstalled() { path_key=path component_path=${component_props[$path_key]} component_file_path="$component_path/firmware.json" @@ -592,7 +591,7 @@ IsInstalled(){ # extendedResultCode(12345) mock value. extendedResultCode=12345 resultDetails="Invalid installedCriteria value." - elif [[ -f "$component_file_path" ]]; then + elif [[ -f $component_file_path ]]; then log_debug "Found component data file '$component_file_path'." @@ -651,7 +650,7 @@ IsInstalled(){ output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" } @@ -659,7 +658,7 @@ IsInstalled(){ # Helper function # Not returning ADUC_RESULT # -ResetComponent(){ +ResetComponent() { path_key=path component_path=${component_props[$path_key]} component_file_path="$component_path/firmware.json" @@ -688,17 +687,17 @@ ResetComponent(){ o="$component_file_path" { - echo "{"; - echo " \"id\": \"$component_id\"," ; - echo " \"name\": \"$component_name\"," ; - echo " \"group\": \"$component_group\"," ; - echo " \"manufacturer\": \"$component_vendor\"," ; - echo " \"model\": \"$component_model\"," ; - echo " \"version\": \"$component_version\"," ; - echo " \"description\": \"This component is generated for testing purposes.\"," ; - echo " \"properties\": {" ; - echo " \"path\": \"$component_path\"" ; - echo " }" ; + echo "{" + echo " \"id\": \"$component_id\"," + echo " \"name\": \"$component_name\"," + echo " \"group\": \"$component_group\"," + echo " \"manufacturer\": \"$component_vendor\"," + echo " \"model\": \"$component_model\"," + echo " \"version\": \"$component_version\"," + echo ' "description": "This component is generated for testing purposes.",' + echo ' "properties": {' + echo " \"path\": \"$component_path\"" + echo " }" echo "}" } > "$o" @@ -736,7 +735,7 @@ DownloadUpdateArtifacts() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -761,8 +760,8 @@ InstallUpdate() { if [ "$installed_criteria" == "" ]; then log_error "Script call missing '--installed-criteria' argument." resultCode=0 - # ADUC_ERC_SCRIPT_HANDLER_MISSING_INSTALLED_CRITERIA (0x30600002) - extendedResultCode=$((0x30600002)) + # ADUC_ERC_SCRIPT_HANDLER_MISSING_INSTALLED_CRITERIA (0x30500002) + extendedResultCode=$((0x30500002)) resultDetails="Script call missing '--installed-criteria' argument." else @@ -796,8 +795,8 @@ InstallUpdate() { # Set result code and details resultCode=0 - # ADUC_ERC_SCRIPT_HANDLER_CHILD_PROCESS_FAILURE_EXITCODE(exitCode) (0x30601000 + exitCode) - extendedResultCode=$((0x30601000 + copy_ret)) + # ADUC_ERC_SCRIPT_HANDLER_CHILD_PROCESS_FAILURE_EXITCODE(exitCode) (0x30501000 + exitCode) + extendedResultCode=$((0x30501000 + copy_ret)) resultDetails="Firmware installation failed. ComponentId: $component_id" else log_info "Install succeeded." @@ -819,7 +818,7 @@ InstallUpdate() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -836,7 +835,7 @@ ApplyUpdate() { # ADUC_Result_Apply_Success = 700 resultCode=700 fi - # No additional error. + # No additional error. extendedResultCode=0 resultDetails= # Prepare ADUC_Result json. @@ -846,7 +845,7 @@ ApplyUpdate() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -866,13 +865,13 @@ SimulatePreInstallSuccess() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } SimulatePostInstallSuccess() { -output "Simulating post-instll step success." + output "Simulating post-instll step success." ret_val=0 # ADUC_Result_Apply_Success = 700 resultCode=700 @@ -886,12 +885,12 @@ output "Simulating post-instll step success." output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } -CancelUpdate(){ +CancelUpdate() { ret_val=0 $ret $ret_val } @@ -941,4 +940,3 @@ if [ -n "$do_cancel_action" ]; then fi $ret $ret_val - diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-diskimage-installscript.sh b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-diskimage-installscript.sh similarity index 92% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-diskimage-installscript.sh rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-diskimage-installscript.sh index 48176f1e5..ab08c69ab 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-diskimage-installscript.sh +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-diskimage-installscript.sh @@ -10,9 +10,9 @@ ret_val=0 # Ensure we dont end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi # Output formatting. @@ -106,61 +106,60 @@ PARAMS= # _timestamp= -update_timestamp() -{ +update_timestamp() { # See https://man7.org/linux/man-pages/man1/date.1.html _timestamp="$(date +'%Y/%m/%d:%H%M%S')" } -log_debug(){ - if [ $log_level -gt 0 ]; then +log_debug() { + if [ $log_level -gt 0 ]; then return fi log "$log_debug_pref" "$@" } -log_info(){ - if [ $log_level -gt 1 ]; then +log_info() { + if [ $log_level -gt 1 ]; then return fi log "$log_info_pref" "$@" } -log_warn(){ - if [ $log_level -gt 2 ]; then +log_warn() { + if [ $log_level -gt 2 ]; then return fi log "$log_warn_pref" "$@" } -log_error(){ - if [ $log_level -gt 3 ]; then +log_error() { + if [ $log_level -gt 3 ]; then return fi log "$log_error_pref" "$@" } -log(){ +log() { update_timestamp - if [ -z $log_file ]; then + if [ -z "$log_file" ]; then echo -e "[$_timestamp]" "$@" >&1 else - echo "[$_timestamp]" "$@" >> $log_file + echo "[$_timestamp]" "$@" >> "$log_file" fi } -output(){ +output() { update_timestamp - if [ -z $output_file ]; then + if [ -z "$output_file" ]; then echo "[$_timestamp]" "$@" >&1 else echo "[$_timestamp]" "$@" >> "$output_file" fi } -result(){ - # NOTE: dont' insert timestamp in result file. - if [ -z $result_file ]; then +result() { + # NOTE: don't insert timestamp in result file. + if [ -z "$result_file" ]; then echo "$@" >&1 else echo "$@" > "$result_file" @@ -188,7 +187,7 @@ print_help() { echo "Device Update reserved argument" echo "===============================" echo "" - echo "--action-isinstalled Perform 'is-installed' check." + echo "--action-is-installed Perform 'is-installed' check." echo " Check whether the selected component [or primary device] current states" echo " satisfies specified 'installedCriteria' data." echo "--installed-criteria Specify the Installed-Criteria string." @@ -201,7 +200,7 @@ print_help() { echo "--restart-to-apply Request the host device to restart when applying update to this component." echo "--restart-agent-to-apply Request the DU Agent to restart when applying update to this component." echo "" - echo "--install-error-policy Indicates how to proceed when an error occurs. Default \"abort\"" + echo '--install-error-policy Indicates how to proceed when an error occurs. Default "abort"' echo " options: abort, continue" echo "--install-reboot-policy Indicates whether a device reboot is required, after install completed successfully." echo " Optios:" @@ -434,7 +433,7 @@ while [[ $1 != "" ]]; do error "--firmware-file parameter is mandatory." $ret 1 fi - firmware_file="$1"; + firmware_file="$1" echo "firmware file: $firmware_file" shift ;; @@ -445,7 +444,7 @@ while [[ $1 != "" ]]; do error "--work-folder parameter is mandatory." $ret 1 fi - workfolder="$1"; + workfolder="$1" shift ;; @@ -460,7 +459,7 @@ while [[ $1 != "" ]]; do error "--out-file parameter is mandatory." $ret 1 fi - output_file="$1"; + output_file="$1" # #Create output file path. @@ -481,7 +480,7 @@ while [[ $1 != "" ]]; do error "--result-file parameter is mandatory." $ret 1 fi - result_file="$1"; + result_file="$1" # #Create result file path. # @@ -500,7 +499,7 @@ while [[ $1 != "" ]]; do error "--log-file parameter is mandatory." $ret 1 fi - log_file="$1"; + log_file="$1" shift ;; --log-level) @@ -554,7 +553,7 @@ done # Example implementation of 'IsInstalled' function, for contoso-motor component. # # Design Goal: -# Determine whether the spcifified 'installedCriteria' (parameter $1) is met. +# Determine whether the specified 'installedCriteria' (parameter $1) is met. # # 'installedCriteria' is a version number of a motor in a mock component's data file # located at "$component_props['path']/firmware.json". @@ -577,7 +576,7 @@ done # ADUC_Result_IsInstalled_Installed = 900, /**< Succeeded and content is installed. */ # ADUC_Result_IsInstalled_NotInstalled = 901, /**< Succeeded and content is not installed */ # -IsInstalled(){ +IsInstalled() { path_key=path component_path=${component_props[$path_key]} component_file_path="$component_path/firmware.json" @@ -592,7 +591,7 @@ IsInstalled(){ # extendedResultCode(12345) mock value. extendedResultCode=12345 resultDetails="Invalid installedCriteria value." - elif [[ -f "$component_file_path" ]]; then + elif [[ -f $component_file_path ]]; then log_debug "Found component data file '$component_file_path'." @@ -651,7 +650,7 @@ IsInstalled(){ output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" } @@ -659,7 +658,7 @@ IsInstalled(){ # Helper function # Not returning ADUC_RESULT # -ResetComponent(){ +ResetComponent() { path_key=path component_path=${component_props[$path_key]} component_file_path="$component_path/firmware.json" @@ -688,17 +687,17 @@ ResetComponent(){ o="$component_file_path" { - echo "{"; - echo " \"id\": \"$component_id\"," ; - echo " \"name\": \"$component_name\"," ; - echo " \"group\": \"$component_group\"," ; - echo " \"manufacturer\": \"$component_vendor\"," ; - echo " \"model\": \"$component_model\"," ; - echo " \"version\": \"$component_version\"," ; - echo " \"description\": \"This component is generated for testing purposes.\"," ; - echo " \"properties\": {" ; - echo " \"path\": \"$component_path\"" ; - echo " }" ; + echo "{" + echo " \"id\": \"$component_id\"," + echo " \"name\": \"$component_name\"," + echo " \"group\": \"$component_group\"," + echo " \"manufacturer\": \"$component_vendor\"," + echo " \"model\": \"$component_model\"," + echo " \"version\": \"$component_version\"," + echo ' "description": "This component is generated for testing purposes.",' + echo ' "properties": {' + echo " \"path\": \"$component_path\"" + echo " }" echo "}" } > "$o" @@ -736,7 +735,7 @@ DownloadUpdateArtifacts() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -761,8 +760,8 @@ InstallUpdate() { if [ "$installed_criteria" == "" ]; then log_error "Script call missing '--installed-criteria' argument." resultCode=0 - # ADUC_ERC_SCRIPT_HANDLER_MISSING_INSTALLED_CRITERIA (0x30600002) - extendedResultCode=$((0x30600002)) + # ADUC_ERC_SCRIPT_HANDLER_MISSING_INSTALLED_CRITERIA (0x30500002) + extendedResultCode=$((0x30500002)) resultDetails="Script call missing '--installed-criteria' argument." else @@ -796,8 +795,8 @@ InstallUpdate() { # Set result code and details resultCode=0 - # ADUC_ERC_SCRIPT_HANDLER_CHILD_PROCESS_FAILURE_EXITCODE(exitCode) (0x30601000 + exitCode) - extendedResultCode=$((0x30601000 + copy_ret)) + # ADUC_ERC_SCRIPT_HANDLER_CHILD_PROCESS_FAILURE_EXITCODE(exitCode) (0x30501000 + exitCode) + extendedResultCode=$((0x30501000 + copy_ret)) resultDetails="Firmware installation failed. ComponentId: $component_id" else log_info "Install succeeded." @@ -819,7 +818,7 @@ InstallUpdate() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -836,7 +835,7 @@ ApplyUpdate() { # ADUC_Result_Apply_Success = 700 resultCode=700 fi - # No additional error. + # No additional error. extendedResultCode=0 resultDetails= # Prepare ADUC_Result json. @@ -846,7 +845,7 @@ ApplyUpdate() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -866,13 +865,13 @@ SimulatePreInstallSuccess() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } SimulatePostInstallSuccess() { -output "Simulating post-instll step success." + output "Simulating post-instll step success." ret_val=0 # ADUC_Result_Apply_Success = 700 resultCode=700 @@ -886,12 +885,12 @@ output "Simulating post-instll step success." output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } -CancelUpdate(){ +CancelUpdate() { ret_val=0 $ret $ret_val } @@ -941,4 +940,3 @@ if [ -n "$do_cancel_action" ]; then fi $ret $ret_val - diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-firmware-installscript.sh b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-firmware-installscript.sh similarity index 92% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-firmware-installscript.sh rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-firmware-installscript.sh index 776a2399f..ab08c69ab 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-firmware-installscript.sh +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-firmware-installscript.sh @@ -10,9 +10,9 @@ ret_val=0 # Ensure we dont end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi # Output formatting. @@ -106,61 +106,60 @@ PARAMS= # _timestamp= -update_timestamp() -{ +update_timestamp() { # See https://man7.org/linux/man-pages/man1/date.1.html _timestamp="$(date +'%Y/%m/%d:%H%M%S')" } -log_debug(){ - if [ $log_level -gt 0 ]; then +log_debug() { + if [ $log_level -gt 0 ]; then return fi log "$log_debug_pref" "$@" } -log_info(){ - if [ $log_level -gt 1 ]; then +log_info() { + if [ $log_level -gt 1 ]; then return fi log "$log_info_pref" "$@" } -log_warn(){ - if [ $log_level -gt 2 ]; then +log_warn() { + if [ $log_level -gt 2 ]; then return fi log "$log_warn_pref" "$@" } -log_error(){ - if [ $log_level -gt 3 ]; then +log_error() { + if [ $log_level -gt 3 ]; then return fi log "$log_error_pref" "$@" } -log(){ +log() { update_timestamp - if [ -z $log_file ]; then + if [ -z "$log_file" ]; then echo -e "[$_timestamp]" "$@" >&1 else - echo "[$_timestamp]" "$@" >> $log_file + echo "[$_timestamp]" "$@" >> "$log_file" fi } -output(){ +output() { update_timestamp - if [ -z $output_file ]; then + if [ -z "$output_file" ]; then echo "[$_timestamp]" "$@" >&1 else echo "[$_timestamp]" "$@" >> "$output_file" fi } -result(){ - # NOTE: dont' insert timestamp in result file. - if [ -z $result_file ]; then +result() { + # NOTE: don't insert timestamp in result file. + if [ -z "$result_file" ]; then echo "$@" >&1 else echo "$@" > "$result_file" @@ -188,7 +187,7 @@ print_help() { echo "Device Update reserved argument" echo "===============================" echo "" - echo "--action-isinstalled Perform 'is-installed' check." + echo "--action-is-installed Perform 'is-installed' check." echo " Check whether the selected component [or primary device] current states" echo " satisfies specified 'installedCriteria' data." echo "--installed-criteria Specify the Installed-Criteria string." @@ -201,7 +200,7 @@ print_help() { echo "--restart-to-apply Request the host device to restart when applying update to this component." echo "--restart-agent-to-apply Request the DU Agent to restart when applying update to this component." echo "" - echo "--install-error-policy Indicates how to proceed when an error occurs. Default \"abort\"" + echo '--install-error-policy Indicates how to proceed when an error occurs. Default "abort"' echo " options: abort, continue" echo "--install-reboot-policy Indicates whether a device reboot is required, after install completed successfully." echo " Optios:" @@ -434,7 +433,7 @@ while [[ $1 != "" ]]; do error "--firmware-file parameter is mandatory." $ret 1 fi - firmware_file="$1"; + firmware_file="$1" echo "firmware file: $firmware_file" shift ;; @@ -445,7 +444,7 @@ while [[ $1 != "" ]]; do error "--work-folder parameter is mandatory." $ret 1 fi - workfolder="$1"; + workfolder="$1" shift ;; @@ -460,7 +459,7 @@ while [[ $1 != "" ]]; do error "--out-file parameter is mandatory." $ret 1 fi - output_file="$1"; + output_file="$1" # #Create output file path. @@ -481,7 +480,7 @@ while [[ $1 != "" ]]; do error "--result-file parameter is mandatory." $ret 1 fi - result_file="$1"; + result_file="$1" # #Create result file path. # @@ -500,7 +499,7 @@ while [[ $1 != "" ]]; do error "--log-file parameter is mandatory." $ret 1 fi - log_file="$1"; + log_file="$1" shift ;; --log-level) @@ -554,7 +553,7 @@ done # Example implementation of 'IsInstalled' function, for contoso-motor component. # # Design Goal: -# Determine whether the spcifified 'installedCriteria' (parameter $1) is met. +# Determine whether the specified 'installedCriteria' (parameter $1) is met. # # 'installedCriteria' is a version number of a motor in a mock component's data file # located at "$component_props['path']/firmware.json". @@ -577,7 +576,7 @@ done # ADUC_Result_IsInstalled_Installed = 900, /**< Succeeded and content is installed. */ # ADUC_Result_IsInstalled_NotInstalled = 901, /**< Succeeded and content is not installed */ # -IsInstalled(){ +IsInstalled() { path_key=path component_path=${component_props[$path_key]} component_file_path="$component_path/firmware.json" @@ -592,7 +591,7 @@ IsInstalled(){ # extendedResultCode(12345) mock value. extendedResultCode=12345 resultDetails="Invalid installedCriteria value." - elif [[ -f "$component_file_path" ]]; then + elif [[ -f $component_file_path ]]; then log_debug "Found component data file '$component_file_path'." @@ -651,7 +650,7 @@ IsInstalled(){ output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" } @@ -659,7 +658,7 @@ IsInstalled(){ # Helper function # Not returning ADUC_RESULT # -ResetComponent(){ +ResetComponent() { path_key=path component_path=${component_props[$path_key]} component_file_path="$component_path/firmware.json" @@ -688,21 +687,20 @@ ResetComponent(){ o="$component_file_path" { - echo "{"; - echo " \"id\": \"$component_id\"," ; - echo " \"name\": \"$component_name\"," ; - echo " \"group\": \"$component_group\"," ; - echo " \"manufacturer\": \"$component_vendor\"," ; - echo " \"model\": \"$component_model\"," ; - echo " \"version\": \"$component_version\"," ; - echo " \"description\": \"This component is generated for testing purposes.\"," ; - echo " \"properties\": {" ; - echo " \"path\": \"$component_path\"" ; - echo " }" ; + echo "{" + echo " \"id\": \"$component_id\"," + echo " \"name\": \"$component_name\"," + echo " \"group\": \"$component_group\"," + echo " \"manufacturer\": \"$component_vendor\"," + echo " \"model\": \"$component_model\"," + echo " \"version\": \"$component_version\"," + echo ' "description": "This component is generated for testing purposes.",' + echo ' "properties": {' + echo " \"path\": \"$component_path\"" + echo " }" echo "}" } > "$o" - echo "#" echo "# Genrated component data file: \"$component_file_path\"" echo "#" @@ -737,7 +735,7 @@ DownloadUpdateArtifacts() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -762,8 +760,8 @@ InstallUpdate() { if [ "$installed_criteria" == "" ]; then log_error "Script call missing '--installed-criteria' argument." resultCode=0 - # ADUC_ERC_SCRIPT_HANDLER_MISSING_INSTALLED_CRITERIA (0x30600002) - extendedResultCode=$((0x30600002)) + # ADUC_ERC_SCRIPT_HANDLER_MISSING_INSTALLED_CRITERIA (0x30500002) + extendedResultCode=$((0x30500002)) resultDetails="Script call missing '--installed-criteria' argument." else @@ -797,8 +795,8 @@ InstallUpdate() { # Set result code and details resultCode=0 - # ADUC_ERC_SCRIPT_HANDLER_CHILD_PROCESS_FAILURE_EXITCODE(exitCode) (0x30601000 + exitCode) - extendedResultCode=$((0x30601000 + copy_ret)) + # ADUC_ERC_SCRIPT_HANDLER_CHILD_PROCESS_FAILURE_EXITCODE(exitCode) (0x30501000 + exitCode) + extendedResultCode=$((0x30501000 + copy_ret)) resultDetails="Firmware installation failed. ComponentId: $component_id" else log_info "Install succeeded." @@ -820,7 +818,7 @@ InstallUpdate() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -837,7 +835,7 @@ ApplyUpdate() { # ADUC_Result_Apply_Success = 700 resultCode=700 fi - # No additional error. + # No additional error. extendedResultCode=0 resultDetails= # Prepare ADUC_Result json. @@ -847,7 +845,7 @@ ApplyUpdate() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -867,13 +865,13 @@ SimulatePreInstallSuccess() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } SimulatePostInstallSuccess() { -output "Simulating post-instll step success." + output "Simulating post-instll step success." ret_val=0 # ADUC_Result_Apply_Success = 700 resultCode=700 @@ -887,12 +885,12 @@ output "Simulating post-instll step success." output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } -CancelUpdate(){ +CancelUpdate() { ret_val=0 $ret $ret_val } @@ -942,4 +940,3 @@ if [ -n "$do_cancel_action" ]; then fi $ret $ret_val - diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-motor-installscript.sh b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-motor-installscript.sh similarity index 92% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-motor-installscript.sh rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-motor-installscript.sh index 776a2399f..ab08c69ab 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-motor-installscript.sh +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-motor-installscript.sh @@ -10,9 +10,9 @@ ret_val=0 # Ensure we dont end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi # Output formatting. @@ -106,61 +106,60 @@ PARAMS= # _timestamp= -update_timestamp() -{ +update_timestamp() { # See https://man7.org/linux/man-pages/man1/date.1.html _timestamp="$(date +'%Y/%m/%d:%H%M%S')" } -log_debug(){ - if [ $log_level -gt 0 ]; then +log_debug() { + if [ $log_level -gt 0 ]; then return fi log "$log_debug_pref" "$@" } -log_info(){ - if [ $log_level -gt 1 ]; then +log_info() { + if [ $log_level -gt 1 ]; then return fi log "$log_info_pref" "$@" } -log_warn(){ - if [ $log_level -gt 2 ]; then +log_warn() { + if [ $log_level -gt 2 ]; then return fi log "$log_warn_pref" "$@" } -log_error(){ - if [ $log_level -gt 3 ]; then +log_error() { + if [ $log_level -gt 3 ]; then return fi log "$log_error_pref" "$@" } -log(){ +log() { update_timestamp - if [ -z $log_file ]; then + if [ -z "$log_file" ]; then echo -e "[$_timestamp]" "$@" >&1 else - echo "[$_timestamp]" "$@" >> $log_file + echo "[$_timestamp]" "$@" >> "$log_file" fi } -output(){ +output() { update_timestamp - if [ -z $output_file ]; then + if [ -z "$output_file" ]; then echo "[$_timestamp]" "$@" >&1 else echo "[$_timestamp]" "$@" >> "$output_file" fi } -result(){ - # NOTE: dont' insert timestamp in result file. - if [ -z $result_file ]; then +result() { + # NOTE: don't insert timestamp in result file. + if [ -z "$result_file" ]; then echo "$@" >&1 else echo "$@" > "$result_file" @@ -188,7 +187,7 @@ print_help() { echo "Device Update reserved argument" echo "===============================" echo "" - echo "--action-isinstalled Perform 'is-installed' check." + echo "--action-is-installed Perform 'is-installed' check." echo " Check whether the selected component [or primary device] current states" echo " satisfies specified 'installedCriteria' data." echo "--installed-criteria Specify the Installed-Criteria string." @@ -201,7 +200,7 @@ print_help() { echo "--restart-to-apply Request the host device to restart when applying update to this component." echo "--restart-agent-to-apply Request the DU Agent to restart when applying update to this component." echo "" - echo "--install-error-policy Indicates how to proceed when an error occurs. Default \"abort\"" + echo '--install-error-policy Indicates how to proceed when an error occurs. Default "abort"' echo " options: abort, continue" echo "--install-reboot-policy Indicates whether a device reboot is required, after install completed successfully." echo " Optios:" @@ -434,7 +433,7 @@ while [[ $1 != "" ]]; do error "--firmware-file parameter is mandatory." $ret 1 fi - firmware_file="$1"; + firmware_file="$1" echo "firmware file: $firmware_file" shift ;; @@ -445,7 +444,7 @@ while [[ $1 != "" ]]; do error "--work-folder parameter is mandatory." $ret 1 fi - workfolder="$1"; + workfolder="$1" shift ;; @@ -460,7 +459,7 @@ while [[ $1 != "" ]]; do error "--out-file parameter is mandatory." $ret 1 fi - output_file="$1"; + output_file="$1" # #Create output file path. @@ -481,7 +480,7 @@ while [[ $1 != "" ]]; do error "--result-file parameter is mandatory." $ret 1 fi - result_file="$1"; + result_file="$1" # #Create result file path. # @@ -500,7 +499,7 @@ while [[ $1 != "" ]]; do error "--log-file parameter is mandatory." $ret 1 fi - log_file="$1"; + log_file="$1" shift ;; --log-level) @@ -554,7 +553,7 @@ done # Example implementation of 'IsInstalled' function, for contoso-motor component. # # Design Goal: -# Determine whether the spcifified 'installedCriteria' (parameter $1) is met. +# Determine whether the specified 'installedCriteria' (parameter $1) is met. # # 'installedCriteria' is a version number of a motor in a mock component's data file # located at "$component_props['path']/firmware.json". @@ -577,7 +576,7 @@ done # ADUC_Result_IsInstalled_Installed = 900, /**< Succeeded and content is installed. */ # ADUC_Result_IsInstalled_NotInstalled = 901, /**< Succeeded and content is not installed */ # -IsInstalled(){ +IsInstalled() { path_key=path component_path=${component_props[$path_key]} component_file_path="$component_path/firmware.json" @@ -592,7 +591,7 @@ IsInstalled(){ # extendedResultCode(12345) mock value. extendedResultCode=12345 resultDetails="Invalid installedCriteria value." - elif [[ -f "$component_file_path" ]]; then + elif [[ -f $component_file_path ]]; then log_debug "Found component data file '$component_file_path'." @@ -651,7 +650,7 @@ IsInstalled(){ output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" } @@ -659,7 +658,7 @@ IsInstalled(){ # Helper function # Not returning ADUC_RESULT # -ResetComponent(){ +ResetComponent() { path_key=path component_path=${component_props[$path_key]} component_file_path="$component_path/firmware.json" @@ -688,21 +687,20 @@ ResetComponent(){ o="$component_file_path" { - echo "{"; - echo " \"id\": \"$component_id\"," ; - echo " \"name\": \"$component_name\"," ; - echo " \"group\": \"$component_group\"," ; - echo " \"manufacturer\": \"$component_vendor\"," ; - echo " \"model\": \"$component_model\"," ; - echo " \"version\": \"$component_version\"," ; - echo " \"description\": \"This component is generated for testing purposes.\"," ; - echo " \"properties\": {" ; - echo " \"path\": \"$component_path\"" ; - echo " }" ; + echo "{" + echo " \"id\": \"$component_id\"," + echo " \"name\": \"$component_name\"," + echo " \"group\": \"$component_group\"," + echo " \"manufacturer\": \"$component_vendor\"," + echo " \"model\": \"$component_model\"," + echo " \"version\": \"$component_version\"," + echo ' "description": "This component is generated for testing purposes.",' + echo ' "properties": {' + echo " \"path\": \"$component_path\"" + echo " }" echo "}" } > "$o" - echo "#" echo "# Genrated component data file: \"$component_file_path\"" echo "#" @@ -737,7 +735,7 @@ DownloadUpdateArtifacts() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -762,8 +760,8 @@ InstallUpdate() { if [ "$installed_criteria" == "" ]; then log_error "Script call missing '--installed-criteria' argument." resultCode=0 - # ADUC_ERC_SCRIPT_HANDLER_MISSING_INSTALLED_CRITERIA (0x30600002) - extendedResultCode=$((0x30600002)) + # ADUC_ERC_SCRIPT_HANDLER_MISSING_INSTALLED_CRITERIA (0x30500002) + extendedResultCode=$((0x30500002)) resultDetails="Script call missing '--installed-criteria' argument." else @@ -797,8 +795,8 @@ InstallUpdate() { # Set result code and details resultCode=0 - # ADUC_ERC_SCRIPT_HANDLER_CHILD_PROCESS_FAILURE_EXITCODE(exitCode) (0x30601000 + exitCode) - extendedResultCode=$((0x30601000 + copy_ret)) + # ADUC_ERC_SCRIPT_HANDLER_CHILD_PROCESS_FAILURE_EXITCODE(exitCode) (0x30501000 + exitCode) + extendedResultCode=$((0x30501000 + copy_ret)) resultDetails="Firmware installation failed. ComponentId: $component_id" else log_info "Install succeeded." @@ -820,7 +818,7 @@ InstallUpdate() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -837,7 +835,7 @@ ApplyUpdate() { # ADUC_Result_Apply_Success = 700 resultCode=700 fi - # No additional error. + # No additional error. extendedResultCode=0 resultDetails= # Prepare ADUC_Result json. @@ -847,7 +845,7 @@ ApplyUpdate() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -867,13 +865,13 @@ SimulatePreInstallSuccess() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } SimulatePostInstallSuccess() { -output "Simulating post-instll step success." + output "Simulating post-instll step success." ret_val=0 # ADUC_Result_Apply_Success = 700 resultCode=700 @@ -887,12 +885,12 @@ output "Simulating post-instll step success." output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } -CancelUpdate(){ +CancelUpdate() { ret_val=0 $ret $ret_val } @@ -942,4 +940,3 @@ if [ -n "$do_cancel_action" ]; then fi $ret $ret_val - diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-steamer-installscript.sh b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-steamer-installscript.sh similarity index 92% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-steamer-installscript.sh rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-steamer-installscript.sh index 776a2399f..ab08c69ab 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-steamer-installscript.sh +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-steamer-installscript.sh @@ -10,9 +10,9 @@ ret_val=0 # Ensure we dont end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi # Output formatting. @@ -106,61 +106,60 @@ PARAMS= # _timestamp= -update_timestamp() -{ +update_timestamp() { # See https://man7.org/linux/man-pages/man1/date.1.html _timestamp="$(date +'%Y/%m/%d:%H%M%S')" } -log_debug(){ - if [ $log_level -gt 0 ]; then +log_debug() { + if [ $log_level -gt 0 ]; then return fi log "$log_debug_pref" "$@" } -log_info(){ - if [ $log_level -gt 1 ]; then +log_info() { + if [ $log_level -gt 1 ]; then return fi log "$log_info_pref" "$@" } -log_warn(){ - if [ $log_level -gt 2 ]; then +log_warn() { + if [ $log_level -gt 2 ]; then return fi log "$log_warn_pref" "$@" } -log_error(){ - if [ $log_level -gt 3 ]; then +log_error() { + if [ $log_level -gt 3 ]; then return fi log "$log_error_pref" "$@" } -log(){ +log() { update_timestamp - if [ -z $log_file ]; then + if [ -z "$log_file" ]; then echo -e "[$_timestamp]" "$@" >&1 else - echo "[$_timestamp]" "$@" >> $log_file + echo "[$_timestamp]" "$@" >> "$log_file" fi } -output(){ +output() { update_timestamp - if [ -z $output_file ]; then + if [ -z "$output_file" ]; then echo "[$_timestamp]" "$@" >&1 else echo "[$_timestamp]" "$@" >> "$output_file" fi } -result(){ - # NOTE: dont' insert timestamp in result file. - if [ -z $result_file ]; then +result() { + # NOTE: don't insert timestamp in result file. + if [ -z "$result_file" ]; then echo "$@" >&1 else echo "$@" > "$result_file" @@ -188,7 +187,7 @@ print_help() { echo "Device Update reserved argument" echo "===============================" echo "" - echo "--action-isinstalled Perform 'is-installed' check." + echo "--action-is-installed Perform 'is-installed' check." echo " Check whether the selected component [or primary device] current states" echo " satisfies specified 'installedCriteria' data." echo "--installed-criteria Specify the Installed-Criteria string." @@ -201,7 +200,7 @@ print_help() { echo "--restart-to-apply Request the host device to restart when applying update to this component." echo "--restart-agent-to-apply Request the DU Agent to restart when applying update to this component." echo "" - echo "--install-error-policy Indicates how to proceed when an error occurs. Default \"abort\"" + echo '--install-error-policy Indicates how to proceed when an error occurs. Default "abort"' echo " options: abort, continue" echo "--install-reboot-policy Indicates whether a device reboot is required, after install completed successfully." echo " Optios:" @@ -434,7 +433,7 @@ while [[ $1 != "" ]]; do error "--firmware-file parameter is mandatory." $ret 1 fi - firmware_file="$1"; + firmware_file="$1" echo "firmware file: $firmware_file" shift ;; @@ -445,7 +444,7 @@ while [[ $1 != "" ]]; do error "--work-folder parameter is mandatory." $ret 1 fi - workfolder="$1"; + workfolder="$1" shift ;; @@ -460,7 +459,7 @@ while [[ $1 != "" ]]; do error "--out-file parameter is mandatory." $ret 1 fi - output_file="$1"; + output_file="$1" # #Create output file path. @@ -481,7 +480,7 @@ while [[ $1 != "" ]]; do error "--result-file parameter is mandatory." $ret 1 fi - result_file="$1"; + result_file="$1" # #Create result file path. # @@ -500,7 +499,7 @@ while [[ $1 != "" ]]; do error "--log-file parameter is mandatory." $ret 1 fi - log_file="$1"; + log_file="$1" shift ;; --log-level) @@ -554,7 +553,7 @@ done # Example implementation of 'IsInstalled' function, for contoso-motor component. # # Design Goal: -# Determine whether the spcifified 'installedCriteria' (parameter $1) is met. +# Determine whether the specified 'installedCriteria' (parameter $1) is met. # # 'installedCriteria' is a version number of a motor in a mock component's data file # located at "$component_props['path']/firmware.json". @@ -577,7 +576,7 @@ done # ADUC_Result_IsInstalled_Installed = 900, /**< Succeeded and content is installed. */ # ADUC_Result_IsInstalled_NotInstalled = 901, /**< Succeeded and content is not installed */ # -IsInstalled(){ +IsInstalled() { path_key=path component_path=${component_props[$path_key]} component_file_path="$component_path/firmware.json" @@ -592,7 +591,7 @@ IsInstalled(){ # extendedResultCode(12345) mock value. extendedResultCode=12345 resultDetails="Invalid installedCriteria value." - elif [[ -f "$component_file_path" ]]; then + elif [[ -f $component_file_path ]]; then log_debug "Found component data file '$component_file_path'." @@ -651,7 +650,7 @@ IsInstalled(){ output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" } @@ -659,7 +658,7 @@ IsInstalled(){ # Helper function # Not returning ADUC_RESULT # -ResetComponent(){ +ResetComponent() { path_key=path component_path=${component_props[$path_key]} component_file_path="$component_path/firmware.json" @@ -688,21 +687,20 @@ ResetComponent(){ o="$component_file_path" { - echo "{"; - echo " \"id\": \"$component_id\"," ; - echo " \"name\": \"$component_name\"," ; - echo " \"group\": \"$component_group\"," ; - echo " \"manufacturer\": \"$component_vendor\"," ; - echo " \"model\": \"$component_model\"," ; - echo " \"version\": \"$component_version\"," ; - echo " \"description\": \"This component is generated for testing purposes.\"," ; - echo " \"properties\": {" ; - echo " \"path\": \"$component_path\"" ; - echo " }" ; + echo "{" + echo " \"id\": \"$component_id\"," + echo " \"name\": \"$component_name\"," + echo " \"group\": \"$component_group\"," + echo " \"manufacturer\": \"$component_vendor\"," + echo " \"model\": \"$component_model\"," + echo " \"version\": \"$component_version\"," + echo ' "description": "This component is generated for testing purposes.",' + echo ' "properties": {' + echo " \"path\": \"$component_path\"" + echo " }" echo "}" } > "$o" - echo "#" echo "# Genrated component data file: \"$component_file_path\"" echo "#" @@ -737,7 +735,7 @@ DownloadUpdateArtifacts() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -762,8 +760,8 @@ InstallUpdate() { if [ "$installed_criteria" == "" ]; then log_error "Script call missing '--installed-criteria' argument." resultCode=0 - # ADUC_ERC_SCRIPT_HANDLER_MISSING_INSTALLED_CRITERIA (0x30600002) - extendedResultCode=$((0x30600002)) + # ADUC_ERC_SCRIPT_HANDLER_MISSING_INSTALLED_CRITERIA (0x30500002) + extendedResultCode=$((0x30500002)) resultDetails="Script call missing '--installed-criteria' argument." else @@ -797,8 +795,8 @@ InstallUpdate() { # Set result code and details resultCode=0 - # ADUC_ERC_SCRIPT_HANDLER_CHILD_PROCESS_FAILURE_EXITCODE(exitCode) (0x30601000 + exitCode) - extendedResultCode=$((0x30601000 + copy_ret)) + # ADUC_ERC_SCRIPT_HANDLER_CHILD_PROCESS_FAILURE_EXITCODE(exitCode) (0x30501000 + exitCode) + extendedResultCode=$((0x30501000 + copy_ret)) resultDetails="Firmware installation failed. ComponentId: $component_id" else log_info "Install succeeded." @@ -820,7 +818,7 @@ InstallUpdate() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -837,7 +835,7 @@ ApplyUpdate() { # ADUC_Result_Apply_Success = 700 resultCode=700 fi - # No additional error. + # No additional error. extendedResultCode=0 resultDetails= # Prepare ADUC_Result json. @@ -847,7 +845,7 @@ ApplyUpdate() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -867,13 +865,13 @@ SimulatePreInstallSuccess() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } SimulatePostInstallSuccess() { -output "Simulating post-instll step success." + output "Simulating post-instll step success." ret_val=0 # ADUC_Result_Apply_Success = 700 resultCode=700 @@ -887,12 +885,12 @@ output "Simulating post-instll step success." output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } -CancelUpdate(){ +CancelUpdate() { ret_val=0 $ret $ret_val } @@ -942,4 +940,3 @@ if [ -n "$do_cancel_action" ]; then fi $ret $ret_val - diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-update-simulatorscript.sh b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-update-simulatorscript.sh similarity index 93% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-update-simulatorscript.sh rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-update-simulatorscript.sh index 0f1bf00e8..7884c8170 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/sample-updates/scripts/contoso-update-simulatorscript.sh +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/sample-updates/scripts/contoso-update-simulatorscript.sh @@ -10,9 +10,9 @@ ret_val=0 # Ensure we dont end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi # Output formatting. @@ -111,61 +111,60 @@ PARAMS= # _timestamp= -update_timestamp() -{ +update_timestamp() { # See https://man7.org/linux/man-pages/man1/date.1.html _timestamp="$(date +'%Y/%m/%d:%H%M%S')" } -log_debug(){ - if [ $log_level -gt 0 ]; then +log_debug() { + if [ $log_level -gt 0 ]; then return fi log "$log_debug_pref" "$@" } -log_info(){ - if [ $log_level -gt 1 ]; then +log_info() { + if [ $log_level -gt 1 ]; then return fi log "$log_info_pref" "$@" } -log_warn(){ - if [ $log_level -gt 2 ]; then +log_warn() { + if [ $log_level -gt 2 ]; then return fi log "$log_warn_pref" "$@" } -log_error(){ - if [ $log_level -gt 3 ]; then +log_error() { + if [ $log_level -gt 3 ]; then return fi log "$log_error_pref" "$@" } -log(){ +log() { update_timestamp - if [ -z $log_file ]; then + if [ -z "$log_file" ]; then echo -e "[$_timestamp]" "$@" >&1 else - echo "[$_timestamp]" "$@" >> $log_file + echo "[$_timestamp]" "$@" >> "$log_file" fi } -output(){ +output() { update_timestamp - if [ -z $output_file ]; then + if [ -z "$output_file" ]; then echo "[$_timestamp]" "$@" >&1 else echo "[$_timestamp]" "$@" >> "$output_file" fi } -result(){ - # NOTE: dont' insert timestamp in result file. - if [ -z $result_file ]; then +result() { + # NOTE: don't insert timestamp in result file. + if [ -z "$result_file" ]; then echo "$@" >&1 else echo "$@" > "$result_file" @@ -193,7 +192,7 @@ print_help() { echo "Device Update reserved argument" echo "===============================" echo "" - echo "--action-isinstalled Perform 'is-installed' check." + echo "--action-is-installed Perform 'is-installed' check." echo " Check whether the selected component [or primary device] current states" echo " satisfies specified 'installedCriteria' data." echo "--installed-criteria Specify the Installed-Criteria string." @@ -206,7 +205,7 @@ print_help() { echo "--restart-to-apply Request the host device to restart when applying update to this component." echo "--restart-agent-to-apply Request the DU Agent to restart when applying update to this component." echo "" - echo "--install-error-policy Indicates how to proceed when an error occurs. Default \"abort\"" + echo '--install-error-policy Indicates how to proceed when an error occurs. Default "abort"' echo " options: abort, continue" echo "--install-reboot-policy Indicates whether a device reboot is required, after install completed successfully." echo " Optios:" @@ -439,7 +438,7 @@ while [[ $1 != "" ]]; do error "--firmware-file parameter is mandatory." $ret 1 fi - firmware_file="$1"; + firmware_file="$1" echo "firmware file: $firmware_file" shift ;; @@ -450,7 +449,7 @@ while [[ $1 != "" ]]; do error "--work-folder parameter is mandatory." $ret 1 fi - workfolder="$1"; + workfolder="$1" shift ;; @@ -465,7 +464,7 @@ while [[ $1 != "" ]]; do error "--out-file parameter is mandatory." $ret 1 fi - output_file="$1"; + output_file="$1" # #Create output file path. @@ -486,7 +485,7 @@ while [[ $1 != "" ]]; do error "--result-file parameter is mandatory." $ret 1 fi - result_file="$1"; + result_file="$1" # #Create result file path. # @@ -505,7 +504,7 @@ while [[ $1 != "" ]]; do error "--log-file parameter is mandatory." $ret 1 fi - log_file="$1"; + log_file="$1" shift ;; --log-level) @@ -595,7 +594,7 @@ done # Example implementation of 'IsInstalled' function, for contoso-motor component. # # Design Goal: -# Determine whether the spcifified 'installedCriteria' (parameter $1) is met. +# Determine whether the specified 'installedCriteria' (parameter $1) is met. # # 'installedCriteria' is a version number of a motor in a mock component's data file # located at "$component_props['path']/firmware.json". @@ -618,7 +617,7 @@ done # ADUC_Result_IsInstalled_Installed = 900, /**< Succeeded and content is installed. */ # ADUC_Result_IsInstalled_NotInstalled = 901, /**< Succeeded and content is not installed */ # -IsInstalled(){ +IsInstalled() { path_key=path component_path=${component_props[$path_key]} component_file_path="$component_path/firmware.json" @@ -633,7 +632,7 @@ IsInstalled(){ # extendedResultCode(12345) mock value. extendedResultCode=12345 resultDetails="Invalid installedCriteria value." - elif [[ -f "$component_file_path" ]]; then + elif [[ -f $component_file_path ]]; then log_debug "Found component data file '$component_file_path'." @@ -692,7 +691,7 @@ IsInstalled(){ output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" } @@ -700,7 +699,7 @@ IsInstalled(){ # Helper function # Not returning ADUC_RESULT # -ResetComponent(){ +ResetComponent() { path_key=path component_path=${component_props[$path_key]} component_file_path="$component_path/firmware.json" @@ -729,17 +728,17 @@ ResetComponent(){ o="$component_file_path" { - echo "{"; - echo " \"id\": \"$component_id\"," ; - echo " \"name\": \"$component_name\"," ; - echo " \"group\": \"$component_group\"," ; - echo " \"manufacturer\": \"$component_vendor\"," ; - echo " \"model\": \"$component_model\"," ; - echo " \"version\": \"$component_version\"," ; - echo " \"description\": \"This component is generated for testing purposes.\"," ; - echo " \"properties\": {" ; - echo " \"path\": \"$component_path\"" ; - echo " }" ; + echo "{" + echo " \"id\": \"$component_id\"," + echo " \"name\": \"$component_name\"," + echo " \"group\": \"$component_group\"," + echo " \"manufacturer\": \"$component_vendor\"," + echo " \"model\": \"$component_model\"," + echo " \"version\": \"$component_version\"," + echo ' "description": "This component is generated for testing purposes.",' + echo ' "properties": {' + echo " \"path\": \"$component_path\"" + echo " }" echo "}" } > "$o" @@ -777,7 +776,7 @@ DownloadUpdateArtifacts() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -802,8 +801,8 @@ InstallUpdate() { if [ "$installed_criteria" == "" ]; then log_error "Script call missing '--installed-criteria' argument." resultCode=0 - # ADUC_ERC_SCRIPT_HANDLER_MISSING_INSTALLED_CRITERIA (0x30600002) - extendedResultCode=$((0x30600002)) + # ADUC_ERC_SCRIPT_HANDLER_MISSING_INSTALLED_CRITERIA (0x30500002) + extendedResultCode=$((0x30500002)) resultDetails="Script call missing '--installed-criteria' argument." else @@ -837,8 +836,8 @@ InstallUpdate() { # Set result code and details resultCode=0 - # ADUC_ERC_SCRIPT_HANDLER_CHILD_PROCESS_FAILURE_EXITCODE(exitCode) (0x30601000 + exitCode) - extendedResultCode=$((0x30601000 + copy_ret)) + # ADUC_ERC_SCRIPT_HANDLER_CHILD_PROCESS_FAILURE_EXITCODE(exitCode) (0x30501000 + exitCode) + extendedResultCode=$((0x30501000 + copy_ret)) resultDetails="Firmware installation failed. ComponentId: $component_id" else log_info "Install succeeded." @@ -860,7 +859,7 @@ InstallUpdate() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -877,7 +876,7 @@ ApplyUpdate() { # ADUC_Result_Apply_Success = 700 resultCode=700 fi - # No additional error. + # No additional error. extendedResultCode=0 resultDetails= # Prepare ADUC_Result json. @@ -887,7 +886,7 @@ ApplyUpdate() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } @@ -905,12 +904,11 @@ SimulateActionResult() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } - SimulatePreInstallSuccess() { output "Simulating pre-instll step success." ret_val=0 @@ -926,13 +924,13 @@ SimulatePreInstallSuccess() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } SimulatePostInstallSuccess() { -output "Simulating post-instll step success." + output "Simulating post-instll step success." ret_val=0 # ADUC_Result_Apply_Success = 700 resultCode=700 @@ -946,12 +944,12 @@ output "Simulating post-instll step success." output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } -CancelUpdate(){ +CancelUpdate() { ret_val=0 $ret $ret_val } @@ -1006,4 +1004,3 @@ if [ -n "$do_cancel_action" ]; then fi $ret $ret_val - diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/tools/add-packages-microsoft-com.sh b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/tools/add-packages-microsoft-com.sh similarity index 97% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/tools/add-packages-microsoft-com.sh rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/tools/add-packages-microsoft-com.sh index 5db0febdf..622baf68f 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/tools/add-packages-microsoft-com.sh +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/tools/add-packages-microsoft-com.sh @@ -1,3 +1,4 @@ +#!/bin/bash # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -10,4 +11,3 @@ curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microso sudo cp ./microsoft.gpg /etc/apt/trusted.gpg.d/ sudo apt-get update - diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/tools/reset-demo-components.sh b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/tools/reset-demo-components.sh similarity index 100% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/tools/reset-demo-components.sh rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/tools/reset-demo-components.sh diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/tutorial1.md b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/tutorial1.md similarity index 96% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/tutorial1.md rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/tutorial1.md index 37a349746..d6e6888ac 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/tutorial1.md +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/tutorial1.md @@ -43,7 +43,7 @@ At this time, this update is version 20 in their `update line of service`. The f ## Preparing Update Instructions (steps) -> **Prerequisites** | Read [Device Update for IoT Hub - Scripts](../../../../../../tools/AduCmdlets/README.md) to understand how to create ADU Import Manifest, and ensure that all required scripts can be run properly. +> **Prerequisites** | Read [Device Update for IoT Hub - Scripts](../../../../../../tools/AduCmdlets/README.md) to understand how to create ADU Import Manifest, and ensure that all required scripts can be run properly. To accomplish the goals, this update requires 2 `update instruction steps` in the Update Manifest: @@ -70,10 +70,10 @@ $topLevelStep1 = New-AduInstallationStep ` ### Top-Level Step #2 - Install `Virtual Motors Firmware 1.0` on all `Motor` components -Since this update is targeting a group of components on the host device, the `child update` and a top-level `reference step` must be created. +Since this update is targeting a group of components on the host device, the `child update` and a top-level `reference step` must be created. The following script snippet is used to create both child update and reference step: ->**NOTE** | Child update identity is required. For this tutorial, we use **"contoso", "contoso-virtual-motors", "1.1"** for `Provider`, `Name`, and `Version` respectively. +>**NOTE** | Child update identity is required. For this tutorial, we use **"contoso", "contoso-virtual-motors", "1.1"** for `Provider`, `Name`, and `Version` respectively. ```powershell @@ -96,11 +96,11 @@ $motorsSelector = @{ group = 'motors' } $motorsCompat = New-AduUpdateCompatibility -Properties $motorsSelector $motorScriptFile = ".\update-files\contoso-motor-installscript.sh" -$motorFirmwareFile = ".\update-files\motor-firmware-$motorsFirmwareVersion.json" +$motorFirmwareFile = ".\update-files\motor-firmware-$motorsFirmwareVersion.json" #------------ # ADD STEP(S) -# +# # This update contains 1 steps. $motorsInstallSteps = @() @@ -165,10 +165,10 @@ $UpdateName = "Virtual-Vacuum" $UpdateVersion = '20.0' # Host Device Info -$DeviceManufacturer = "contoso" -$DeviceModel = 'virtual-vacuum-v1' +$Manufacturer = "contoso" +$Model = 'virtual-vacuum-v1' -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model $parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" @@ -177,7 +177,7 @@ $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($par # Create the parent update containing 1 inline step and 1 reference step. # ------------------------------------------------------ Write-Host " Preparing parent update $parentUpdateIdStr..." -$payloadFiles = +$payloadFiles = $parentSteps = @() #------------ diff --git a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/tutorial1.ps1 b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/tutorial1.ps1 similarity index 96% rename from src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/tutorial1.ps1 rename to src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/tutorial1.ps1 index b2793f320..83b3b7ff9 100644 --- a/src/extensions/component-enumerators/examples/contoso-component-enumerator/demo/tutorial1.ps1 +++ b/src/extensions/component_enumerators/examples/contoso_component_enumerator/demo/tutorial1.ps1 @@ -46,11 +46,11 @@ $motorsSelector = @{ group = 'motors' } $motorsCompat = New-AduUpdateCompatibility -Properties $motorsSelector $motorScriptFile = ".\update-files\contoso-motor-installscript.sh" -$motorFirmwareFile = ".\update-files\motor-firmware-$motorsFirmwareVersion.json" +$motorFirmwareFile = ".\update-files\motor-firmware-$motorsFirmwareVersion.json" #------------ # ADD STEP(S) -# +# # This update contains 1 steps. $motorsInstallSteps = @() @@ -109,10 +109,10 @@ $UpdateName = "Virtual-Vacuum" $UpdateVersion = '20.0' # Host Device Info -$DeviceManufacturer = "contoso" -$DeviceModel = 'virtual-vacuum-v1' +$Manufacturer = "contoso" +$Model = 'virtual-vacuum-v1' -$parentCompat = New-AduUpdateCompatibility -DeviceManufacturer $DeviceManufacturer -DeviceModel $DeviceModel +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model $parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" @@ -121,7 +121,7 @@ $parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($par # Create the parent update containing 1 inline step and 1 reference step. # ------------------------------------------------------ Write-Host " Preparing parent update $parentUpdateIdStr..." -$payloadFiles = +$payloadFiles = $parentSteps = @() #------------ diff --git a/src/extensions/content-downloaders/CMakeLists.txt b/src/extensions/content-downloaders/CMakeLists.txt deleted file mode 100644 index 4e944d105..000000000 --- a/src/extensions/content-downloaders/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -cmake_minimum_required (VERSION 3.5) - -project (content_downloaders) - -add_subdirectory (curl-downloader) -add_subdirectory (deliveryoptimization-downloader) diff --git a/src/extensions/content-downloaders/curl-downloader/CMakeLists.txt b/src/extensions/content-downloaders/curl-downloader/CMakeLists.txt deleted file mode 100644 index c8254ffe1..000000000 --- a/src/extensions/content-downloaders/curl-downloader/CMakeLists.txt +++ /dev/null @@ -1,25 +0,0 @@ -project (curl-content-downloader) - -include (agentRules) - -compileasc99 () - -add_library ( - ${PROJECT_NAME} SHARED - curl-content-downloader.cpp -) - -#find_package (Parson REQUIRED) - -add_library (aduc::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) - -target_include_directories (${PROJECT_NAME} PUBLIC ${ADUC_EXTENSION_INCLUDES} ${ADUC_EXPORT_INCLUDES}) - -target_link_libraries ( - ${PROJECT_NAME} - PRIVATE aziotsharedutil aduc::c_utils aduc::logging - aduc::process_utils - aduc::string_utils - aduc::hash_utils) - -install (TARGETS ${PROJECT_NAME} LIBRARY DESTINATION ${ADUC_EXTENSIONS_INSTALL_FOLDER}) diff --git a/src/extensions/content_downloaders/CMakeLists.txt b/src/extensions/content_downloaders/CMakeLists.txt new file mode 100644 index 000000000..de7a67b9b --- /dev/null +++ b/src/extensions/content_downloaders/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required (VERSION 3.5) + +project (content_downloaders) + +add_subdirectory (curl_downloader) +add_subdirectory (deliveryoptimization_downloader) diff --git a/src/extensions/content-downloaders/README.md b/src/extensions/content_downloaders/README.md similarity index 100% rename from src/extensions/content-downloaders/README.md rename to src/extensions/content_downloaders/README.md diff --git a/src/extensions/content_downloaders/curl_downloader/CMakeLists.txt b/src/extensions/content_downloaders/curl_downloader/CMakeLists.txt new file mode 100644 index 000000000..a7f0eaa18 --- /dev/null +++ b/src/extensions/content_downloaders/curl_downloader/CMakeLists.txt @@ -0,0 +1,23 @@ +project (curl_content_downloader) + +include (agentRules) + +compileasc99 () + +add_library (${PROJECT_NAME} SHARED) +add_library (aduc::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +target_sources ( + ${PROJECT_NAME} PRIVATE curl_content_downloader.cpp curl_content_downloader.EXPORTS.cpp + curl_content_downloader.h) + +target_include_directories (${PROJECT_NAME} PUBLIC ${ADU_EXTENSION_INCLUDES} ${ADU_EXPORT_INCLUDES}) + +target_link_libraries ( + ${PROJECT_NAME} + PRIVATE aduc::contract_utils + aduc::hash_utils + aduc::logging + aduc::process_utils) + +install (TARGETS ${PROJECT_NAME} LIBRARY DESTINATION ${ADUC_EXTENSIONS_INSTALL_FOLDER}) diff --git a/src/extensions/content_downloaders/curl_downloader/curl_content_downloader.EXPORTS.cpp b/src/extensions/content_downloaders/curl_downloader/curl_content_downloader.EXPORTS.cpp new file mode 100644 index 000000000..767e737fb --- /dev/null +++ b/src/extensions/content_downloaders/curl_downloader/curl_content_downloader.EXPORTS.cpp @@ -0,0 +1,53 @@ +/** + * @file curl_content_downloader.EXPORTS.cpp + * @brief The exports for Content Downloader Extension. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include "curl_content_downloader.h" // for Download_curl +#include // for EXTERN_C_BEGIN, EXTERN_C_END +#include // for ADUC_ExtensionContractInfo +#include // for ADUC_DownloadProgressCallback +#include // for ADUC_FileEntity + +EXTERN_C_BEGIN + +///////////////////////////////////////////////////////////////////////////// +// BEGIN Shared Library Export Functions +// +// These are the function symbols that the device update agent will +// lookup and call. +// + +ADUC_Result Download( + const ADUC_FileEntity* entity, + const char* workflowId, + const char* workFolder, + unsigned int retryTimeout, + ADUC_DownloadProgressCallback downloadProgressCallback) +{ + return Download_curl(entity, workflowId, workFolder, retryTimeout, downloadProgressCallback); +} + +ADUC_Result Initialize(const char* initializeData) +{ + UNREFERENCED_PARAMETER(initializeData); + return { ADUC_GeneralResult_Success }; +} + +/** + * @brief Gets the extension contract info. + * + * @param[out] contractInfo The extension contract info. + * @return ADUC_Result The result. + */ +ADUC_Result GetContractInfo(ADUC_ExtensionContractInfo* contractInfo) +{ + contractInfo->majorVer = ADUC_V1_CONTRACT_MAJOR_VER; + contractInfo->majorVer = ADUC_V1_CONTRACT_MINOR_VER; + return ADUC_Result{ ADUC_GeneralResult_Success, 0 }; +} + +EXTERN_C_END diff --git a/src/extensions/content-downloaders/curl-downloader/curl-content-downloader.cpp b/src/extensions/content_downloaders/curl_downloader/curl_content_downloader.cpp similarity index 90% rename from src/extensions/content-downloaders/curl-downloader/curl-content-downloader.cpp rename to src/extensions/content_downloaders/curl_downloader/curl_content_downloader.cpp index 836a564b4..7e754021f 100644 --- a/src/extensions/content-downloaders/curl-downloader/curl-content-downloader.cpp +++ b/src/extensions/content_downloaders/curl_downloader/curl_content_downloader.cpp @@ -5,21 +5,17 @@ * @copyright Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + #include "aduc/content_downloader_extension.hpp" +#include "aduc/contract_utils.h" #include "aduc/hash_utils.h" #include "aduc/logging.h" -#include "aduc/process_utils.hpp" +#include "aduc/process_utils.hpp" // for ADUC_LaunchChildProcess -#include #include -#include // for FILE -#include // for calloc -#include // for strcasecmp #include // for stat #include -EXTERN_C_BEGIN - ADUC_Result Download_curl( const ADUC_FileEntity* entity, const char* workflowId, @@ -186,21 +182,3 @@ ADUC_Result Download_curl( result.ExtendedResultCode); return result; } - -ADUC_Result Download( - const ADUC_FileEntity* entity, - const char* workflowId, - const char* workFolder, - unsigned int retryTimeout, - ADUC_DownloadProgressCallback downloadProgressCallback) -{ - return Download_curl(entity, workflowId, workFolder, retryTimeout, downloadProgressCallback); -} - -ADUC_Result Initialize(const char* initializeData) -{ - UNREFERENCED_PARAMETER(initializeData); - return { ADUC_GeneralResult_Success }; -} - -EXTERN_C_END diff --git a/src/extensions/content_downloaders/curl_downloader/curl_content_downloader.h b/src/extensions/content_downloaders/curl_downloader/curl_content_downloader.h new file mode 100644 index 000000000..1c4806056 --- /dev/null +++ b/src/extensions/content_downloaders/curl_downloader/curl_content_downloader.h @@ -0,0 +1,11 @@ + +#include // for ADUC_Result +#include // for ADUC_DownloadProgressCallback +#include // for ADUC_FileEntity + +ADUC_Result Download_curl( + const ADUC_FileEntity* entity, + const char* workflowId, + const char* workFolder, + unsigned int retryTimeout, + ADUC_DownloadProgressCallback downloadProgressCallback); diff --git a/src/extensions/content-downloaders/deliveryoptimization-downloader/CMakeLists.txt b/src/extensions/content_downloaders/deliveryoptimization_downloader/CMakeLists.txt similarity index 58% rename from src/extensions/content-downloaders/deliveryoptimization-downloader/CMakeLists.txt rename to src/extensions/content_downloaders/deliveryoptimization_downloader/CMakeLists.txt index 57557ebbc..29c2eb967 100644 --- a/src/extensions/content-downloaders/deliveryoptimization-downloader/CMakeLists.txt +++ b/src/extensions/content_downloaders/deliveryoptimization_downloader/CMakeLists.txt @@ -1,22 +1,28 @@ -project (deliveryoptimization-content-downloader) +project (deliveryoptimization_content_downloader) include (agentRules) compileasc99 () -add_library (${PROJECT_NAME} SHARED deliveryoptimization-content-downloader.cpp) - find_package (deliveryoptimization_sdk CONFIG REQUIRED) +add_library (${PROJECT_NAME} SHARED) add_library (aduc::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) -target_include_directories (${PROJECT_NAME} PUBLIC ${ADUC_EXTENSION_INCLUDES} +target_sources ( + ${PROJECT_NAME} + PRIVATE deliveryoptimization_content_downloader.cpp + deliveryoptimization_content_downloader.EXPORTS.cpp + deliveryoptimization_content_downloader.h) + +target_include_directories (${PROJECT_NAME} PUBLIC ${ADU_EXTENSION_INCLUDES} ${ADUC_EXPORT_INCLUDES}) target_link_libraries ( ${PROJECT_NAME} PRIVATE aziotsharedutil aduc::c_utils + aduc::contract_utils aduc::hash_utils aduc::logging aduc::process_utils diff --git a/src/extensions/content_downloaders/deliveryoptimization_downloader/deliveryoptimization_content_downloader.EXPORTS.cpp b/src/extensions/content_downloaders/deliveryoptimization_downloader/deliveryoptimization_content_downloader.EXPORTS.cpp new file mode 100644 index 000000000..1f637b3d0 --- /dev/null +++ b/src/extensions/content_downloaders/deliveryoptimization_downloader/deliveryoptimization_content_downloader.EXPORTS.cpp @@ -0,0 +1,104 @@ +/** + * @file deliveryoptimization_content_downloader.cpp + * @brief Content Downloader Extension using Microsoft Delivery Optimization Agent. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include "deliveryoptimization_content_downloader.h" // for do_download +#include // for EXTERN_C_BEGIN, EXTERN_C_END +#include +#include +#include +#include +#include + +#include // for stat + +#include +#include + +EXTERN_C_BEGIN + +///////////////////////////////////////////////////////////////////////////// +// BEGIN Shared Library Export Functions +// +// These are the function symbols that the device update agent will +// lookup and call. +// + +/** + * @brief Gets the extension contract info. + * + * @param[out] contractInfo The extension contract info. + * @return ADUC_Result The result. + */ +ADUC_Result GetContractInfo(ADUC_ExtensionContractInfo* contractInfo) +{ + contractInfo->majorVer = ADUC_V1_CONTRACT_MAJOR_VER; + contractInfo->majorVer = ADUC_V1_CONTRACT_MINOR_VER; + return ADUC_Result{ ADUC_GeneralResult_Success, 0 }; +} + +/** + * @brief Initializes the content downloader. + * + * @param initializeData The initialization data. + * @return ADUC_Result The result. + */ +ADUC_Result Initialize(const char* initializeData) +{ + ADUC_Result result{ ADUC_GeneralResult_Success }; + + if (initializeData == nullptr) + { + Log_Info("Skipping downloader initialization. NULL input."); + goto done; + } + + // The connection string is valid (IoT hub connection successful) and we are ready for further processing. + // Send connection string to DO SDK for it to discover the Edge gateway if present. + if (ConnectionStringUtils_IsNestedEdge(initializeData)) + { + const int ret = deliveryoptimization_set_iot_connection_string(initializeData); + if (ret != 0) + { + // Since it is nested edge and if DO fails to accept the connection string, then we go ahead and + // fail the startup. + Log_Error("Failed to set DO connection string in Nested Edge scenario, result: %d", ret); + result.ResultCode = ADUC_Result_Failure; + result.ExtendedResultCode = ADUC_ERROR_DELIVERY_OPTIMIZATION_DOWNLOADER_EXTERNAL_FAILURE(ret); + goto done; + } + } + +done: + return result; +} + +/** + * @brief The download export. + * + * @param entity The file entity. + * @param workflowId The workflow id. + * @param workFolder The work folder for the update payloads. + * @param retryTimeout The retry timeout. + * @param downloadProgressCallback The download progress callback function. + * @return ADUC_Result The result. + */ +ADUC_Result Download( + const ADUC_FileEntity* entity, + const char* workflowId, + const char* workFolder, + unsigned int retryTimeout, + ADUC_DownloadProgressCallback downloadProgressCallback) +{ + return do_download(entity, workflowId, workFolder, retryTimeout, downloadProgressCallback); +} + +// +// END Shared Library Export Functions +///////////////////////////////////////////////////////////////////////////// + +EXTERN_C_END diff --git a/src/extensions/content-downloaders/deliveryoptimization-downloader/deliveryoptimization-content-downloader.cpp b/src/extensions/content_downloaders/deliveryoptimization_downloader/deliveryoptimization_content_downloader.cpp similarity index 58% rename from src/extensions/content-downloaders/deliveryoptimization-downloader/deliveryoptimization-content-downloader.cpp rename to src/extensions/content_downloaders/deliveryoptimization_downloader/deliveryoptimization_content_downloader.cpp index 7d642645d..6c9ecdc6f 100644 --- a/src/extensions/content-downloaders/deliveryoptimization-downloader/deliveryoptimization-content-downloader.cpp +++ b/src/extensions/content_downloaders/deliveryoptimization_downloader/deliveryoptimization_content_downloader.cpp @@ -7,6 +7,7 @@ */ #include "aduc/connection_string_utils.h" #include "aduc/content_downloader_extension.hpp" +#include "aduc/contract_utils.h" #include "aduc/hash_utils.h" #include "aduc/logging.h" #include "aduc/process_utils.hpp" @@ -21,12 +22,9 @@ #include #include -#include namespace MSDO = microsoft::deliveryoptimization; -EXTERN_C_BEGIN - ADUC_Result do_download( const ADUC_FileEntity* entity, const char* workflowId, @@ -58,70 +56,22 @@ ADUC_Result do_download( entity->DownloadUri, fullFilePath.str().c_str()); - try + const std::error_code doErrorCode = MSDO::download::download_url_to_path( + entity->DownloadUri, fullFilePath.str(), false, std::chrono::seconds(retryTimeout)); + if (!doErrorCode) { - MSDO::download::download_url_to_path( - entity->DownloadUri, fullFilePath.str(), false, std::chrono::seconds(retryTimeout)); - resultCode = ADUC_Result_Download_Success; + extendedResultCode = 0; } - // Catch DO exception only to get extended result code. Other exceptions will be caught by CallResultMethodAndHandleExceptions - catch (const MSDO::exception& e) - { - const int32_t doErrorCode = e.error_code(); - - Log_Info("Caught DO exception, msg: %s, code: %d (%#08x)", e.what(), doErrorCode, doErrorCode); - - if (doErrorCode == static_cast(std::errc::operation_canceled)) - { - Log_Info("Download was cancelled"); - if (downloadProgressCallback != nullptr) - { - downloadProgressCallback(workflowId, entity->FileId, ADUC_DownloadProgressState_Cancelled, 0, 0); - } - - resultCode = ADUC_Result_Failure_Cancelled; - } - else - { - if (doErrorCode == static_cast(std::errc::timed_out)) - { - Log_Error("Download failed due to DO timeout"); - } - - resultCode = ADUC_Result_Failure; - } - - extendedResultCode = MAKE_ADUC_DELIVERY_OPTIMIZATION_EXTENDEDRESULTCODE(doErrorCode); - } - catch (const std::exception& e) - { - Log_Error("DO download failed with an unhandled std exception: %s", e.what()); - - resultCode = ADUC_Result_Failure; - if (errno != 0) - { - extendedResultCode = MAKE_ADUC_ERRNO_EXTENDEDRESULTCODE(errno); - } - else - { - extendedResultCode = ADUC_ERC_NOTRECOVERABLE; - } - } - catch (...) + else { - Log_Error("DO download failed due to an unknown exception"); + // Note: The call to download_url_to_path() does not make use of a cancellation token, + // so the download can only timeout or hit a fatal error. + Log_Error("DO error, msg: %s, code: %#08x, timeout? %d", doErrorCode.message().c_str(), doErrorCode.value(), + (doErrorCode == std::errc::timed_out)); resultCode = ADUC_Result_Failure; - - if (errno != 0) - { - extendedResultCode = MAKE_ADUC_ERRNO_EXTENDEDRESULTCODE(errno); - } - else - { - extendedResultCode = ADUC_ERC_NOTRECOVERABLE; - } + extendedResultCode = MAKE_ADUC_DELIVERY_OPTIMIZATION_EXTENDEDRESULTCODE(doErrorCode.value()); } // If we downloaded successfully, validate the file hash. @@ -198,45 +148,3 @@ ADUC_Result do_download( Log_Info("Download resultCode: %d, extendedCode: %d", resultCode, extendedResultCode); return ADUC_Result{ resultCode, extendedResultCode }; } - -ADUC_Result Download( - const ADUC_FileEntity* entity, - const char* workflowId, - const char* workFolder, - unsigned int retryTimeout, - ADUC_DownloadProgressCallback downloadProgressCallback) -{ - return do_download(entity, workflowId, workFolder, retryTimeout, downloadProgressCallback); -} - -ADUC_Result Initialize(const char* initializeData) -{ - ADUC_Result result{ ADUC_GeneralResult_Success }; - - if (initializeData == nullptr) - { - Log_Info("Skipping downloader initialization. NULL input."); - goto done; - } - - // The connection string is valid (IoT hub connection successful) and we are ready for further processing. - // Send connection string to DO SDK for it to discover the Edge gateway if present. - if (ConnectionStringUtils_IsNestedEdge(initializeData)) - { - const int ret = deliveryoptimization_set_iot_connection_string(initializeData); - if (ret != 0) - { - // Since it is nested edge and if DO fails to accept the connection string, then we go ahead and - // fail the startup. - Log_Error("Failed to set DO connection string in Nested Edge scenario, result: %d", ret); - result.ResultCode = ADUC_Result_Failure; - result.ExtendedResultCode = ADUC_ERROR_DELIVERY_OPTIMIZATION_DOWNLOADER_EXTERNAL_FAILURE(ret); - goto done; - } - } - -done: - return result; -} - -EXTERN_C_END diff --git a/src/extensions/content_downloaders/deliveryoptimization_downloader/deliveryoptimization_content_downloader.h b/src/extensions/content_downloaders/deliveryoptimization_downloader/deliveryoptimization_content_downloader.h new file mode 100644 index 000000000..72b1cec62 --- /dev/null +++ b/src/extensions/content_downloaders/deliveryoptimization_downloader/deliveryoptimization_content_downloader.h @@ -0,0 +1,15 @@ +#ifndef DELIVERYOPTIMIZATION_CONTENT_DOWNLOADER_HELPERS_H +#define DELIVERYOPTIMIZATION_CONTENT_DOWNLOADER_HELPERS_H + +#include // for ADUC_Result +#include // for ADUC_DownloadProgressCallback +#include // for ADUC_FileEntity + +ADUC_Result do_download( + const ADUC_FileEntity* entity, + const char* workflowId, + const char* workFolder, + unsigned int retryTimeout, + ADUC_DownloadProgressCallback downloadProgressCallback); + +#endif // DELIVERYOPTIMIZATION_CONTENT_DOWNLOADER_HELPERS_H diff --git a/src/extensions/download_handlers/CMakeLists.txt b/src/extensions/download_handlers/CMakeLists.txt new file mode 100644 index 000000000..93cb7899b --- /dev/null +++ b/src/extensions/download_handlers/CMakeLists.txt @@ -0,0 +1,5 @@ +project (download_handlers) + +add_subdirectory (download_handler_factory) +add_subdirectory (download_handler_plugin) +add_subdirectory (plugin_examples) diff --git a/src/extensions/download_handlers/README.md b/src/extensions/download_handlers/README.md new file mode 100644 index 000000000..116e1c48d --- /dev/null +++ b/src/extensions/download_handlers/README.md @@ -0,0 +1,37 @@ +# Download Handler Extensibility Point + +A download handler extension exports the function symbols + signatures defined in [extension_download_handler_export_symbols.h](../inc/aduc/exports/extension_download_handler_export_symbols.h) + +When the [v5 update manifest](../../../docs/agent-reference/update-manifest-v5-schema.md) has a `downloadHandlerId` property on an update payload file, then the core agent will see if there exists a registered download handler for that id. If there is, it will load the associated shared library and lookup and call the following exports: + +- `GetContractInfo` + - Populates the `contractInfo` out parameter +- `Initialize` + - Initializes the handler +- `ProcessUpdate` + - returns `ADUC_Result_Download_Handler_SuccessSkipDownload` if it is able to produce the update payload in the download sandbox work folder; otherwise, it returns a ResultCode of `ADUC_Result_DownloadHandler_RequiredFullDownload` or a failure ResultCode. + +The Agent will not download the update payload if it receives `ADUC_Result_Download_Handler_SuccessSkipDownload` from `ProcessUpdate` call; otherwise, it will continue with a fallback download of the update payload as though the downloadHandlerId were not there. + +After successful `Install` and `Apply`, the agent will call `OnUpdateWorkflowCompleted` symbol on the DownloadHandler. + +When Agent shuts down, it will call `Cleanup()` on the DownloadHandler Extension. + +## Usage by Microsoft Delta Download Handler to implement Delta Updates + +The [Microsoft Delta Download Handler](./plugin_examples/microsoft_delta_download_handler/handler/plugin/src/microsoft_delta_download_handler_plugin.EXPORTS.c) is a DownloadHandler that calls the [Diff API](https://github.com/Azure/iot-hub-device-update-delta#diff-api) to produce a large swupdate .swu update payload from a "source .swu file" in a local [source update cache](./plugin_examples/microsoft_delta_download_handler/source_update_cache/inc/aduc/source_update_cache.h) and a much smaller diff file that is downloaded. + +The diff file is produced using the [Diff Generation](https://github.com/Azure/iot-hub-device-update-delta#diff-generation) tool. This tool outputs two files: + +1. The diff file +2. A new .swu update payload file with ext3/ext4 raw image in it that is recompressed with ZSTD compression algorithm. + +The source .swu is baked into the OS image at the expected location for the cache, or will be placed there after either a first image-based update with that .swu or via a preceeding step (e.g. script handler) in a multi-step update. + +It is recommended to use [swupdate handler v2 handler](../step_handlers/swupdate_handler_v2/README.md) (update type of "microsoft/swupdate:2") where: +- `"scriptFileName"` in `"handlerProperties"` of import/update manifest is set to a payload file script that will call `swupdate` appropriately. +- `"swuFileName"` in `"handlerProperties"` is set to the payload file corresponding to the recompressed target .swu swupdate CPIO archive file. + +`swupdate` executable will need to be built with `CONFIG_ZSTD=y` in swupdate's `.config` file. + +NOTE: swupdate source code will need to be greater than equal to [release tag 2019.11](https://github.com/sbabic/swupdate/releases/tag/2019.11) since that was the release when zstd compression support was first added. diff --git a/src/extensions/download_handlers/download_handler_factory/CMakeLists.txt b/src/extensions/download_handlers/download_handler_factory/CMakeLists.txt new file mode 100644 index 000000000..7601887fa --- /dev/null +++ b/src/extensions/download_handlers/download_handler_factory/CMakeLists.txt @@ -0,0 +1,29 @@ +set (target_name download_handler_factory) + +include (agentRules) +compileasc99 () + +add_library (${target_name} STATIC "") +add_library (aduc::${target_name} ALIAS ${target_name}) + +# Turn -fPIC on, in order to use this library in a shared library. +set_property (TARGET ${target_name} PROPERTY POSITION_INDEPENDENT_CODE ON) + +target_include_directories (${target_name} PUBLIC inc ${ADUC_EXPORT_INCLUDES}) + +target_sources (${target_name} PRIVATE src/download_handler_factory.cpp) + +target_link_libraries ( + ${target_name} + PRIVATE aduc::adu_types + aduc::c_utils + aduc::download_handler_plugin + aduc::extension_utils + aduc::hash_utils + aduc::logging + aduc::parser_utils) + +target_compile_definitions ( + ${target_name} + PRIVATE ADUC_DOWNLOAD_HANDLER_EXTENSION_DIR="${ADUC_DOWNLOAD_HANDLER_EXTENSION_DIR}" + ADUC_DOWNLOAD_HANDLER_REG_FILENAME="${ADUC_DOWNLOAD_HANDLER_REG_FILENAME}") diff --git a/src/extensions/download_handlers/download_handler_factory/inc/aduc/download_handler_factory.h b/src/extensions/download_handlers/download_handler_factory/inc/aduc/download_handler_factory.h new file mode 100644 index 000000000..73a829354 --- /dev/null +++ b/src/extensions/download_handlers/download_handler_factory/inc/aduc/download_handler_factory.h @@ -0,0 +1,14 @@ +#ifndef DOWNLOAD_HANDLER_FACTORY_H +#define DOWNLOAD_HANDLER_FACTORY_H + +typedef void* DownloadHandlerHandle; + +/** + * @brief Gets the plugin handle for a downloadHandlerId, which may involve loading the plugin. + * + * @param downloadHandlerId The download handler id from the update metadata of an update payload file. + * @return DownloadHandlerHandle The opaque handle to the download handler plugin. + */ +DownloadHandlerHandle ADUC_DownloadHandlerFactory_LoadDownloadHandler(const char* downloadHandlerId); + +#endif // DOWNLOAD_HANDLER_FACTORY_H diff --git a/src/extensions/download_handlers/download_handler_factory/inc/aduc/download_handler_factory.hpp b/src/extensions/download_handlers/download_handler_factory/inc/aduc/download_handler_factory.hpp new file mode 100644 index 000000000..81d3776ab --- /dev/null +++ b/src/extensions/download_handlers/download_handler_factory/inc/aduc/download_handler_factory.hpp @@ -0,0 +1,52 @@ +/** + * @file download_handler_factory.hpp + * @brief Declaration of the DownloadHandlerFactory. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#ifndef ADUC_DOWNLOAD_HANDLER_FACTORY_HPP +#define ADUC_DOWNLOAD_HANDLER_FACTORY_HPP + +#include +#include +#include + +// Forward declarations +class DownloadHandlerPlugin; + +/** + * @brief The download handler factory that creates and owns all download handler plugin instances. + * + */ +class DownloadHandlerFactory +{ +public: + /** + * @brief Get the singleton instance of the factory object. + * + * @return DownloadHandlerFactory* The factory singleton instance. + */ + static DownloadHandlerFactory* GetInstance(); + + /** + * @brief Provides the plugin instance for the downloadHandlerId. + * + * @param downloadHandlerId The download handler id. + * @return DownloadHandlerPlugin* The download handler plugin instance. + */ + DownloadHandlerPlugin* LoadDownloadHandler(const std::string& downloadHandlerId); + +private: + DownloadHandlerFactory() = default; + + /** + * @brief The collection of download handler plugin instances, keyed by downloadHandleId string. + * @details The plugin instances will be auto-freed upon destruction of the factory instance. + * + */ + std::unordered_map> cachedPlugins; +}; + +#endif // ADUC_DOWNLOAD_HANDLER_FACTORY_HPP diff --git a/src/extensions/download_handlers/download_handler_factory/src/download_handler_factory.cpp b/src/extensions/download_handlers/download_handler_factory/src/download_handler_factory.cpp new file mode 100644 index 000000000..839e51069 --- /dev/null +++ b/src/extensions/download_handlers/download_handler_factory/src/download_handler_factory.cpp @@ -0,0 +1,118 @@ +/** + * @file download_handler_factory.cpp + * @brief Implementation of the DownloadHandlerFactory. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include "aduc/download_handler_factory.hpp" +#include "aduc/download_handler_plugin.hpp" // DownloadHandlerPlugin +#include // EXTERN_C_{BEGIN,END} +#include // GetDownloadHandlerFileEntity +#include // ADUC_HashUtils_VerifyWithStrongestHash +#include // ADUC_Logging_GetLevel +#include // ADUC_FileEntity_Uninit +#include // ADUC_FileEntity +#include // memset +#include + +using DownloadHandlerHandle = void*; + +class FileEntityWrapper +{ + ADUC_FileEntity entity = {}; + bool inited = false; + +public: + FileEntityWrapper() = delete; + FileEntityWrapper(const FileEntityWrapper&) = delete; + FileEntityWrapper& operator=(const FileEntityWrapper&) = delete; + FileEntityWrapper(FileEntityWrapper&&) = delete; + FileEntityWrapper& operator=(FileEntityWrapper&&) = delete; + + FileEntityWrapper(ADUC_FileEntity* sourceFileEntity) + { + // transfer ownership + entity = *sourceFileEntity; + memset(sourceFileEntity, 0, sizeof(ADUC_FileEntity)); + inited = true; + } + + ~FileEntityWrapper() + { + if (inited) + { + inited = false; + ADUC_FileEntity_Uninit(&entity); + } + } + + const ADUC_FileEntity* operator->() + { + return &entity; + } +}; + +// static +DownloadHandlerFactory* DownloadHandlerFactory::GetInstance() +{ + static DownloadHandlerFactory s_instance; + return &s_instance; +} + +DownloadHandlerPlugin* DownloadHandlerFactory::LoadDownloadHandler(const std::string& downloadHandlerId) +{ + auto entry = cachedPlugins.find(downloadHandlerId); + if (entry != cachedPlugins.end()) + { + return (entry->second).get(); + } + + ADUC_FileEntity downloadHandlerFileEntity = {}; + if (!GetDownloadHandlerFileEntity(downloadHandlerId.c_str(), &downloadHandlerFileEntity)) + { + return nullptr; + } + + FileEntityWrapper autoFileEntity(&downloadHandlerFileEntity); + + if (!ADUC_HashUtils_VerifyWithStrongestHash( + autoFileEntity->TargetFilename, autoFileEntity->Hash, autoFileEntity->HashCount)) + { + return nullptr; + } + + try + { + auto plugin = std::unique_ptr( + new DownloadHandlerPlugin(autoFileEntity->TargetFilename, ADUC_Logging_GetLevel())); + cachedPlugins.insert(std::make_pair(downloadHandlerId, plugin.get())); + return plugin.release(); + } + catch (...) + { + return nullptr; + } +} + +EXTERN_C_BEGIN + +DownloadHandlerHandle ADUC_DownloadHandlerFactory_LoadDownloadHandler(const char* downloadHandlerId) +{ + DownloadHandlerHandle handle = nullptr; + + try + { + DownloadHandlerFactory* factory = DownloadHandlerFactory::GetInstance(); + DownloadHandlerPlugin* plugin = factory->LoadDownloadHandler(downloadHandlerId); + handle = reinterpret_cast(plugin); + } + catch (...) + { + } + + return handle; +} + +EXTERN_C_END diff --git a/src/extensions/download_handlers/download_handler_plugin/CMakeLists.txt b/src/extensions/download_handlers/download_handler_plugin/CMakeLists.txt new file mode 100644 index 000000000..37b8e1390 --- /dev/null +++ b/src/extensions/download_handlers/download_handler_plugin/CMakeLists.txt @@ -0,0 +1,24 @@ +set (target_name download_handler_plugin) + +include (agentRules) +compileasc99 () + +add_library (${target_name} STATIC) +add_library (aduc::${target_name} ALIAS ${target_name}) + +# Turn -fPIC on, in order to use this library in a shared library. +set_property (TARGET ${target_name} PROPERTY POSITION_INDEPENDENT_CODE ON) + +target_include_directories (${target_name} PUBLIC inc ${ADUC_EXPORT_INCLUDES} + ${ADU_EXTENSION_INCLUDES}) + +target_sources (${target_name} PRIVATE src/download_handler_plugin.cpp) + +target_link_libraries ( + ${target_name} + PUBLIC aduc::adu_types + aduc::contract_utils + aduc::logging + aduc::shared_lib) + +target_compile_features (${target_name} PRIVATE cxx_lambdas cxx_uniform_initialization) diff --git a/src/extensions/download_handlers/download_handler_plugin/inc/aduc/download_handler_plugin.h b/src/extensions/download_handlers/download_handler_plugin/inc/aduc/download_handler_plugin.h new file mode 100644 index 000000000..3bf664609 --- /dev/null +++ b/src/extensions/download_handlers/download_handler_plugin/inc/aduc/download_handler_plugin.h @@ -0,0 +1,26 @@ +#ifndef DOWNLOAD_HANDLER_PLUGIN_H +#define DOWNLOAD_HANDLER_PLUGIN_H + +#include // EXTERN_C_* +#include // ADUC_Result +#include // ADUC_FileEntity +#include // ADUC_FileEntity + +EXTERN_C_BEGIN + +typedef void* DownloadHandlerHandle; + +/** + * @brief Called when the update workflow successfully completes. + * In the case of Delta download handler plugin, it moves the file at the payloadFilePath to the cache. + * + * @param[in] handle The handle to the download handler. + * @param[in] workflowHandle The workflow handle. + * @return ADUC_Result The result. + */ +ADUC_Result ADUC_DownloadHandlerPlugin_OnUpdateWorkflowCompleted( + const DownloadHandlerHandle handle, const ADUC_WorkflowHandle workflowHandle); + +EXTERN_C_END + +#endif // DOWNLOAD_HANDLER_PLUGIN_H diff --git a/src/extensions/download_handlers/download_handler_plugin/inc/aduc/download_handler_plugin.hpp b/src/extensions/download_handlers/download_handler_plugin/inc/aduc/download_handler_plugin.hpp new file mode 100644 index 000000000..e7e67fdb8 --- /dev/null +++ b/src/extensions/download_handlers/download_handler_plugin/inc/aduc/download_handler_plugin.hpp @@ -0,0 +1,42 @@ +/** + * @file delta_download_plugin.hpp + * @brief header for DownloadHandlerPlugin class that abstracts using a download handler extension shared library + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#ifndef DOWNLOAD_HANDLER_PLUGIN_HPP +#define DOWNLOAD_HANDLER_PLUGIN_HPP + +#include // ADUC_ExtensionContractInfo +#include // ADUC_LOG_SEVERITY +#include // ADUC_Result +#include // aduc::SharedLib +#include // typedef struct ADUC_FileEntity +#include + +using ADUC_WorkflowHandle = void*; + +class DownloadHandlerPlugin +{ +public: + DownloadHandlerPlugin(const DownloadHandlerPlugin&) = delete; + DownloadHandlerPlugin& operator=(const DownloadHandlerPlugin&) = delete; + DownloadHandlerPlugin(DownloadHandlerPlugin&&) = delete; + DownloadHandlerPlugin& operator=(DownloadHandlerPlugin&&) = delete; + + DownloadHandlerPlugin(const std::string& libPath, ADUC_LOG_SEVERITY logLevel); + ~DownloadHandlerPlugin(); + ADUC_Result ProcessUpdate( + const ADUC_WorkflowHandle workflowHandle, + const ADUC_FileEntity* fileEntity, + const char* payloadFilePath) const; + ADUC_Result OnUpdateWorkflowCompleted(const ADUC_WorkflowHandle workflowHandle) const; + ADUC_Result GetContractInfo(ADUC_ExtensionContractInfo* contractInfo) const; + +private: + aduc::SharedLib lib; +}; + +#endif // DOWNLOAD_HANDLER_PLUGIN_HPP diff --git a/src/extensions/download_handlers/download_handler_plugin/src/download_handler_plugin.cpp b/src/extensions/download_handlers/download_handler_plugin/src/download_handler_plugin.cpp new file mode 100644 index 000000000..bf3991b80 --- /dev/null +++ b/src/extensions/download_handlers/download_handler_plugin/src/download_handler_plugin.cpp @@ -0,0 +1,158 @@ +/** + * @file download_handler_plugin.cpp + * @brief Implements the download handler plugin. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include "aduc/download_handler_plugin.h" +#include "aduc/download_handler_plugin.hpp" + +#include +#include +#include +#include // for CallExport + +using InitializeFn = void (*)(ADUC_LOG_SEVERITY logLevel); + +using CleanupFn = void (*)(); + +using ProcessUpdateFn = ADUC_Result (*)( + const ADUC_WorkflowHandle workflowHandle, const ADUC_FileEntity* fileEntity, const char* targetFilePath); + +using OnUpdateWorkflowCompletedFn = ADUC_Result (*)(const ADUC_WorkflowHandle workflowHandle); +using GetContractInfoFn = ADUC_Result (*)(ADUC_ExtensionContractInfo* contractInfo); + +/** + * @brief Construct a new Download Handler Plugin object + * + * @param libPath The path to the dynamic library. + * @param logLevel The log severity. + */ +DownloadHandlerPlugin::DownloadHandlerPlugin(const std::string& libPath, ADUC_LOG_SEVERITY logLevel) : lib(libPath) +{ + const char* const symbol = DOWNLOAD_HANDLER__Initialize__EXPORT_SYMBOL; + CallExport( + symbol, lib, nullptr /* outResult */, logLevel); +} + +/** + * @brief Destroy the Download Handler Plugin object + * + */ +DownloadHandlerPlugin::~DownloadHandlerPlugin() +{ + const char* const symbol = DOWNLOAD_HANDLER__Cleanup__EXPORT_SYMBOL; + CallExport(symbol, lib, nullptr /* outResult */); +} + +/** + * @brief Processes the update to either produce the target file path so that the core agent can skip downloading the + * update payload or do some pre-download processing and then tell the agent to continue on downloading the payload. + * + * @param workflowHandle The workflow handle, containing the update deployment info. + * @param fileEntity The file entity with the update payload metadata, where RelatedFiles is most applicable to + * download handlers. + * @param targetFilePath The file path of the file that the plugin should create if wanting to return a ResultCode of + * ADUC_Result_Download_Handler_SuccessSkipDownload. + * @return ADUC_Result The result. When able to produce the target file path using workflowHandle and fileEntity inputs, + * it returns a result with ResultCode of ADUC_Result_Download_Handler_SuccessSkipDownload to tell the agent to skip + * downloading the update content. When it wants the agent to go ahead and download the update payload as usual, it + * returns ADUC_Result_Download_Handler_RequiredFullDownload success ResultCode. + */ +ADUC_Result DownloadHandlerPlugin::ProcessUpdate( + const ADUC_WorkflowHandle workflowHandle, const ADUC_FileEntity* fileEntity, const char* targetFilePath) const +{ + ADUC_Result result{ ADUC_GeneralResult_Failure, 0 }; + + const char* const symbol = DOWNLOAD_HANDLER__ProcessUpdate__EXPORT_SYMBOL; + CallExport( + symbol, lib, &result /* outResult */, workflowHandle, fileEntity, targetFilePath); + + Log_Info( + "DownloadHandlerPlugin ProcessUpdate result - rc: %d, erc: %08x", + result.ResultCode, + result.ExtendedResultCode); + + return result; +} + +/** + * @brief Calls the download handler plugin's export function to handle workflow completion. + * This is called on by the core agent for update payloads associated with this download handler when the update + * deployment workflow was successfully applied to the device. The plugin can do cleanup of resources it needed for + * payloads associated with this download handler plugin as well as any post-processing needed such as copying files + * from the sandbox to caches, etc. + * + * @param workflowHandle The workflow handle. + * @return ADUC_Result The result. + */ +ADUC_Result DownloadHandlerPlugin::OnUpdateWorkflowCompleted(const ADUC_WorkflowHandle workflowHandle) const +{ + ADUC_Result result{ ADUC_GeneralResult_Failure, 0 }; + + CallExport( + DOWNLOAD_HANDLER__OnUpdateWorkflowCompleted__EXPORT_SYMBOL, lib, &result /* outResult */, workflowHandle); + + Log_Info( + "DownloadHandlerPlugin OnUpdateWorkflowCompleted result - rc: %d, erc: %08x", + result.ResultCode, + result.ExtendedResultCode); + + return result; +} + +/** + * @brief Gets the contract info for the download handler plugin. + * + * @param[out] contractInfo The contract info. + * @return ADUC_Result The result. + */ +ADUC_Result DownloadHandlerPlugin::GetContractInfo(ADUC_ExtensionContractInfo* contractInfo) const +{ + ADUC_Result result{ ADUC_GeneralResult_Failure, 0 }; + + CallExport( + DOWNLOAD_HANDLER__GetContractInfo__EXPORT_SYMBOL, lib, &result /* outResult */, contractInfo); + + Log_Info( + "DownloadHandlerPlugin GetContractInfo result - rc: %d, erc: %08x", + result.ResultCode, + result.ExtendedResultCode); + + return result; +} + +EXTERN_C_BEGIN + +/** + * @brief The C API for invoking the post-processing after the update deployment has completed hook for an update with + * a download handler id . + * + * @param handle The download handler handle opaque object. + * @param workflowHandle The workflow handle. + * @return ADUC_Result The result. + */ +ADUC_Result ADUC_DownloadHandlerPlugin_OnUpdateWorkflowCompleted( + const DownloadHandlerHandle handle, const ADUC_WorkflowHandle workflowHandle) +{ + ADUC_Result result = { ADUC_Result_Failure, + ADUC_ERC_DOWNLOAD_HANDLER_PLUGIN_ON_UPDATE_WORKFLOW_COMPLETED_FAILURE }; + try + { + // Do not free the DownloadHandlerHandle handle that is owned by DownloadHandlerFactory. + if (handle != nullptr) + { + auto plugin = reinterpret_cast(handle); + result = plugin->OnUpdateWorkflowCompleted(workflowHandle); + } + } + catch (...) + { + } + + return result; +} + +EXTERN_C_END diff --git a/src/extensions/download_handlers/plugin_examples/CMakeLists.txt b/src/extensions/download_handlers/plugin_examples/CMakeLists.txt new file mode 100644 index 000000000..4f3f51d93 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/CMakeLists.txt @@ -0,0 +1,3 @@ +project (plugin_examples) + +add_subdirectory (microsoft_delta_download_handler) diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/CMakeLists.txt b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/CMakeLists.txt new file mode 100644 index 000000000..45333dda8 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/CMakeLists.txt @@ -0,0 +1,4 @@ +project (microsoft_delta_download_handler) + +add_subdirectory (handler) +add_subdirectory (source_update_cache) diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/CMakeLists.txt b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/CMakeLists.txt new file mode 100644 index 000000000..a676dabf7 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/CMakeLists.txt @@ -0,0 +1,5 @@ +project (microsoft_delta_download_handler_handler) + +add_subdirectory (lib) +add_subdirectory (plugin) +add_subdirectory (utils) diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/lib/CMakeLists.txt b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/lib/CMakeLists.txt new file mode 100644 index 000000000..d1b3bb23d --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/lib/CMakeLists.txt @@ -0,0 +1,23 @@ +set (target_name microsoft-delta-download-handler) + +include (agentRules) +compileasc99 () + +add_library (${target_name} STATIC) +add_library (aduc::${target_name} ALIAS ${target_name}) + +# Turn -fPIC on, in order to use this library in a shared library. +set_property (TARGET ${target_name} PROPERTY POSITION_INDEPENDENT_CODE ON) + +target_include_directories (${target_name} PUBLIC inc ${ADUC_EXPORT_INCLUDES}) + +target_sources (${target_name} PRIVATE src/microsoft_delta_download_handler.c) + +target_link_libraries ( + ${target_name} + PUBLIC aduc::adu_types + PRIVATE aduc::c_utils + aduc::microsoft_delta_download_handler_utils + aduc::logging + aduc::source_update_cache + aduc::workflow_utils) diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/lib/inc/aduc/microsoft_delta_download_handler.h b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/lib/inc/aduc/microsoft_delta_download_handler.h new file mode 100644 index 000000000..0b8611f70 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/lib/inc/aduc/microsoft_delta_download_handler.h @@ -0,0 +1,51 @@ +/** + * @file microsoft_delta_download_handler.h + * @brief Function prototypes for the delta download handler library functions used + * by the sample libmicrosoft_delta_download_handler.so plugin. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#ifndef __DELTA_DOWNLOAD_HANDLER_H__ +#define __DELTA_DOWNLOAD_HANDLER_H__ + +#include /* ADUC_Result */ +#include /* ADUC_FileEntity */ +#include /* ADUC_WorkflowHandle */ + +/** + * @brief Processes the target update from FileEntity metadata at the given output filepath. + * For this download handler, each relatedFile in the FileEntity metadata represents a delta update, + * which is much smaller than the target update content. It attempts to download the delta update and + * produce the target update using the delta processor. If successful, it tells the agent to skip download; + * otherwise, it tells the agent that a full download is required. + * + * @param[in] workflowHandle The workflow handle. + * @param[in] fileEntity The FileEntity metadata of the update content and its related files. + * @param[in] payloadFilePath The sandbox output filepath where the update content would normally be written. + * @param[in] updateCacheBasePath The update cache base path. Use NULL for default. + * @return ADUC_Result The result. + * On success, returns ADUC_Result_Download_Handler_SuccessSkipDownload to tell the + * agent to skip downloading the update content (since it was able to produce it at the payloadFilePath). + * On failure, returns ADUC_Result_Download_Handler_RequiredFullDownload success ResultCode + * to tell the agent to download the update content as a fallback measure. + */ +ADUC_Result MicrosoftDeltaDownloadHandler_ProcessUpdate( + const ADUC_WorkflowHandle workflowHandle, + const ADUC_FileEntity* fileEntity, + const char* payloadFilePath, + const char* updateCacheBasePath); + +/** + * @brief Called when the update workflow successfully completes. + * In the case of Delta download handler plugin, it moves all the payload files from download sandbox to the cache. + * + * @param[in] workflowHandle The workflow handle. + * @param[in] updateCacheBasePath The update cache base path. Use NULL for default. + * @return ADUC_Result The result. + */ +ADUC_Result MicrosoftDeltaDownloadHandler_OnUpdateWorkflowCompleted( + const ADUC_WorkflowHandle workflowHandle, const char* updateCacheBasePath); + +#endif /* __DELTA_DOWNLOAD_HANDLER_H__ */ diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/lib/src/microsoft_delta_download_handler.c b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/lib/src/microsoft_delta_download_handler.c new file mode 100644 index 000000000..cb4401c74 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/lib/src/microsoft_delta_download_handler.c @@ -0,0 +1,139 @@ +/** + * @file microsoft_delta_download_handler.c + * @brief Implementation for the delta download handler library functions used + * by the sample libmicrosoft_delta_download_handler.so plugin. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include "aduc/microsoft_delta_download_handler.h" +#include "aduc/microsoft_delta_download_handler_utils.h" +#include // ADUC_Logging_*, Log_* +#include // ADUC_SourceUpdateCache_Move +#include // IsNullOrEmpty +#include // ADUC_Result_Success, etc +#include // workflow_get_workfolder + +/** + * @brief Processes the target update from FileEntity metadata at the given output filepath. + * For this download handler, each relatedFile in the FileEntity metadata represents a delta update, + * which is much smaller than the target update content. It attempts to download the delta update and + * produce the target update using the delta processor. If successful, it tells the agent to skip download; + * otherwise, it tells the agent that a full download is required. + * + * @param[in] workflowHandle The workflow handle. + * @param[in] fileEntity The FileEntity metadata of the update content and its related files. + * @param[in] payloadFilePath The sandbox output filepath where the update content would normally be written. + * @param[in] updateCacheBasePath The update cache base path. Use NULL for default. + * @return ADUC_Result The result. + * On success, returns ADUC_Result_Download_Handler_SuccessSkipDownload to tell the + * agent to skip downloading the update content (since it was able to produce it at the payloadFilePath). + * On failure, returns ADUC_Result_Download_Handler_RequiredFullDownload success ResultCode + * to tell the agent to download the update content as a fallback measure. + */ +ADUC_Result MicrosoftDeltaDownloadHandler_ProcessUpdate( + const ADUC_WorkflowHandle workflowHandle, + const ADUC_FileEntity* fileEntity, + const char* payloadFilePath, + const char* updateCacheBasePath) +{ + ADUC_Result result = { .ResultCode = ADUC_Result_Failure, .ExtendedResultCode = 0 }; + + // These represent hard failures for this download handler. + // Most notably, this download handler requires related files. + // In general, related files may be optional for a download handler. + if (workflowHandle == NULL || fileEntity == NULL || payloadFilePath == NULL || fileEntity->RelatedFiles == NULL + || fileEntity->RelatedFileCount <= 0) + { + result.ExtendedResultCode = ADUC_ERC_DDH_BAD_ARGS; + goto done; + } + + // Each relatedFile represents a delta update associated with a different + // source update in the source update update cache. + // + // To save bandwidth (delta updates are much smaller than a full update), + // try processing each delta update until one succeeds. + // + // If processing of all relatedFile fails, then return + // ADUC_Result_Download_RequiredFullDownload success result code, which + // will cause the agent to not fail and download the original, full update. + for (int index = 0; index < fileEntity->RelatedFileCount; ++index) + { + ADUC_Result relatedFileResult = {}; + ADUC_RelatedFile* relatedFile = &fileEntity->RelatedFiles[index]; + + if (relatedFile->Properties == NULL || relatedFile->PropertiesCount < 1) + { + result.ExtendedResultCode = ADUC_ERC_DDH_RELATEDFILE_NO_PROPERTIES; + goto done; + } + + relatedFileResult = MicrosoftDeltaDownloadHandlerUtils_ProcessRelatedFile( + workflowHandle, + relatedFile, + payloadFilePath, + updateCacheBasePath, + MicrosoftDeltaDownloadHandlerUtils_ProcessDeltaUpdate, + MicrosoftDeltaDownloadHandlerUtils_DownloadDeltaUpdate); + + if (relatedFileResult.ResultCode == ADUC_Result_Success_Cache_Miss) + { + Log_Warn("src update cache miss for Delta %d", index); + workflow_set_success_erc(workflowHandle, ADUC_ERC_DDH_SOURCE_UPDATE_CACHE_MISS); + continue; + } + + if (IsAducResultCodeSuccess(relatedFileResult.ResultCode)) + { + Log_Info("Processing Delta %d succeeded", index); + result.ResultCode = ADUC_Result_Success; + break; + } + + Log_Warn("Delta %d failed, ERC: 0x%08x.", index, relatedFileResult.ExtendedResultCode); + workflow_set_success_erc(workflowHandle, relatedFileResult.ExtendedResultCode); + // continue processing the next relatedFile + } + + if (IsAducResultCodeSuccess(result.ResultCode)) + { + result.ResultCode = ADUC_Result_Download_Handler_SuccessSkipDownload; + } + else + { + result.ResultCode = ADUC_Result_Download_Handler_RequiredFullDownload; + }; + +done: + + return result; +} + +/** + * @brief Called when the update workflow successfully completes. + * In the case of Delta download handler plugin, it moves all the payloads from sandbox to cache + * so that they will available as source updates for future delta updates. + * + * @param[in] workflowHandle The workflow handle. + * @param[in] updateCacheBasePath The update cache base path. Use NULL for default. + * @return ADUC_Result The result. + */ +ADUC_Result MicrosoftDeltaDownloadHandler_OnUpdateWorkflowCompleted( + const ADUC_WorkflowHandle workflowHandle, const char* updateCacheBasePath) +{ + ADUC_Result result = { .ResultCode = ADUC_Result_Failure }; + + if (workflowHandle == NULL) + { + result.ExtendedResultCode = ADUC_ERC_DDH_BAD_ARGS; + goto done; + } + + result = ADUC_SourceUpdateCache_Move(workflowHandle, updateCacheBasePath); + +done: + + return result; +} diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/plugin/CMakeLists.txt b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/plugin/CMakeLists.txt new file mode 100644 index 000000000..c82fa7898 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/plugin/CMakeLists.txt @@ -0,0 +1,19 @@ +set (target_name microsoft_delta_download_handler) + +include (agentRules) +compileasc99 () + +add_library (${target_name} MODULE "") + +target_include_directories (${target_name} PUBLIC inc ${ADUC_EXPORT_INCLUDES}) + +target_sources (${target_name} PRIVATE src/microsoft_delta_download_handler_plugin.EXPORTS.c) + +target_link_libraries ( + ${target_name} + PRIVATE aduc::adu_types + aduc::contract_utils + aduc::microsoft-delta-download-handler + aduc::logging) + +install (TARGETS ${target_name} LIBRARY DESTINATION ${ADUC_EXTENSIONS_INSTALL_FOLDER}) diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/plugin/src/microsoft_delta_download_handler_plugin.EXPORTS.c b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/plugin/src/microsoft_delta_download_handler_plugin.EXPORTS.c new file mode 100644 index 000000000..76a18bcf2 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/plugin/src/microsoft_delta_download_handler_plugin.EXPORTS.c @@ -0,0 +1,99 @@ + +/** + * @file microsoft_delta_download_handler_plugin.c + * @brief An example implementation of a DownloadHandler plugin module + * that produces full target updates using a source update update cache and + * can cache updates once they've been verified to be good upon workflow success. + * + * This plugin module provides the following exported function symbols to satisfy the DownloadHandler agent interface: + * Initialize - Do one-time initialization (e.g. initialize logging), + * Cleanup - Free resources and cleanup right before unloading, + * ProcessUpdate - Do processing using data provided by ADUC_WorkflowHandle and update file metadata (ADUC_FileEntity), + * OnUpdateWorkflowCompleted - Callback for post-processing when the current update has been installed and applied successfully. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include "aduc/microsoft_delta_download_handler.h" + +#include // ADUC_ExtensionContractInfo +#include // ADUC_Logging_*, Log_* +#include // ADUC_Result_* + +///////////////////////////////////////////////////////////////////////////// +// BEGIN Shared Library Export Functions +// +// These are the function symbols that the device update agent will +// lookup and call. +// + +/** + * @brief One-time initialization for the download handler. + * + * @param logLevel The desired loglevel if logging is used. + */ +void Initialize(ADUC_LOG_SEVERITY logLevel) +{ + ADUC_Logging_Init(logLevel, "delta-download-handler"); +} + +/** + * @brief Cleanup logic before library is unloaded. + */ +void Cleanup() +{ + ADUC_Logging_Uninit(); +} + +/** + * @brief Processes the target update from FileEntity metadata at the given output filepath. + * For this download handler, each relatedFile in the FileEntity metadata represents a delta update, + * which is much smaller than the target update content. It attempts to download the delta update and + * produce the target update using the delta processor. If successful, it tells the agent to skip download; + * otherwise, it tells the agent that a full download is required. + * + * @param[in] workflowHandle The workflow handle. + * @param[in] fileEntity The FileEntity metadata of the update content and its related files. + * @param[in] targetUpdateFilePath The target update path to write the update content when returning ADUC_Result_Download_Handler_SuccessSkipDownload. + * @return ADUC_Result The result. + * Returning ADUC_Result_Download_Handler_SuccessSkipDownload ResultCode tells the agent to skip downloading the update content (since this download handler able to produce it at the targetUpdateFilePath). + * Returning ADUC_Result_Download_Handler_RequiredFullDownload ResultCode tells the agent to download the update content (this download handler did not produce it by other means). + */ +ADUC_Result ProcessUpdate( + const ADUC_WorkflowHandle workflowHandle, const ADUC_FileEntity* fileEntity, const char* targetUpdateFilePath) +{ + return MicrosoftDeltaDownloadHandler_ProcessUpdate( + workflowHandle, fileEntity, targetUpdateFilePath, NULL /* updateCacheBasePath */); +} + +/** + * @brief Called when the update workflow successfully completes. + * In the case of Delta download handler plugin, it moves all the payloads from sandbox to cache + * so that they will available as source updates for future delta updates. + * + * @param[in] workflowHandle The workflow handle. + * @return ADUC_Result The result. + */ +ADUC_Result OnUpdateWorkflowCompleted(const ADUC_WorkflowHandle workflowHandle) +{ + return MicrosoftDeltaDownloadHandler_OnUpdateWorkflowCompleted(workflowHandle, NULL /* updateCacheBasePath */); +} + +/** + * @brief Gets the extension contract info. + * + * @param[out] contractInfo The extension contract info. + * @return ADUC_Result The result. + */ +ADUC_Result GetContractInfo(ADUC_ExtensionContractInfo* contractInfo) +{ + ADUC_Result result = { ADUC_GeneralResult_Success, 0 }; + contractInfo->majorVer = ADUC_V1_CONTRACT_MAJOR_VER; + contractInfo->minorVer = ADUC_V1_CONTRACT_MINOR_VER; + return result; +} + +// +// END Shared Library Export Functions +///////////////////////////////////////////////////////////////////////////// diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/CMakeLists.txt b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/CMakeLists.txt new file mode 100644 index 000000000..749ee0af2 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/CMakeLists.txt @@ -0,0 +1,29 @@ +set (target_name microsoft_delta_download_handler_utils) + +include (agentRules) +compileasc99 () + +add_library (${target_name} STATIC "") +add_library (aduc::${target_name} ALIAS ${target_name}) + +# Turn -fPIC on, in order to use this library in a shared library. +set_property (TARGET ${target_name} PROPERTY POSITION_INDEPENDENT_CODE ON) + +target_include_directories (${target_name} PUBLIC inc ${ADUC_EXPORT_INCLUDES}) + +target_sources (${target_name} PRIVATE src/microsoft_delta_download_handler_utils.c + src/microsoft_delta_download_handler_utils.cpp) + +target_link_libraries ( + ${target_name} + PUBLIC aduc::adu_types aziotsharedutil + PRIVATE aduc::c_utils + aduc::extension_manager + aduc::logging + aduc::shared_lib + aduc::source_update_cache + aduc::workflow_utils) + +if (ADUC_BUILD_UNIT_TESTS) + add_subdirectory (tests) +endif () diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/inc/aduc/microsoft_delta_download_handler_utils.h b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/inc/aduc/microsoft_delta_download_handler_utils.h new file mode 100644 index 000000000..59aa7ed85 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/inc/aduc/microsoft_delta_download_handler_utils.h @@ -0,0 +1,107 @@ +/** + * @file microsoft_delta_download_handler_utils.h + * @brief The microsoft delta download handler helper function prototypes. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#ifndef MICROSOFT_DELTA_DOWNLOAD_HANDLER_UTILS_H +#define MICROSOFT_DELTA_DOWNLOAD_HANDLER_UTILS_H + +#include // EXTERN_C_BEGIN, EXTERN_C_END +#include // ADUC_Result_* +#include // ADUC_RelatedFile +#include // ADUC_WorkflowHandle +#include // STRING_* + +EXTERN_C_BEGIN + +typedef ADUC_Result (*ProcessDeltaUpdateFn)( + const char* sourceUpdateFilePath, const char* deltaUpdateFilePath, const char* targetUpdateFilePath); + +typedef ADUC_Result (*DownloadDeltaUpdateFn)( + const ADUC_WorkflowHandle workflowHandle, const ADUC_RelatedFile* relatedFile); + +/** + * @brief Processes a related file of an update for delta download handling. + * + * @param workflowHandle The workflow handle. + * @param relatedFile The related file. + * @param payloadFilePath The payload file path. + * @param updateCacheBasePath The update cache base path. Use NULL for default. + * @param processDeltaUpdateFn The function to call to process delta updates. + * @param downloadDeltaUpdateFn The function to call to download the delta update. + * @return ADUC_Result The result. + * @details Returns ADUC_Result_Success_Cache_Miss when delta relatedFile source update not found in cache. + */ +ADUC_Result MicrosoftDeltaDownloadHandlerUtils_ProcessRelatedFile( + const ADUC_WorkflowHandle workflowHandle, + const ADUC_RelatedFile* relatedFile, + const char* payloadFilePath, + const char* updateCacheBasePath, + ProcessDeltaUpdateFn processDeltaUpdateFn, + DownloadDeltaUpdateFn downloadDeltaUpdateFn); + +/** + * @brief Looks up the source update in the source update cache and outputs the path to it, if it exists. + * + * @param[in] workflowHandle The workflow handle. + * @param[in] relatedFile The related file with the relationship to the source update. + * @param[in] updateCacheBasePath The update cache base path. Use NULL for default. + * @param[out] outPathHandle The STRING_HANDLE to hold the resultant filepath to the source update. + * @return ADUC_Result The result. + * @details Returns ResultCode of ADUC_Result_Success_Cache_Miss on cache miss, ADUC_Result_Success on cache hit, and ADUC_Result_Failure otherwise. + */ +ADUC_Result MicrosoftDeltaDownloadHandlerUtils_LookupSourceUpdateCachePath( + const ADUC_WorkflowHandle workflowHandle, + const ADUC_RelatedFile* relatedFile, + const char* updateCacheBasePath, + STRING_HANDLE* outPathHandle); + +/** + * @brief Gets the source hash and hash algorithm from the related file. + * + * @param[in] relatedFile The related file. + * @param[out] outHash The output parameter for the hash. + * @param[out] outAlg The output parameter for the alg. + * @return ADUC_Result The result. + */ +ADUC_Result MicrosoftDeltaDownloadHandlerUtils_GetSourceUpdateProperties( + const ADUC_RelatedFile* relatedFile, STRING_HANDLE* outHash, STRING_HANDLE* outAlg); + +/** + * @brief Downloads a delta update related file. + * + * @param workflowHandle The workflow handle. + * @param relatedFile The related file. + * @return ADUC_Result The result. + */ +ADUC_Result MicrosoftDeltaDownloadHandlerUtils_DownloadDeltaUpdate( + const ADUC_WorkflowHandle workflowHandle, const ADUC_RelatedFile* relatedFile); + +/** + * @brief Gets the file path to the delta update downloaded in the download sandbox work folder. + * + * @param[in] workflowHandle The workflow handle. + * @param[in] relatedFile The related file with the relationship to the source update. + * @param[out] outPathHandle The STRING_HANDLE to hold the resultant filepath to the delta update. + * @return ADUC_Result The result. + */ +ADUC_Result MicrosoftDeltaDownloadHandlerUtils_GetDeltaUpdateDownloadSandboxPath( + const ADUC_WorkflowHandle workflowHandle, const ADUC_RelatedFile* relatedFile, STRING_HANDLE* outPathHandle); + +/** + * @brief Creates a target update from the source and delta updates. + * + * @param sourceUpdateFilePath The source update path. + * @param deltaUpdateFilePath The delta update path. + * @param targetUpdatePath The target update path. + * @return ADUC_Result The result. + */ +ADUC_Result MicrosoftDeltaDownloadHandlerUtils_ProcessDeltaUpdate( + const char* sourceUpdateFilePath, const char* deltaUpdateFilePath, const char* targetUpdateFilePath); + +EXTERN_C_END + +#endif // MICROSOFT_DELTA_DOWNLOAD_HANDLER_UTILS_H diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/src/microsoft_delta_download_handler_utils.c b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/src/microsoft_delta_download_handler_utils.c new file mode 100644 index 000000000..889b91840 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/src/microsoft_delta_download_handler_utils.c @@ -0,0 +1,337 @@ +/** + * @file microsoft_delta_download_handler_utils.c + * @brief The Microsoft delta download handler helper function implementations. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include "aduc/microsoft_delta_download_handler_utils.h" +#include "aduc/c_utils.h" // EXTERN_C_BEGIN, EXTERN_C_END +#include "aduc/source_update_cache.h" // ADUC_SourceUpdateCache_Lookup +#include // ExtensionManager_Download +#include // IsNullOrEmpty +#include // workflow_* +#include // mallocAndStrcpy_s +#include // STRING_* +#include // free + +/* external linkage */ +extern ExtensionManager_Download_Options Default_ExtensionManager_Download_Options; + +EXTERN_C_BEGIN + +/** + * @brief Processes a related file of an update for delta download handling. + * + * @param workflowHandle The workflow handle. + * @param relatedFile The related file. + * @param payloadFilePath The payload file path. + * @param updateCacheBasePath The update cache base path. Use NULL for default. + * @param processDeltaUpdateFn The function to call to process delta updates. + * @param downloadDeltaUpdateFn The function to call to download the delta update. + * @return ADUC_Result The result. + * @details Returns ADUC_Result_Success_Cache_Miss when delta relatedFile source update not found in cache. + */ +ADUC_Result MicrosoftDeltaDownloadHandlerUtils_ProcessRelatedFile( + const ADUC_WorkflowHandle workflowHandle, + const ADUC_RelatedFile* relatedFile, + const char* payloadFilePath, + const char* updateCacheBasePath, + ProcessDeltaUpdateFn processDeltaUpdateFn, + DownloadDeltaUpdateFn downloadDeltaUpdateFn) +{ + ADUC_Result result = { .ResultCode = ADUC_Result_Failure }; + STRING_HANDLE sourceUpdatePathHandle = NULL; + STRING_HANDLE deltaUpdatePathHandle = NULL; + + if (workflowHandle == NULL || relatedFile == NULL || payloadFilePath == NULL || processDeltaUpdateFn == NULL) + { + result.ExtendedResultCode = ADUC_ERC_DDH_BAD_ARGS; + return result; + } + + // + // See if source full update is in the update cache. + // + result = MicrosoftDeltaDownloadHandlerUtils_LookupSourceUpdateCachePath( + workflowHandle, relatedFile, updateCacheBasePath, &sourceUpdatePathHandle); + if (IsAducResultCodeFailure(result.ResultCode)) + { + goto done; + } + + if (result.ResultCode == ADUC_Result_Success_Cache_Miss) + { + goto done; + } + + Log_Debug("cached source update found at '%s'. Downloading delta update...", STRING_c_str(sourceUpdatePathHandle)); + + // + // Download the delta update file. + // + result = downloadDeltaUpdateFn(workflowHandle, relatedFile); + if (IsAducResultCodeFailure(result.ResultCode)) + { + Log_Error("DeltaUpdate download failed, erc 0x%08x.", result.ExtendedResultCode); + goto done; + } + + // + // Get the path to the downloaded delta update file in the sandbox. + // + result = MicrosoftDeltaDownloadHandlerUtils_GetDeltaUpdateDownloadSandboxPath( + workflowHandle, relatedFile, &deltaUpdatePathHandle); + if (IsAducResultCodeFailure(result.ResultCode)) + { + Log_Error("get delta update sandbox path, erc 0x%08x.", result.ExtendedResultCode); + goto done; + } + + Log_Debug("Processing delta update at '%s'...", STRING_c_str(deltaUpdatePathHandle)); + + // + // Use the delta processor to produce the full target update from the source update and delta update. + // + const char* srcPath = STRING_c_str(sourceUpdatePathHandle); + const char* deltaPath = STRING_c_str(deltaUpdatePathHandle); + result = processDeltaUpdateFn(srcPath, deltaPath, payloadFilePath); + if (IsAducResultCodeFailure(result.ResultCode)) + { + Log_Error("processing delta update failed, ERC 0x%08x", result.ExtendedResultCode); + goto done; + } + + result.ResultCode = ADUC_Result_Success; + +done: + + STRING_delete(deltaUpdatePathHandle); + STRING_delete(sourceUpdatePathHandle); + + return result; +} + +/** + * @brief Looks up the source update in the source update cache and outputs the path to it, if it exists. + * + * @param[in] workflowHandle The workflow handle. + * @param[in] relatedFile The related file with the relationship to the source update. + * @param[in] updateCacheBasePath The update cache base path. Use NULL for default. + * @param[out] outPathHandle The STRING_HANDLE to hold the resultant filepath to the source update. + * @return ADUC_Result The result. + * @details Returns ResultCode of ADUC_Result_Success_Cache_Miss on cache miss, ADUC_Result_Success on cache hit, and ADUC_Result_Failure otherwise. + */ +ADUC_Result MicrosoftDeltaDownloadHandlerUtils_LookupSourceUpdateCachePath( + const ADUC_WorkflowHandle workflowHandle, + const ADUC_RelatedFile* relatedFile, + const char* updateCacheBasePath, + STRING_HANDLE* outPathHandle) +{ + ADUC_Result result = { .ResultCode = ADUC_Result_Failure }; + STRING_HANDLE sourceUpdatePath = NULL; + ADUC_UpdateId* updateId = NULL; + + STRING_HANDLE sourceUpdateHash = NULL; + STRING_HANDLE sourceUpdateAlg = NULL; + + result = + MicrosoftDeltaDownloadHandlerUtils_GetSourceUpdateProperties(relatedFile, &sourceUpdateHash, &sourceUpdateAlg); + if (IsAducResultCodeFailure(result.ResultCode)) + { + Log_Error("get source update properties failed, erc 0x%08x", result.ExtendedResultCode); + goto done; + } + + result = workflow_get_expected_update_id(workflowHandle, &updateId); + if (IsAducResultCodeFailure(result.ResultCode)) + { + Log_Error("get updateId, erc 0x%08x", result.ExtendedResultCode); + goto done; + } + + Log_Debug("Get SourceUpdatePath for relatedFile '%s'", relatedFile->FileName); + + result = ADUC_SourceUpdateCache_Lookup( + updateId->Provider, + STRING_c_str(sourceUpdateHash), + STRING_c_str(sourceUpdateAlg), + updateCacheBasePath, + &sourceUpdatePath); + if (IsAducResultCodeFailure(result.ResultCode)) + { + Log_Error("source lookup failed, erc 0x%08x", result.ExtendedResultCode); + goto done; + } + + if (result.ResultCode == ADUC_Result_Success_Cache_Miss) + { + Log_Warn("source update cache miss"); + goto done; + } + + *outPathHandle = sourceUpdatePath; + sourceUpdatePath = NULL; + + result.ResultCode = ADUC_Result_Success; + +done: + STRING_delete(sourceUpdateHash); + STRING_delete(sourceUpdateAlg); + workflow_free_update_id(updateId); + free(sourceUpdatePath); + + return result; +} + +/** + * @brief Gets the source hash and hash algorithm from the related file. + * + * @param[in] relatedFile The related file. + * @param[out] outHash The output parameter for the hash. + * @param[out] outAlg The output parameter for the alg. + * @return ADUC_Result The result. + */ +ADUC_Result MicrosoftDeltaDownloadHandlerUtils_GetSourceUpdateProperties( + const ADUC_RelatedFile* relatedFile, STRING_HANDLE* outHash, STRING_HANDLE* outAlg) +{ + ADUC_Result result = { .ResultCode = ADUC_Result_Failure, .ExtendedResultCode = 0 }; + + const char* sourceHash = NULL; + const char* sourceAlg = NULL; + + STRING_HANDLE tempHash = NULL; + STRING_HANDLE tempAlg = NULL; + + if (relatedFile == NULL || outHash == NULL || outAlg == NULL) + { + result.ExtendedResultCode = ADUC_ERC_DDH_BAD_ARGS; + return result; + } + + for (int indexProperty = 0; indexProperty < relatedFile->PropertiesCount; ++indexProperty) + { + const char* propertyName = relatedFile->Properties[indexProperty].Name; + if (strcmp(propertyName, "microsoft.sourceFileHash") == 0) + { + sourceHash = relatedFile->Properties[indexProperty].Value; + } + else if (strcmp(propertyName, "microsoft.sourceFileHashAlgorithm") == 0) + { + sourceAlg = relatedFile->Properties[indexProperty].Value; + } + } + + if (IsNullOrEmpty(sourceHash) || IsNullOrEmpty(sourceAlg)) + { + Log_Error("Missing microsoft.sourceFileHash or microsoft.sourceFileHashAlgorithm relatedFile property."); + + result.ExtendedResultCode = ADUC_ERC_DDH_RELATEDFILE_BAD_OR_MISSING_HASH_PROPERTIES; + + goto done; + } + + // Only set outputs once all allocations succeeded + tempHash = STRING_construct(sourceHash); + tempAlg = STRING_construct(sourceAlg); + + if (tempHash == NULL || tempAlg == NULL) + { + result.ExtendedResultCode = ADUC_ERC_NOMEM; + goto done; + } + + // now can transfer + *outHash = tempHash; + tempHash = NULL; + + *outAlg = tempAlg; + tempAlg = NULL; + + result.ResultCode = ADUC_Result_Success; + +done: + STRING_delete(tempHash); + STRING_delete(tempAlg); + + return result; +} + +/** + * @brief Downloads a delta update related file. + * + * @param workflowHandle The workflow handle. + * @param relatedFile The related file. + * @return ADUC_Result The result. + */ +ADUC_Result MicrosoftDeltaDownloadHandlerUtils_DownloadDeltaUpdate( + const ADUC_WorkflowHandle workflowHandle, const ADUC_RelatedFile* relatedFile) +{ + Log_Debug("Try download delta update from '%s'", relatedFile->DownloadUri); + + ADUC_FileEntity deltaUpdateFileEntity = { + .DownloadUri = relatedFile->DownloadUri, + .FileId = relatedFile->FileId, + .Hash = relatedFile->Hash, + .HashCount = relatedFile->HashCount, + .SizeInBytes = relatedFile->SizeInBytes, + .TargetFilename = relatedFile->FileName, + }; + + return ExtensionManager_Download( + &deltaUpdateFileEntity, + workflowHandle, + &Default_ExtensionManager_Download_Options, + NULL /* downloadProgressCallback */); +} + +/** + * @brief Gets the file path to the delta update downloaded in the download sandbox work folder. + * + * @param[in] workflowHandle The workflow handle. + * @param[in] relatedFile The related file with the relationship to the source update. + * @param[out] outPathHandle The STRING_HANDLE to hold the resultant filepath to the delta update. + * @return ADUC_Result The result. + */ +ADUC_Result MicrosoftDeltaDownloadHandlerUtils_GetDeltaUpdateDownloadSandboxPath( + const ADUC_WorkflowHandle workflowHandle, const ADUC_RelatedFile* relatedFile, STRING_HANDLE* outPathHandle) +{ + ADUC_Result result = { .ResultCode = ADUC_Result_Failure }; + STRING_HANDLE sandboxPathHandle = NULL; + + char* workFolder = workflow_get_workfolder(workflowHandle); + + if (workFolder == NULL) + { + result.ExtendedResultCode = ADUC_ERC_NOMEM; + goto done; + } + + sandboxPathHandle = STRING_new(); + + if (sandboxPathHandle == NULL) + { + result.ExtendedResultCode = ADUC_ERC_NOMEM; + goto done; + } + + if (STRING_sprintf(sandboxPathHandle, "%s/%s", workFolder, relatedFile->FileName) != 0) + { + result.ExtendedResultCode = ADUC_ERC_DDH_MAKE_DELTA_UPDATE_PATH; + goto done; + } + + *outPathHandle = sandboxPathHandle; + sandboxPathHandle = NULL; + + result.ResultCode = ADUC_Result_Success; + +done: + free(workFolder); + STRING_delete(sandboxPathHandle); + + return result; +} + +EXTERN_C_END diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/src/microsoft_delta_download_handler_utils.cpp b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/src/microsoft_delta_download_handler_utils.cpp new file mode 100644 index 000000000..983018ffa --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/src/microsoft_delta_download_handler_utils.cpp @@ -0,0 +1,150 @@ +/** + * @file microsoft_delta_download_handler_utils.c + * @brief The Microsoft delta download handler helper function implementations. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include "aduc/microsoft_delta_download_handler_utils.h" +#include "aduc/c_utils.h" // EXTERN_C_BEGIN, EXTERN_C_END +#include "aduc/logging.h" +#include "aduc/result.h" // MAKE_DELTA_PROCESSOR_EXTENDEDRESULTCODE +#include "aduc/shared_lib.hpp" + +const char* AduDiffSharedLibName = "libadudiffapi.so"; + +// function pointer types +using adu_apply_handle = void*; + +using adu_diff_apply_create_session_fn = adu_apply_handle (*)(); +using adu_diff_apply_close_session_fn = void (*)(adu_apply_handle handle); + +using adu_diff_apply_fn = + int (*)(adu_apply_handle session, const char* sourcePath, const char* deltaPath, const char* targetPath); + +using adu_diff_apply_get_error_count_fn = size_t (*)(adu_apply_handle handle); +using adu_diff_apply_get_error_text_fn = const char* (*)(adu_apply_handle handle, size_t index); +using adu_diff_apply_get_error_code_fn = int (*)(adu_apply_handle handle, size_t index); + +EXTERN_C_BEGIN + +/** + * @brief Creates a target update from the source and delta updates. + * + * @param sourceUpdateFilePath The source update path. + * @param deltaUpdateFilePath The delta update path. + * @param targetUpdatePath The target update path. + * @return ADUC_Result The result. + */ +ADUC_Result MicrosoftDeltaDownloadHandlerUtils_ProcessDeltaUpdate( + const char* sourceUpdateFilePath, const char* deltaUpdateFilePath, const char* targetUpdateFilePath) +{ + Log_Debug( + "Making '%s' from src '%s' and delta '%s'", + targetUpdateFilePath, + sourceUpdateFilePath, + deltaUpdateFilePath); + + ADUC_Result result = { ADUC_Result_Failure }; + + adu_apply_handle session = nullptr; + + adu_diff_apply_create_session_fn createSessionFn = nullptr; + adu_diff_apply_close_session_fn closeSessionFn = nullptr; + adu_diff_apply_fn applyFn = nullptr; + adu_diff_apply_get_error_count_fn getErrorCountFn = nullptr; + adu_diff_apply_get_error_text_fn getErrorTextFn = nullptr; + adu_diff_apply_get_error_code_fn getErrorCodeFn = nullptr; + + try + { + Log_Debug("load diff processor %s ...", AduDiffSharedLibName); + + result.ExtendedResultCode = ADUC_ERC_DDH_PROCESSOR_LOAD_LIB; + aduc::SharedLib diffApi{ AduDiffSharedLibName }; + + Log_Debug("ensure symbols ..."); + + result.ExtendedResultCode = ADUC_ERC_DDH_PROCESSOR_ENSURE_SYMBOLS; + diffApi.EnsureSymbols({ "adu_diff_apply", + "adu_diff_apply_close_session", + "adu_diff_apply_create_session", + "adu_diff_apply_get_error_code", + "adu_diff_apply_get_error_count", + "adu_diff_apply_get_error_text" }); + + createSessionFn = + reinterpret_cast(diffApi.GetSymbol("adu_diff_apply_create_session")); + closeSessionFn = + reinterpret_cast(diffApi.GetSymbol("adu_diff_apply_close_session")); + applyFn = reinterpret_cast(diffApi.GetSymbol("adu_diff_apply")); + getErrorCountFn = + reinterpret_cast(diffApi.GetSymbol("adu_diff_apply_get_error_count")); + getErrorTextFn = + reinterpret_cast(diffApi.GetSymbol("adu_diff_apply_get_error_text")); + getErrorCodeFn = + reinterpret_cast(diffApi.GetSymbol("adu_diff_apply_get_error_code")); + + Log_Debug("create session ..."); + + session = createSessionFn(); + if (session == nullptr) + { + Log_Error("create diffapply session failed"); + result.ExtendedResultCode = ADUC_ERC_DDH_PROCESSOR_CREATE_SESSION; + } + else + { + Log_Debug("Apply diff ..."); + + int res = applyFn(session, sourceUpdateFilePath, deltaUpdateFilePath, targetUpdateFilePath); + + if (res == 0) + { + result.ResultCode = ADUC_Result_Success; + } + else + { + Log_Error("diff apply - overall err: %d", res); + result.ExtendedResultCode = MAKE_DELTA_PROCESSOR_EXTENDEDRESULTCODE(res); + + size_t errorCount = getErrorCountFn(session); + for (size_t errIndex = 0; errIndex < errorCount; ++errIndex) + { + int error_code = getErrorCodeFn(session, errIndex); + const char* error_text = getErrorTextFn(session, errIndex); // do not free + // + Log_Error("diff apply - errcode %d: '%s'", error_code, error_text); + + result.ExtendedResultCode = MAKE_DELTA_PROCESSOR_EXTENDEDRESULTCODE(error_code); + } + } + } + } + catch (const std::exception& e) + { + Log_Error("Unhandled std exception: %s", e.what()); + } + catch (...) + { + Log_Error("Unhandled exception"); + } + + if (session != nullptr && closeSessionFn != nullptr) + { + Log_Debug("close session ..."); + closeSessionFn(session); + } + + if (IsAducResultCodeSuccess(result.ResultCode)) + { + result.ExtendedResultCode = 0; + } + + Log_Debug("ResultCode %d, erc %d", result.ResultCode, result.ExtendedResultCode); + + return result; +} + +EXTERN_C_END diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/tests/CMakeLists.txt b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/tests/CMakeLists.txt new file mode 100644 index 000000000..00cba73ae --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/tests/CMakeLists.txt @@ -0,0 +1,22 @@ +project (microsoft_delta_download_handler_util_unit_tests) + +include (agentRules) +compileasc99 () +disablertti () + +find_package (Catch2 REQUIRED) + +add_executable (${PROJECT_NAME} "") + +target_sources (${PROJECT_NAME} PRIVATE main.cpp microsoft_delta_download_handler_utils_ut.cpp) + +target_link_libraries ( + ${PROJECT_NAME} + PRIVATE aduc::adu_types + aduc::microsoft_delta_download_handler_utils + aduc::workflow_utils + Catch2::Catch2) + +include (CTest) +include (Catch) +catch_discover_tests (${PROJECT_NAME}) diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/tests/main.cpp b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/tests/main.cpp new file mode 100644 index 000000000..9992ccda9 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/tests/main.cpp @@ -0,0 +1,10 @@ + +/** + * @file main.cpp + * @brief The Microsoft delta download handler util lib tests main entry point. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ +#define CATCH_CONFIG_MAIN +#include diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/tests/microsoft_delta_download_handler_utils_ut.cpp b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/tests/microsoft_delta_download_handler_utils_ut.cpp new file mode 100644 index 000000000..e708376cb --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/handler/utils/tests/microsoft_delta_download_handler_utils_ut.cpp @@ -0,0 +1,151 @@ + +/** + * @file microsoft_delta_download_handler_ut.cpp + * @brief Unit Tests for microsoft_delta_download_handler library + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include +using Catch::Matchers::Equals; + +#include "aduc/microsoft_delta_download_handler_utils.h" +#include // ADUC_Result_* +#include // ADUC_Result_* +#include // ADUC_RelatedFile, ADUC_FileEntity +#include // ADUC_WorkflowHandle +#include +#include + +#define TEST_WORKFLOW_ID "7e3e7d32de4db3ef1337bac7341ab347" +#define TEST_PAYLOAD_FILE_ID "ac47d3bab772454283ae95f0bbb1a1de" +#define TEST_DELTA_FILE_ID "312d0351155037c4900d76473d371c35" + +const std::string updateManifest{ + R"( { )" + R"( "compatibility": [ )" + R"( { )" + R"( "deviceManufacturer": "contoso", )" + R"( "deviceModel": "toaster" )" + R"( } )" + R"( ], )" + R"( "createdDateTime": "2022-03-12T12:22:37.2627901Z", )" + R"( "files": { )" + R"( "PAYLOAD_FILE_ID": { )" + R"( "fileName": "target_update.swu", )" + R"( "hashes": { )" + R"( "sha256": "PAYLOAD_HASH" )" + R"( }, )" + R"( "sizeInBytes": 98765, )" + R"( "properties": { )" + R"( }, )" + R"( "downloadHandler": { )" + R"( "id": "microsoft/delta:1" )" + R"( }, )" + R"( "relatedFiles": { )" + R"( "DELTA_FILE_ID": { )" + R"( "fileName": "DELTA_UPDATE_FILE_NAME", )" + R"( "sizeInBytes": 1234, )" + R"( "hashes": { )" + R"( "sha256": "DELTA_UPDATE_HASH" )" + R"( }, )" + R"( "properties": { )" + R"( "microsoft.sourceFileHash": "SOURCE_UPDATE_HASH", )" + R"( "microsoft.sourceFileHashAlgorithm": "sha256" )" + R"( } )" + R"( } )" + R"( } )" + R"( } )" + R"( }, )" + R"( "instructions": { )" + R"( "steps": [ )" + R"( { )" + R"( "files": [ )" + R"( "f222b9ffefaaac577" )" + R"( ], )" + R"( "handler": "microsoft/swupdate:1", )" + R"( "handlerProperties": { )" + R"( "installedCriteria": "toaster_firmware-0.1" )" + R"( } )" + R"( } )" + R"( ] )" + R"( }, )" + R"( "manifestVersion": "5", )" + R"( "updateId": { )" + R"( "name": "toaster_firmware", )" + R"( "provider": "contoso", )" + R"( "version": "0.1" )" + R"( } )" + R"( } )" +}; + +const std::string desiredTemplate{ + R"( { )" + R"( "fileUrls": { )" + R"( "PAYLOAD_FILE_ID": "http://hostname:port/path/to/full_target_update.swu", )" + R"( "DELTA_FILE_ID": "http://hostname:port/path/to/delta_update.delta" )" + R"( }, )" + R"( "updateManifest": "UPDATE_MANIFEST", )" + R"( "updateManifestSignature": "SIGNATURE", )" + R"( "workflow": { )" + R"( "action": 3, )" + R"( "id": "WORKFLOW_ID_GUID" )" + R"( } )" + R"( } )" +}; + +ADUC_Result MockProcessDeltaUpdateFn( + const char* _sourceUpdateFilePath, const char* _deltaUpdateFilePath, const char* _targetUpdateFilePath) +{ + ADUC_Result result = { ADUC_Result_Success }; + return result; +} + +ADUC_Result MockDownloadDeltaUpdateFn(const ADUC_WorkflowHandle _workflowHandle, const ADUC_RelatedFile* _relatedFile) +{ + ADUC_Result result = { ADUC_Result_Success }; + return result; +} + +TEST_CASE("MicrosoftDeltaDownloadHandlerUtils_ProcessRelatedFile Cache Miss") +{ + ADUC_Result result = {}; + + // + // Arrange + // + JSON_Value* updateManifestTemplate = json_parse_string(updateManifest.c_str()); + REQUIRE(updateManifestTemplate != nullptr); + + char* serialized = json_serialize_to_string(updateManifestTemplate); + REQUIRE(serialized != nullptr); + + std::string serializedUpdateManifest = serialized; + json_free_serialized_string(serialized); + serialized = nullptr; + serializedUpdateManifest = std::regex_replace(serializedUpdateManifest, std::regex("\""), "\\\""); + + std::string desired = std::regex_replace(desiredTemplate, std::regex("UPDATE_MANIFEST"), serializedUpdateManifest); + + desired = std::regex_replace(desired, std::regex("WORKFLOW_ID_GUID"), TEST_WORKFLOW_ID); + desired = std::regex_replace(desired, std::regex("PAYLOAD_FILE_ID"), TEST_PAYLOAD_FILE_ID); + desired = std::regex_replace(desired, std::regex("DELTA_FILE_ID"), TEST_DELTA_FILE_ID); + + ADUC_WorkflowHandle handle = nullptr; + result = workflow_init(desired.c_str(), false, &handle); + REQUIRE(IsAducResultCodeSuccess(result.ResultCode)); + + ADUC_FileEntity* fileEntity = nullptr; + REQUIRE(workflow_get_update_file(handle, 0, &fileEntity)); + + // + // Act + // + result = MicrosoftDeltaDownloadHandlerUtils_ProcessRelatedFile( + handle, &fileEntity->RelatedFiles[0], "/foo/", "/bar/", MockProcessDeltaUpdateFn, MockDownloadDeltaUpdateFn); + + CHECK(result.ResultCode == ADUC_Result_Success_Cache_Miss); + + workflow_free_file_entity(fileEntity); +} diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/CMakeLists.txt b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/CMakeLists.txt new file mode 100644 index 000000000..51291c7a8 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required (VERSION 3.5) + +set (target_name source_update_cache) + +include (agentRules) +compileasc99 () + +find_package (azure_c_shared_utility REQUIRED) + +add_library (${target_name} STATIC "") +add_library (aduc::${target_name} ALIAS ${target_name}) + +target_include_directories (${target_name} PUBLIC inc ${ADUC_EXPORT_INCLUDES}) + +target_sources (${target_name} PRIVATE src/source_update_cache.c src/source_update_cache_utils.cpp + src/source_update_cache_utils.c) + +target_link_libraries ( + ${target_name} + PUBLIC aduc::adu_types aduc::c_utils + PRIVATE aduc::file_utils + aduc::path_utils + aduc::permission_utils + aduc::system_utils + aduc::workflow_utils + aziotsharedutil) + +target_compile_definitions ( + ${target_name} + PRIVATE + ADUC_DELTA_DOWNLOAD_HANDLER_SOURCE_UPDATE_CACHE_DIR="${ADUC_DELTA_DOWNLOAD_HANDLER_SOURCE_UPDATE_CACHE_DIR}" +) + +if (ADUC_DELTA_DOWNLOAD_HANDLER_SOURCE_UPDATE_CACHE_COMMIT_STRATEGY STREQUAL "TWO_PHASE_COMMIT") + target_compile_definitions (${target_name} PRIVATE TWO_PHASE_COMMIT="1") +endif () + +if (ADUC_BUILD_UNIT_TESTS) + add_subdirectory (tests) +endif () diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/inc/aduc/source_update_cache.h b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/inc/aduc/source_update_cache.h new file mode 100644 index 000000000..1e75d9195 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/inc/aduc/source_update_cache.h @@ -0,0 +1,42 @@ +/** + * @file source_update_cache.c + * @brief The source update cache for delta download handler update payloads. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#ifndef __SOURCE_UPDATE_CACHE_H__ +#define __SOURCE_UPDATE_CACHE_H__ + +#include // ADUC_RESULT +#include // ADUC_WorkflowHandle +#include // STRING_HANDLE + +/** + * @brief Looks up a source update from the source update cache. + * + * @param updateIdProvider The provider of the UpdateId. + * @param sourceUpdateHash The hash of the source update that acts as the key for the cache entry. + * @param sourceUpdateAlgorithm The hash algorithm of the source update that makes up the other half of the composite key. + * @param updateCacheBasePath The update cache base path. Use NULL for default. + * @param outSourceUpdatePath The output filepath of the source update in the cache. + * @return ADUC_Result The result. On success that is not ADUC_Result_Success_Cache_Miss, outSourceUpdatePath will have a path to the source update file. + */ +ADUC_Result ADUC_SourceUpdateCache_Lookup( + const char* updateIdProvider, + const char* sourceUpdateHash, + const char* sourceUpdateAlgorithm, + const char* updateCacheBasePath, + STRING_HANDLE* outSourceUpdatePath); + +/** + * @brief Moves a payload from the sandbox into the update cache. + * + * @param workflowHandle The workflow handle. + * @param updateCacheBasePath The update cache base path. Use NULL for default. + * @return ADUC_Result The result. + */ +ADUC_Result ADUC_SourceUpdateCache_Move(const ADUC_WorkflowHandle workflowHandle, const char* updateCacheBasePath); + +#endif // __SOURCE_UPDATE_CACHE_H__ diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/inc/aduc/source_update_cache_utils.h b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/inc/aduc/source_update_cache_utils.h new file mode 100644 index 000000000..8be34c0e3 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/inc/aduc/source_update_cache_utils.h @@ -0,0 +1,31 @@ +/** + * @file source_update_cache.c + * @brief The source update cache for delta download handler update payloads. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#ifndef SOURCE_UPDATE_CACHE_UTILS_H +#define SOURCE_UPDATE_CACHE_UTILS_H + +#include // EXTERN_C_* +#include // ADUC_Result +#include // ADUC_WorkflowHandle +#include // STRING_HANDLE +#include // off_t + +EXTERN_C_BEGIN + +STRING_HANDLE ADUC_SourceUpdateCacheUtils_CreateSourceUpdateCachePath( + const char* provider, const char* hash, const char* alg, const char* updateCacheBasePath); + +ADUC_Result ADUC_SourceUpdateCacheUtils_MoveToUpdateCache( + const ADUC_WorkflowHandle workflowHandle, const char* updateCacheBasePath); + +int ADUC_SourceUpdateCacheUtils_PurgeOldestFromUpdateCache( + const ADUC_WorkflowHandle workflowHandle, off_t totalSize, const char* updateCacheBasePath); + +EXTERN_C_END + +#endif // SOURCE_UPDATE_CACHE_UTILS_H diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/src/source_update_cache.c b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/src/source_update_cache.c new file mode 100644 index 000000000..a6302ae2b --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/src/source_update_cache.c @@ -0,0 +1,126 @@ + +/** + * @file source_update_cache.cpp + * @brief File cache for updates used as a source for delta update processing. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include "aduc/source_update_cache.h" +#include "aduc/source_update_cache_utils.h" // ADUC_SourceUpdateCacheUtils_CreateSourceUpdateCachePath +#include // PermissionUtils_VerifyFilemodeBitmask +#include // SystemUtils_IsFile, ADUC_SystemUtils_MkDirRecursiveAduUser +#include // ADUC_Result_Success_Cache_Miss +#include // for mallocAndStrcpy_s +#include +#include // dirname +#include // rename +#include // free +#include // S_IRUSR + +/** + * @brief Looks up a source update from the source update cache. + * + * @param updateIdProvider The provider of the UpdateId. + * @param sourceUpdateHash The hash of the source update that acts as the key for the cache entry. + * @param sourceUpdateAlgorithm The hash algorithm of the source update that makes up the other half of the composite key. + * @param updateCacheBasePath The update cache base path. Use NULL for default. + * @param outSourceUpdatePath The output filepath of the source update in the cache. + * @return ADUC_Result The result. On success that is not ADUC_Result_Success_Cache_Miss, outSourceUpdatePath will have a path to the source update file. + */ +ADUC_Result ADUC_SourceUpdateCache_Lookup( + const char* updateIdProvider, + const char* sourceUpdateHash, + const char* sourceUpdateAlgorithm, + const char* updateCacheBasePath, + STRING_HANDLE* outSourceUpdatePath) +{ + ADUC_Result result = { .ResultCode = ADUC_Result_Failure }; + + STRING_HANDLE filePath = ADUC_SourceUpdateCacheUtils_CreateSourceUpdateCachePath( + updateIdProvider, sourceUpdateHash, sourceUpdateAlgorithm, updateCacheBasePath); + if (filePath == NULL) + { + result.ExtendedResultCode = ADUC_ERC_LOOKUP_CREATE_PATH; + goto done; + } + + // file exists and is readable + if (!SystemUtils_IsFile(STRING_c_str(filePath), NULL) + || !PermissionUtils_VerifyFilemodeBitmask(STRING_c_str(filePath), S_IRUSR)) + { + result.ResultCode = ADUC_Result_Success_Cache_Miss; + goto done; + } + + *outSourceUpdatePath = filePath; + filePath = NULL; + + result.ResultCode = ADUC_Result_Success; + +done: + STRING_delete(filePath); + + return result; +} + +static ADUC_Result getPayloadTotalSize(const ADUC_WorkflowHandle workflowHandle, off_t* outSize) +{ + ADUC_Result result = { .ResultCode = ADUC_Result_Failure }; + *outSize = 0; + return result; +} + +/** + * @brief Moves all payloads from the download sandbox work folder to the cache. + * + * @param workflowHandle The workflow handle. + * @param updateCacheBasePath The update cache base path. Use NULL for default. + * @return ADUC_Result The result. + */ +ADUC_Result ADUC_SourceUpdateCache_Move(const ADUC_WorkflowHandle workflowHandle, const char* updateCacheBasePath) +{ + ADUC_Result result = { .ResultCode = ADUC_Result_Failure }; + + off_t spaceRequired = 0; + result = getPayloadTotalSize(workflowHandle, &spaceRequired); + + int res = -1; + +#ifndef TWO_PHASE_COMMIT + // When NOT two-phase commit, proactively make space by pre-purging cache dir of oldest files upto size of sandboxFilePath. + res = ADUC_SourceUpdateCacheUtils_PurgeOldestFromUpdateCache(workflowHandle, spaceRequired, updateCacheBasePath); + if (res != 0) + { + Log_Error("pre-purge failed, res %d", res); + result.ExtendedResultCode = ADUC_ERC_MOVE_PREPURGE; + goto done; + } +#endif + + result = ADUC_SourceUpdateCacheUtils_MoveToUpdateCache(workflowHandle, updateCacheBasePath); + if (IsAducResultCodeFailure(result.ResultCode)) + { + Log_Error("Failed to move sandbox payloads to update cache. erc: %d", result.ExtendedResultCode); + result.ExtendedResultCode = ADUC_ERC_MOVE_PAYLOAD; + goto done; + } + +#ifdef TWO_PHASE_COMMIT + // In the case of two-phase commit, purge cache dir of oldest files upto size of sandboxFilePath AFTER move/copy. + res = ADUC_SourceUpdateCacheUtils_PurgeOldestFromUpdateCache(workflowHandle, spaceRequired, updateCacheBasePath); + if (res != 0) + { + Log_Error("post-purge failed, res %d", res); + result.ExtendedResultCode = ADUC_ERC_MOVE_POSTPURGE; + goto done; + } +#endif + + result.ResultCode = ADUC_Result_Success; + +done: + + return result; +} diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/src/source_update_cache_utils.c b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/src/source_update_cache_utils.c new file mode 100644 index 000000000..c41a4b7e3 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/src/source_update_cache_utils.c @@ -0,0 +1,257 @@ + +/** + * @file source_update_cache_utils.c + * @brief utils for source_update_cache + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include "aduc/source_update_cache_utils.h" +#include // SanitizePathSegment +#include // IsNullOrEmpty +#include // ADUC_SystemUtils_* +#include // ADUC_FileEntity +#include // workflow_* +#include // mallocAndStrcpy_s, strcat_s +#include // STRING_* +#include // dirname +#include // free, malloc + +EXTERN_C_BEGIN + +/** + * @brief Converts a base64 encoded string to a safer file name / path segment. + * @param unencoded The base64 encoded string that has not been encoded to the file path encoding scheme. + * @return STRING_HANDLE The file path string, or NULL on error. + * @details Caller owns it and must call STRING_delete() when done with it. + */ +static STRING_HANDLE encodeBase64ForFilePath(const char* unencoded) +{ + char encoded[1024] = { 0 }; + char str[2] = { 0, 0 }; + STRING_HANDLE encodedHandle = NULL; + size_t lengthUnencoded = 0; + + if (IsNullOrEmpty(unencoded)) + { + return NULL; + } + + // base64 alphabet has a-z, A-Z, 0-9, '+', '/', and '='. + // For '+', '/', and '=', encode using '_' followed by 2-digit hex ascii code. + + lengthUnencoded = strlen(unencoded); + for (int index = 0; index < lengthUnencoded; ++index) + { + switch (unencoded[index]) + { + case '+': + if (strcat_s(encoded, ARRAY_SIZE(encoded), "_2B") != 0) + { + goto done; + } + break; + case '/': + if (strcat_s(encoded, ARRAY_SIZE(encoded), "_2F") != 0) + { + goto done; + } + break; + case '=': + if (strcat_s(encoded, ARRAY_SIZE(encoded), "_3D") != 0) + { + goto done; + } + break; + default: + str[0] = unencoded[index]; + if (strcat_s(encoded, ARRAY_SIZE(encoded), str) != 0) + { + goto done; + } + break; + } + } + + encodedHandle = STRING_construct(encoded); + if (encodedHandle == NULL) + { + goto done; + } + +done: + + return encodedHandle; +} + +/** + * @brief Creates the string file path for the cache file. + * @param provider The updateId provider from the update metadata. + * @param hash The related file source hash from the update metadata. + * @param alg The related file source hash algorithm from the update metadata. + * @param updateCacheBasePath The path to the base of update cache. NULL for default. + * @return STRING_HANDLE The file path string. Caller owns it and must call free() when done with it. + */ +STRING_HANDLE ADUC_SourceUpdateCacheUtils_CreateSourceUpdateCachePath( + const char* provider, const char* hash, const char* alg, const char* updateCacheBasePath) +{ + STRING_HANDLE resultPath = NULL; + + STRING_HANDLE sanitizedProvider = NULL; + STRING_HANDLE sanitizedHashAlgorithm = NULL; + STRING_HANDLE encodedHash = NULL; + + // file path format: + // {base_dir}/{provider}/{hashAlg}-{hash} + + sanitizedProvider = SanitizePathSegment(provider); + if (sanitizedProvider == NULL) + { + goto done; + } + + sanitizedHashAlgorithm = SanitizePathSegment(alg); + if (sanitizedHashAlgorithm == NULL) + { + goto done; + } + + encodedHash = encodeBase64ForFilePath(hash); + if (encodedHash == NULL) + { + goto done; + } + + resultPath = STRING_construct_sprintf( + "%s/%s/%s-%s", + IsNullOrEmpty(updateCacheBasePath) ? ADUC_DELTA_DOWNLOAD_HANDLER_SOURCE_UPDATE_CACHE_DIR : updateCacheBasePath, + STRING_c_str(sanitizedProvider), + STRING_c_str(sanitizedHashAlgorithm), + STRING_c_str(encodedHash)); + +done: + STRING_delete(sanitizedProvider); + STRING_delete(sanitizedHashAlgorithm); + STRING_delete(encodedHash); + + return resultPath; +} + +/** + * @brief Moves all payloads of the current update from the download sandbox work folder to the update cache. + * @param workflowHandle The workflow handle. + * @param updateCacheBasePath The path to the base of update cache. NULL for default. + * @return ADUC_Result The result. + */ +ADUC_Result ADUC_SourceUpdateCacheUtils_MoveToUpdateCache( + const ADUC_WorkflowHandle workflowHandle, const char* updateCacheBasePath) +{ + ADUC_Result result = { .ResultCode = ADUC_Result_Failure }; + int res = -1; + ADUC_FileEntity* fileEntity = NULL; + STRING_HANDLE sandboxUpdatePayloadFile = NULL; + ADUC_UpdateId* updateId = NULL; + STRING_HANDLE updateCacheFilePath = NULL; + char dirPath[1024] = ""; + + size_t countPayloads = workflow_get_update_files_count(workflowHandle); + for (size_t index = 0; index < countPayloads; ++index) + { + workflow_free_file_entity(fileEntity); + fileEntity = NULL; + + if (!workflow_get_update_file(workflowHandle, index, &fileEntity)) + { + Log_Error("get update file %d", index); + goto done; + } + + workflow_get_entity_workfolder_filepath(workflowHandle, fileEntity, &sandboxUpdatePayloadFile); + + result = workflow_get_expected_update_id(workflowHandle, &updateId); + if (IsAducResultCodeFailure(result.ResultCode)) + { + Log_Error("get updateId, erc 0x%08x", result.ExtendedResultCode); + goto done; + } + + // When update is already installed, payloads would not be downloaded but it would still + // attempt to move to cache with OnUpdateWorkflowCompleted contract call because overall + // is it Apply Success result, so guard against non-existent sandbox file. + if (!SystemUtils_IsFile(STRING_c_str(sandboxUpdatePayloadFile), NULL)) + { + result.ExtendedResultCode = ADUC_ERC_MISSING_SOURCE_SANDBOX_FILE; + goto done; + } + + const char* provider = updateId->Provider; + const char* hash = (fileEntity->Hash[0]).value; + const char* alg = (fileEntity->Hash[0]).type; + + updateCacheFilePath = + ADUC_SourceUpdateCacheUtils_CreateSourceUpdateCachePath(provider, hash, alg, updateCacheBasePath); + if (updateCacheFilePath == NULL) + { + result.ExtendedResultCode = ADUC_ERC_MOVE_CREATE_CACHE_PATH; + goto done; + } + + if (strcpy_s(dirPath, ARRAY_SIZE(dirPath), STRING_c_str(updateCacheFilePath)) != 0) + { + result.ExtendedResultCode = ADUC_ERC_NOTRECOVERABLE; + goto done; + } + + const char* dirPathCache = dirname(dirPath); // free() not needed + if (dirPathCache == NULL) + { + result.ExtendedResultCode = ADUC_ERC_NOTRECOVERABLE; + goto done; + } + + if (ADUC_SystemUtils_MkDirRecursiveDefault(dirPathCache) != 0) + { + result.ExtendedResultCode = ADUC_ERC_MOVE_CREATE_CACHE_PATH; + goto done; + } + + // First try to move the file. + // errno EXDEV would be common if copying across different mount points. + // For any failure, it falls back to copy. + + Log_Debug( + "moving '%s' -> '%s'", STRING_c_str(sandboxUpdatePayloadFile), STRING_c_str(updateCacheFilePath)); + + res = rename(STRING_c_str(sandboxUpdatePayloadFile), STRING_c_str(updateCacheFilePath)); + if (res != 0) + { + Log_Warn("rename, errno %d", errno); + + // fallback to copy + // + if (ADUC_SystemUtils_CopyFileToDir( + STRING_c_str(sandboxUpdatePayloadFile), dirPathCache, false /* overwriteExistingFile */) + != 0) + { + Log_Error("Copy Failed"); + result.ExtendedResultCode = ADUC_ERC_MOVE_COPYFALLBACK; + goto done; + } + } + + STRING_delete(updateCacheFilePath); + updateCacheFilePath = NULL; + } + + result.ResultCode = ADUC_Result_Success; + +done: + STRING_delete(sandboxUpdatePayloadFile); + STRING_delete(updateCacheFilePath); + workflow_free_file_entity(fileEntity); + + return result; +} + +EXTERN_C_END diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/src/source_update_cache_utils.cpp b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/src/source_update_cache_utils.cpp new file mode 100644 index 000000000..b942def60 --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/src/source_update_cache_utils.cpp @@ -0,0 +1,198 @@ + +/** + * @file source_update_cache_utils.cpp + * @brief utils for source_update_cache + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include "aduc/source_update_cache_utils.h" +#include // ADUC_INODE_SENTINEL_VALUE +#include // aduc::findFilesInDir +#include +#include +#include // std:: +#include // std::priority_queue +#include +#include +#include // stat +#include // ino_t +#include // time_t +#include // unlink +#include + +struct LessThan_UpdateCachePurgeFile +{ +}; + +class UpdateCachePurgeFile +{ +public: + UpdateCachePurgeFile(ino_t fileNodeId, time_t lastModifiedTime, off_t fileSize, const std::string& filePath) : + inode(fileNodeId), mtime(lastModifiedTime), size(fileSize), path(filePath) + { + } + + bool operator<(const UpdateCachePurgeFile& other) const + { + return mtime < other.mtime; + } + + ino_t GetInode() const + { + return inode; + } + + time_t GetLastModifiedTime() const + { + return mtime; + } + + off_t GetSizeInBytes() const + { + return size; + } + + std::string GetFilePath() const + { + return path; + } + +private: + ino_t inode; ///< the inode from stat call. + time_t mtime; ///< last modified time from stat call. + off_t size; ///< The size of the file in bytes. + std::string path; ///< the abs path to the file. +}; + +EXTERN_C_BEGIN + +/** + * @brief Deletes oldest files from the update cache until given totalSize is freed, or no more files exist. + * Excludes payload files of the current update. + * @param workflowHandle The workflow handle. + * @param totalSize The maximum total size in bytes to be freed up in the update cache. + * @param updateCacheBasePath The path to the base of update cache. NULL for default. + * @return int 0 on success. + */ +int ADUC_SourceUpdateCacheUtils_PurgeOldestFromUpdateCache( + const ADUC_WorkflowHandle workflowHandle, off_t totalSize, const char* updateCacheBasePath) +{ + int result = -1; + + // Algorithm + // + // 1. Create priority queue of type (UpdateCachePurgeFile*), where UpdateCachePurgeFile items sort by last modified time + // 2. Find all files under the dir and add a corresponding UpdateCachePurgeFile entry to the priority queue + // 3. from workflowHandle, get the inode for each payload file and remove from the priority queue (if exists) + // (the inode is saved at the time of moving the payload from sandbox to cache) + // 4. while pq has items and totalSize > 0 + // pop pq and delete the non-payload file at that item's path + // + try + { + // get the files currently in the update cache + std::vector filesInCache; + aduc::findFilesInDir( + updateCacheBasePath == nullptr ? ADUC_DELTA_DOWNLOAD_HANDLER_SOURCE_UPDATE_CACHE_DIR : updateCacheBasePath, + &filesInCache); + + std::priority_queue oldestCacheFiles; + + // populate set of inodes of any payloads that have been moved to cache from sandbox + std::set updatePayloadInodes; + size_t countPayloads = workflow_get_update_files_count(workflowHandle); + for (size_t index = 0; index < countPayloads; ++index) + { + ino_t payload_inode = workflow_get_update_file_inode(workflowHandle, index); + if (payload_inode != ADUC_INODE_SENTINEL_VALUE) + { + updatePayloadInodes.insert(payload_inode); + } + } + + // filter out the current update's payloads that were moved to cache to avoid removal + if (updatePayloadInodes.size() > 0) + { + Log_Debug("Removing %d payload paths from the cache purge list.", updatePayloadInodes.size()); + + auto newEnd = std::remove_if( + filesInCache.begin(), filesInCache.end(), [&updatePayloadInodes](const std::string& filePath) { + struct stat st + { + }; + if (stat(filePath.c_str(), &st) != 0) + { + Log_Warn("filter - stat '%s', errno: %d", filePath.c_str(), errno); + st.st_ino = ADUC_INODE_SENTINEL_VALUE; + return false; // err on the side of not removing it + } + + const ino_t& updateCacheInode = st.st_ino; + + // Remove from list of files to be purged when the file's inode is in the set of update payload inodes + std::set::iterator iter = updatePayloadInodes.find(updateCacheInode); + if (iter == updatePayloadInodes.end()) + { + return false; + } + + return true; + }); + + filesInCache.erase(newEnd); + } + + // insert into purge list + std::for_each(filesInCache.begin(), filesInCache.end(), [&oldestCacheFiles](const std::string& filePath) { + struct stat st + { + }; + if (stat(filePath.c_str(), &st) != 0) + { + Log_Warn("pq push - stat '%s', errno: %d", filePath.c_str(), errno); + } + else + { + oldestCacheFiles.emplace(st.st_ino, st.st_mtime, st.st_size, filePath); + } + }); + + // delete files until made enough room as per totalSize needed for new upate payloads + while (!oldestCacheFiles.empty() && totalSize > 0) + { + UpdateCachePurgeFile cachePurgeFile = oldestCacheFiles.top(); + oldestCacheFiles.pop(); + off_t fileSize = cachePurgeFile.GetSizeInBytes(); + + std::string filePathForDelete{ std::move(cachePurgeFile.GetFilePath()) }; + int res = unlink(filePathForDelete.c_str()); + if (res != 0) + { + Log_Error("unlink '%s', inode %d - errno: %d", filePathForDelete.c_str(), cachePurgeFile.GetInode(), errno); + result = -1; // overall it is a failure, but keep going to attempt to free up space. + continue; + } + else + { + totalSize -= fileSize; // off_t is signed + } + } + + result = 0; + } + catch (const std::exception& e) + { + const char* what = e.what(); + Log_Error("Unhandled std exception: %s", what); + } + catch (...) + { + Log_Error("Unhandled exception"); + } + + return result; +} + +EXTERN_C_END diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/tests/CMakeLists.txt b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/tests/CMakeLists.txt new file mode 100644 index 000000000..54c3a7eda --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/tests/CMakeLists.txt @@ -0,0 +1,34 @@ +project (source_update_cache_unit_tests) + +include (agentRules) +compileasc99 () +disablertti () + +find_package (Catch2 REQUIRED) +find_package (Parson REQUIRED) + +add_executable (${PROJECT_NAME} "") + +target_include_directories (${PROJECT_NAME} PRIVATE inc ${ADUC_EXPORT_INCLUDES}) + +target_sources (${PROJECT_NAME} PRIVATE main.cpp source_update_cache_utils_ut.cpp) + +target_link_libraries ( + ${PROJECT_NAME} + PRIVATE aduc::file_utils + aduc::source_update_cache + aduc::test_utils + aduc::system_utils + aduc::workflow_utils + Catch2::Catch2 + Parson::parson) + +target_compile_definitions ( + ${PROJECT_NAME} + PRIVATE + ADUC_DELTA_DOWNLOAD_HANDLER_SOURCE_UPDATE_CACHE_DIR="${ADUC_DELTA_DOWNLOAD_HANDLER_SOURCE_UPDATE_CACHE_DIR}" +) + +include (CTest) +include (Catch) +catch_discover_tests (${PROJECT_NAME}) diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/tests/main.cpp b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/tests/main.cpp new file mode 100644 index 000000000..906ea983b --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/tests/main.cpp @@ -0,0 +1,10 @@ + +/** + * @file main.cpp + * @brief The source update cache tests main entry point. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ +#define CATCH_CONFIG_MAIN +#include diff --git a/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/tests/source_update_cache_utils_ut.cpp b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/tests/source_update_cache_utils_ut.cpp new file mode 100644 index 000000000..e1991178c --- /dev/null +++ b/src/extensions/download_handlers/plugin_examples/microsoft_delta_download_handler/source_update_cache/tests/source_update_cache_utils_ut.cpp @@ -0,0 +1,394 @@ +/** + * @file source_update_cache_utils_ut.cpp + * @brief Unit Tests for source_update_cache_utils + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include "aduc/source_update_cache_utils.h" + +#include +using Catch::Matchers::Equals; +#include // ADUC_INODE_SENTINEL_VALUE +#include // aduc::AutoDir +#include // aduc::AutoWorkflowHandle +#include // EXTERN_C_BEGIN, EXTERN_C_END +#include // aduc::findFilesInDir +#include // aduc::FreeDeleter +#include // IsNullOrEmpty +#include // SystemUtils_IsFile +#include // ADUC_Workflow +#include // workflow_*, ADUC_WorkflowHandle +#include // std::find +#include // std::ofstream +#include // std::unique_ptr +#include // json_* +#include // std::regex, std::regex_replace +#include +#include // struct stat, off_t + +#define PROVIDER_NAME "TestProvider" + +#define TEST_DIR "/tmp/adutest/source_update_cache_utils_ut" + +#define TEST_SANDBOX_BASE_PATH TEST_DIR "/test_sandbox" + +#define TEST_CACHE_BASE_PATH TEST_DIR "/test_cache" + +#define TEST_CACHE_BASE_PROVIDER_PATH TEST_CACHE_BASE_PATH "/" PROVIDER_NAME + +#define UPDATE_PAYLOAD_FILENAME "full_target_update.swu" + +#define UPDATE_PAYLOAD_ABS_CACHE_PATH TEST_CACHE_BASE_PATH "/" PROVIDER_NAME "/" UPDATE_PAYLOAD_FILENAME + +#define NON_PAYLOAD_ABS_CACHE_PATH \ + TEST_CACHE_BASE_PATH "/" PROVIDER_NAME "/" \ + "nonPayloadSourceUpdate.swu" + +#define WORKFLOW_PROPERTY_FIELD_WORKFOLDER "_workFolder" +#define TEST_WORKFLOW_ID "6f4d0e93-de4d-b3ef-85f0-ba0cfe1030f3" + +using Deleter = aduc::FreeDeleter; +using AutoDir = aduc::AutoDir; + +// fwd decls +EXTERN_C_BEGIN +bool workflow_set_string_property(ADUC_WorkflowHandle handle, const char* property, const char* value); +EXTERN_C_END + +const std::string updateContentData = "hello\n"; + +// Setup update manifest in workflow handle +const std::string updateManifest{ + R"( { )" + R"( "updateId": { )" + R"( "provider": "TestProvider", )" + R"( "name": "updateName1", )" + R"( "version": "1.0" )" + R"( }, )" + R"( "files": { )" + R"( "fileId1": { )" + R"( "fileName": "full_target_update.swu", )" + R"( "hashes": { )" + R"( "sha256": "hash1/+==" )" + R"( }, )" + R"( "sizeInBytes": 6, )" + R"( "properties": { )" + R"( } )" + R"( } )" + R"( } )" + R"( } )" +}; + +const std::string desiredTemplate{ + R"( { )" + R"( "fileUrls": { )" + R"( "fileId1": "http://hostname:port/path/to/full_target_update.swu" )" + R"( }, )" + R"( "updateManifest": "UPDATE_MANIFEST", )" + R"( "updateManifestSignature": "SIGNATURE", )" + R"( "workflow": { )" + R"( "action": 3, )" + R"( "id": "WORKFLOW_ID_GUID" )" + R"( } )" + R"( } )" +}; + +TEST_CASE("ADUC_SourceUpdateCacheUtils_CreateSourceUpdateCachePath") +{ + const char* test_provider = "test_Provider/1234"; + const char* test_hash = "0123+abcxyz/ABCXYZ=="; + const char* test_alg = "test_Alg_1024"; + + { + // + // Act + // + STRING_HANDLE testSourceUpdateCachePath = ADUC_SourceUpdateCacheUtils_CreateSourceUpdateCachePath( + test_provider, test_hash, test_alg, NULL /* updateCacheBasePath */); + REQUIRE_FALSE(IsNullOrEmpty(STRING_c_str(testSourceUpdateCachePath))); + + // + // Assert + // + const char* expectedPath = "/var/lib/adu/sdc/test_Provider_1234/test_Alg_1024-0123_2Babcxyz_2FABCXYZ_3D_3D"; + CHECK_THAT(STRING_c_str(testSourceUpdateCachePath), Equals(expectedPath)); + STRING_delete(testSourceUpdateCachePath); + } + + { + // + // Act + // + STRING_HANDLE testSourceUpdateCachePath = ADUC_SourceUpdateCacheUtils_CreateSourceUpdateCachePath( + test_provider, test_hash, test_alg, TEST_CACHE_BASE_PATH); + REQUIRE_FALSE(IsNullOrEmpty(STRING_c_str(testSourceUpdateCachePath))); + + // + // Assert + // + const char* expectedPath2 = + TEST_CACHE_BASE_PATH "/test_Provider_1234/test_Alg_1024-0123_2Babcxyz_2FABCXYZ_3D_3D"; + CHECK_THAT(STRING_c_str(testSourceUpdateCachePath), Equals(expectedPath2)); + STRING_delete(testSourceUpdateCachePath); + } +} + +TEST_CASE("ADUC_SourceUpdateCacheUtils_MoveToUpdateCache") +{ + ADUC_Result result = {}; + + // + // Arrange + // + auto autoFreeWorkflowHandle = std::unique_ptr{ new ADUC_Workflow }; + REQUIRE(autoFreeWorkflowHandle.get() != nullptr); + ADUC_WorkflowHandle handle = reinterpret_cast(&autoFreeWorkflowHandle); + + // auto rmdir on scope exit + AutoDir testBaseDir(TEST_DIR); + AutoDir testCache(TEST_CACHE_BASE_PATH); + AutoDir testSandbox(TEST_SANDBOX_BASE_PATH); + + // Remove and recreate test dirs. + REQUIRE(testBaseDir.RemoveDir()); + // explicitly do not create cache dir as it should get created. + REQUIRE(testSandbox.CreateDir()); + + // Write file to test sandbox dir + const std::string payloadFilePath = std::string(testSandbox.GetDir().c_str()) + "/" UPDATE_PAYLOAD_FILENAME; + + { + std::ofstream outStream{ payloadFilePath }; + outStream << updateContentData; + } + + JSON_Value* updateManifestJson = json_parse_string(updateManifest.c_str()); + REQUIRE(updateManifestJson != nullptr); + + char* serialized = json_serialize_to_string(updateManifestJson); + REQUIRE(serialized != nullptr); + + std::string serializedUpdateManifest = serialized; + json_free_serialized_string(serialized); + serialized = nullptr; + + serializedUpdateManifest = std::regex_replace(serializedUpdateManifest, std::regex("\""), "\\\""); + std::string desired = std::regex_replace(desiredTemplate, std::regex("UPDATE_MANIFEST"), serializedUpdateManifest); + desired = std::regex_replace(desired, std::regex("WORKFLOW_ID_GUID"), TEST_WORKFLOW_ID); + result = workflow_init(desired.c_str(), false, &handle); + REQUIRE(IsAducResultCodeSuccess(result.ResultCode)); + + // Override work folder to be test sandbox dir + REQUIRE(workflow_set_string_property(handle, WORKFLOW_PROPERTY_FIELD_WORKFOLDER, testSandbox.GetDir().c_str())); + + // + // Act + // + result = ADUC_SourceUpdateCacheUtils_MoveToUpdateCache(handle, TEST_CACHE_BASE_PATH); + CHECK(result.ResultCode == ADUC_Result_Success); + + // + // Assert + // + const char* expectedFileInUpdateCache = TEST_CACHE_BASE_PATH "/TestProvider/sha256-hash1_2F_2B_3D_3D"; + CHECK_FALSE(SystemUtils_IsFile(payloadFilePath.c_str(), nullptr)); // should no longer be in sandbox + CHECK(SystemUtils_IsFile(expectedFileInUpdateCache, nullptr)); // and should've been moved to update cache + + workflow_uninit(handle); +} + +// TODO: re-activate this section after this resolves: Bug 40886231: Arm32 UT ADUC_SourceUpdateCacheUtils_PurgeOldestFromUpdateCache failed +// TEST_CASE("ADUC_SourceUpdateCacheUtils_PurgeOldestFromUpdateCache") +// { +// // +// // Arrange (common) +// // +// AutoDir testBaseDir(TEST_DIR); +// AutoDir testCache(TEST_CACHE_BASE_PROVIDER_PATH); + +// off_t nonPayloadFileSize = 0; + +// const std::string NonPayloadFileContents = "old source update data\n"; + +// auto setupCacheFilesFn = [&](bool createUpdatePayloadFile, bool createNonUpdatePayloadFile) { +// testCache.RemoveDir(); +// testCache.CreateDir(); + +// if (createUpdatePayloadFile) +// { +// // cache dir file that is part of update payload +// const std::string PayloadFilePath = UPDATE_PAYLOAD_ABS_CACHE_PATH; +// std::ofstream outStream{ PayloadFilePath }; +// outStream << updateContentData; +// } + +// if (createNonUpdatePayloadFile) +// { +// // cache dir file that is NOT part of update payload +// { +// std::ofstream outStream{ NON_PAYLOAD_ABS_CACHE_PATH }; +// outStream << NonPayloadFileContents; +// } + +// struct stat st +// { +// }; +// nonPayloadFileSize = (stat(NON_PAYLOAD_ABS_CACHE_PATH, &st) == 0) ? st.st_size : 0; +// REQUIRE(nonPayloadFileSize > 0); +// } +// }; + +// auto setupWorkflowHandleFn = [&](ADUC_WorkflowHandle* handle) { +// JSON_Value* updateManifestJson = json_parse_string(updateManifest.c_str()); +// REQUIRE(updateManifestJson != nullptr); + +// char* serialized = json_serialize_to_string(updateManifestJson); +// REQUIRE(serialized != nullptr); + +// std::string serializedUpdateManifest = serialized; +// json_free_serialized_string(serialized); +// serialized = nullptr; + +// serializedUpdateManifest = std::regex_replace(serializedUpdateManifest, std::regex("\""), "\\\""); +// std::string desired = +// std::regex_replace(desiredTemplate, std::regex("UPDATE_MANIFEST"), serializedUpdateManifest); +// desired = std::regex_replace(desired, std::regex("WORKFLOW_ID_GUID"), TEST_WORKFLOW_ID); +// ADUC_Result result = workflow_init(desired.c_str(), false, handle); +// REQUIRE(IsAducResultCodeSuccess(result.ResultCode)); +// }; + +// SECTION("no cache files exist") +// { +// // +// // Arrange +// // +// setupCacheFilesFn(false /* createUpdatePayloadFile */, false /* createNonUpdatePayloadFile */); + +// ADUC_WorkflowHandle handle = nullptr; +// setupWorkflowHandleFn(&handle); +// REQUIRE(handle != nullptr); +// aduc::AutoWorkflowHandle autoWorkflowHandle{ handle }; +// ADUC_Workflow* workflow = reinterpret_cast(handle); + +// // +// // Act +// // +// int res = ADUC_SourceUpdateCacheUtils_PurgeOldestFromUpdateCache( +// handle, 1 /* totalSize */, testCache.GetDir().c_str() /* updateCacheBasePath */); +// REQUIRE(res == 0); + +// // +// // Assert +// // +// std::vector filesInDir; +// aduc::findFilesInDir(testCache.GetDir(), &filesInDir); +// CHECK(filesInDir.size() == 0); +// } + +// SECTION("No cache files that are not update payload") +// { +// // +// // Arrange +// // +// setupCacheFilesFn(true /* createUpdatePayloadFile */, false /* createNonUpdatePayloadFile */); + +// ADUC_WorkflowHandle handle = nullptr; +// setupWorkflowHandleFn(&handle); +// REQUIRE(handle != nullptr); +// aduc::AutoWorkflowHandle autoWorkflowHandle{ handle }; +// ADUC_Workflow* workflow = reinterpret_cast(handle); + +// // set inode for payload +// struct stat st +// { +// }; +// ino_t inodeUpdatePayloadInCache = +// (stat(UPDATE_PAYLOAD_ABS_CACHE_PATH, &st) == 0) ? st.st_ino : ADUC_INODE_SENTINEL_VALUE; +// REQUIRE(inodeUpdatePayloadInCache != ADUC_INODE_SENTINEL_VALUE); +// REQUIRE(workflow_set_update_file_inode(handle, 0, inodeUpdatePayloadInCache)); +// REQUIRE(workflow_get_update_file_inode(handle, 0) == st.st_ino); + +// // +// // Act +// // +// int res = ADUC_SourceUpdateCacheUtils_PurgeOldestFromUpdateCache( +// handle, 1 /* totalSize */, testCache.GetDir().c_str() /* updateCacheBasePath */); +// REQUIRE(res == 0); + +// // +// // Assert +// // +// std::vector filesInDir; +// aduc::findFilesInDir(testCache.GetDir(), &filesInDir); +// REQUIRE(filesInDir.size() == 1); +// CHECK_THAT(filesInDir[0], Equals(UPDATE_PAYLOAD_ABS_CACHE_PATH)); +// } + +// SECTION("not current update payload deleted when < totalSize") +// { +// // +// // Arrange +// // +// setupCacheFilesFn(true /* createUpdatePayloadFile */, true /* createNonUpdatePayloadFile */); + +// ADUC_WorkflowHandle handle = nullptr; +// setupWorkflowHandleFn(&handle); +// REQUIRE(handle != nullptr); +// aduc::AutoWorkflowHandle autoWorkflowHandle{ handle }; +// ADUC_Workflow* workflow = reinterpret_cast(handle); + +// // set inode for payload +// struct stat st +// { +// }; +// ino_t inodeUpdatePayloadInCache = +// (stat(UPDATE_PAYLOAD_ABS_CACHE_PATH, &st) == 0) ? st.st_ino : ADUC_INODE_SENTINEL_VALUE; +// REQUIRE(inodeUpdatePayloadInCache != ADUC_INODE_SENTINEL_VALUE); +// REQUIRE(workflow_set_update_file_inode(handle, 0, inodeUpdatePayloadInCache)); +// REQUIRE(workflow_get_update_file_inode(handle, 0) == st.st_ino); + +// // ensure upfront that there are 2 files in the test cache, the update payload and the non-payload file. +// { +// std::vector filesInDir; +// aduc::findFilesInDir(testCache.GetDir(), &filesInDir); + +// CHECK(filesInDir.size() == 2); + +// bool foundUpdatePayloadInCache = filesInDir.end() +// != std::find_if(filesInDir.begin(), filesInDir.end(), [](const std::string& filePath) { +// return filePath == UPDATE_PAYLOAD_ABS_CACHE_PATH; +// }); +// bool foundNonPayloadInCache = filesInDir.end() +// != std::find_if(filesInDir.begin(), filesInDir.end(), [](const std::string& filePath) { +// return filePath == NON_PAYLOAD_ABS_CACHE_PATH; +// }); + +// auto item0 = filesInDir[0]; +// auto item1 = filesInDir[1]; + +// // pre-condition +// auto cond = item0 == NON_PAYLOAD_ABS_CACHE_PATH && item1 == UPDATE_PAYLOAD_ABS_CACHE_PATH +// || item0 == UPDATE_PAYLOAD_ABS_CACHE_PATH && item1 == NON_PAYLOAD_ABS_CACHE_PATH; +// CHECK(cond); +// } + +// // +// // Act +// // +// int res = ADUC_SourceUpdateCacheUtils_PurgeOldestFromUpdateCache( +// handle, nonPayloadFileSize + 1 /* totalSize */, testCache.GetDir().c_str() /* updateCacheBasePath */); +// REQUIRE(res == 0); + +// // +// // Assert +// // +// { +// std::vector filesInDir; +// aduc::findFilesInDir(testCache.GetDir(), &filesInDir); +// CHECK(filesInDir.size() == 1); +// CHECK_THAT(filesInDir[0], Equals(UPDATE_PAYLOAD_ABS_CACHE_PATH)); +// } +// } +// } diff --git a/src/extensions/extension_manager/CMakeLists.txt b/src/extensions/extension_manager/CMakeLists.txt index 8ef5efaa8..12b907436 100644 --- a/src/extensions/extension_manager/CMakeLists.txt +++ b/src/extensions/extension_manager/CMakeLists.txt @@ -27,19 +27,22 @@ target_compile_definitions ( # target_link_libraries ( ${PROJECT_NAME} - PUBLIC aduc::extension_utils + PUBLIC aduc::adu_types # download.h, update_content.h, and workflow.h used by header and impl PRIVATE aduc::c_utils - aduc::content_handlers + aduc::contract_utils + aduc::download_handler_factory + aduc::download_handler_plugin aduc::exception_utils - aduc::string_utils + aduc::extension_utils + aduc::hash_utils aduc::logging + aduc::parser_utils + aduc::path_utils + aduc::string_utils + aduc::workflow_utils ${CMAKE_DL_LIBS}) # # Turn -fPIC on, in order to use this library in another shared library. # set_property (TARGET ${PROJECT_NAME} PROPERTY POSITION_INDEPENDENT_CODE ON) - -if (${ADUC_PLATFORM_LAYER} STREQUAL "simulator") - target_compile_definitions (${PROJECT_NAME} PUBLIC ADUC_SIMULATOR_MODE=1) -endif () diff --git a/src/extensions/extension_manager/inc/aduc/extension_manager.h b/src/extensions/extension_manager/inc/aduc/extension_manager.h index 81abe136b..665be7961 100644 --- a/src/extensions/extension_manager/inc/aduc/extension_manager.h +++ b/src/extensions/extension_manager/inc/aduc/extension_manager.h @@ -9,6 +9,12 @@ #ifndef ADUC_EXTENSION_MANAGER_H #define ADUC_EXTENSION_MANAGER_H +#include /* ExtensionManager_DownloadOptions */ +#include /* ADUC_RESULT */ +#include /* ADUC_DownloadProgressCallback */ +#include /* ADUC_FileEntity */ +#include /* ADUC_WorkflowHandle */ + EXTERN_C_BEGIN /** @@ -20,22 +26,34 @@ EXTERN_C_BEGIN ADUC_Result ExtensionManager_InitializeContentDownloader(const char* initializeData); /** - * @brief Downloads using the content downloader extension. + * @brief Downloads by using metadata download handler id and falls back to download with content downloader extension. * * @param entity The file entity to download. - * @param workflowId The workflow id. - * @param workFolder The work folder sandbox. - * @param retryTimeout The retry timeout. + * @param workflowHandle The workflow handle opaque object for per-workflow workflow data. + * @param options The options for the download. * @param downloadProgressCallback The download progress callback. * @return ADUC_Result The result of downloading. */ -ADUC_Result ExtensionManager_Download(const ADUC_FileEntity* entity, const char* workflowId, const char* workFolder, unsigned int retryTimeout, ADUC_DownloadProgressCallback downloadProgressCallback); +ADUC_Result ExtensionManager_Download( + const ADUC_FileEntity* entity, + ADUC_WorkflowHandle workflowHandle, + ExtensionManager_Download_Options* options, + ADUC_DownloadProgressCallback downloadProgressCallback); /** * @brief Uninitializes the extension manager. */ void ExtensionManager_Uninit(); +/** + * @brief Gets the file path of the entity target update under the download work folder sandbox. + * + * @param workflowHandle The workflow handle. + * @param entity The file entity. + * @return char* the workflow folder, or NULL on error. + */ +char* ExtensionManager_GetEntityWorkFolderFilePath(ADUC_WorkflowHandle workflowHandle, const ADUC_FileEntity* entity); + EXTERN_C_END #endif // ADUC_EXTENSION_MANAGER_H diff --git a/src/extensions/extension_manager/inc/aduc/extension_manager.hpp b/src/extensions/extension_manager/inc/aduc/extension_manager.hpp index 9b852b264..849b1e9a7 100644 --- a/src/extensions/extension_manager/inc/aduc/extension_manager.hpp +++ b/src/extensions/extension_manager/inc/aduc/extension_manager.hpp @@ -9,37 +9,36 @@ #ifndef ADUC_EXTENSION_MANAGER_HPP #define ADUC_EXTENSION_MANAGER_HPP -#include "aduc/component_enumerator_extension.hpp" -#include "aduc/extension_utils.h" -#include "aduc/result.h" +#include +#include +#include +#include // ADUC_LOG_SEVERITY +#include // ADUC_Result +#include // ADUC_DownloadProgressCallback +#include // ADUC_FileEntity #include #include #include -typedef enum tagADUC_ExtensionType -{ - ADUC_UPDATE_CONTENT_HADDLER, - ADUC_CONTENT_DOWNLOADER, - ADUC_COMPONENT_ENUMERATOR, -} ADUC_ExtensionType; - // Default DO retry timeout is 24 hours. #define DO_RETRY_TIMEOUT_DEFAULT (60 * 60 * 24) // Forward declaration. class ContentHandler; -typedef ContentHandler* (*UPDATE_CONTENT_HANDLER_CREATE_PROC)(ADUC_LOG_SEVERITY logLevel); +using ADUC_WorkflowHandle = void*; class ExtensionManager { public: static ADUC_Result LoadContentDownloaderLibrary(void** contentDownloaderLibrary); static ADUC_Result SetContentDownloaderLibrary(void* contentDownloaderLibrary); + static ADUC_Result GetContentDownloaderContractVersion(ADUC_ExtensionContractInfo* contractInfo); static bool IsComponentsEnumeratorRegistered(); static ADUC_Result LoadComponentEnumeratorLibrary(void** componentEnumerator); + static ADUC_Result GetComponentEnumeratorContractVersion(ADUC_ExtensionContractInfo* contractInfo); static ADUC_Result LoadUpdateContentHandlerExtension(const std::string& updateType, ContentHandler** handler); static ADUC_Result SetUpdateContentHandlerExtension(const std::string& updateType, ContentHandler* handler); @@ -70,17 +69,15 @@ class ExtensionManager * @brief * * @param entity An #ADUC_FileEntity object with information of the file to be downloaded. - * @param workflowId A workflow identifier. - * @param workFolder A full path to target directory (sandbox). - * @param retryTimeout A download retry timeout (in seconds). + * @param workflowHandle The workflow handle opaque object for per-workflow workflow data. + * @param downloadOptions The download options. * @param downloadProgressCallback A download progress reporting callback. * @return ADUC_Result */ static ADUC_Result Download( const ADUC_FileEntity* entity, - const char* workflowId, - const char* workFolder, - unsigned int retryTimeout, + ADUC_WorkflowHandle workflowHandle, + ExtensionManager_Download_Options* downloadOptions, ADUC_DownloadProgressCallback downloadProgressCallback); private: @@ -102,8 +99,9 @@ class ExtensionManager static std::unordered_map _libs; static std::unordered_map _contentHandlers; static void* _contentDownloader; + static ADUC_ExtensionContractInfo _contentDownloaderContractVersion; static void* _componentEnumerator; - static pthread_mutex_t factoryMutex; + static ADUC_ExtensionContractInfo _componentEnumeratorContractVersion; }; #endif // ADUC_EXTENSION_MANAGER_HPP diff --git a/src/extensions/extension_manager/inc/aduc/extension_manager_download_options.h b/src/extensions/extension_manager/inc/aduc/extension_manager_download_options.h new file mode 100644 index 000000000..84b12f4de --- /dev/null +++ b/src/extensions/extension_manager/inc/aduc/extension_manager_download_options.h @@ -0,0 +1,9 @@ +#ifndef ADUC_EXTENSION_MANAGER_DOWNLOAD_OPTIONS_H +#define ADUC_EXTENSION_MANAGER_DOWNLOAD_OPTIONS_H + +typedef struct tagADUC_ExtensionManager_Download_Options +{ + unsigned int retryTimeout; +} ExtensionManager_Download_Options; + +#endif // #define ADUC_EXTENSION_MANAGER_DOWNLOAD_OPTIONS_H diff --git a/src/extensions/extension_manager/src/extension_manager.cpp b/src/extensions/extension_manager/src/extension_manager.cpp index 48bb2510f..b68863283 100644 --- a/src/extensions/extension_manager/src/extension_manager.cpp +++ b/src/extensions/extension_manager/src/extension_manager.cpp @@ -5,45 +5,58 @@ * @copyright Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ -#include "aduc/component_enumerator_extension.hpp" -#include "aduc/content_downloader_extension.hpp" -#include "aduc/content_handler.hpp" - -#include "aduc/c_utils.h" -#include "aduc/exceptions.hpp" -#include "aduc/extension_manager.hpp" -#include "aduc/extension_utils.h" -#include "aduc/hash_utils.h" // for SHAversion -#include "aduc/logging.h" -#include "aduc/parser_utils.h" -#include "aduc/result.h" -#include "aduc/string_utils.hpp" + +#include +#include +#include +#include +#include + +#include +#include // ADUC::StringUtils::cstr_wrapper +#include +#include +#include +#include +#include +#include +#include // for SHAversion +#include +#include +#include // SanitizePathSegment +#include +#include +#include +#include +#include // ADUC_WorkflowHandle +#include #include #include -#include - -#include -#include -#include // for mallocAndStrcpy_s // Note: this requires ${CMAKE_DL_LIBS} #include #include +// type aliases +using UPDATE_CONTENT_HANDLER_CREATE_PROC = ContentHandler* (*)(ADUC_LOG_SEVERITY logLevel); +using GET_CONTRACT_INFO_PROC = ADUC_Result (*)(ADUC_ExtensionContractInfo* contractInfo); +using WorkflowHandle = void*; +using ADUC::StringUtils::cstr_wrapper; + +EXTERN_C_BEGIN +ExtensionManager_Download_Options Default_ExtensionManager_Download_Options = { + .retryTimeout = 60 * 60 * 24 /* default : 24 hour */, +}; +EXTERN_C_END + // Static members. std::unordered_map ExtensionManager::_libs; std::unordered_map ExtensionManager::_contentHandlers; void* ExtensionManager::_contentDownloader; +ADUC_ExtensionContractInfo ExtensionManager::_contentDownloaderContractVersion; void* ExtensionManager::_componentEnumerator; - -STRING_HANDLE FolderNameFromHandlerId(const char* handlerId) -{ - STRING_HANDLE name = STRING_construct(handlerId); - STRING_replace(name, '/', '_'); - STRING_replace(name, ':', '_'); - return name; -} +ADUC_ExtensionContractInfo ExtensionManager::_componentEnumeratorContractVersion; /** * @brief Loads extension shared library file. @@ -191,10 +204,10 @@ ExtensionManager::LoadUpdateContentHandlerExtension(const std::string& updateTyp { ADUC_Result result = { ADUC_Result_Failure }; - UPDATE_CONTENT_HANDLER_CREATE_PROC createUpdateContentHandlerExtension = nullptr; + UPDATE_CONTENT_HANDLER_CREATE_PROC createUpdateContentHandlerExtensionFn = nullptr; + GET_CONTRACT_INFO_PROC getContractInfoFn = nullptr; void* libHandle = nullptr; - STRING_HANDLE folderName = nullptr; - STRING_HANDLE path = nullptr; + ADUC_ExtensionContractInfo contractInfo{}; Log_Info("Loading Update Content Handler for '%s'.", updateType.c_str()); @@ -203,7 +216,14 @@ ExtensionManager::LoadUpdateContentHandlerExtension(const std::string& updateTyp Log_Error("Invalid argument(s)."); result.ExtendedResultCode = ADUC_ERC_EXTENSION_CREATE_FAILURE_INVALID_ARG(ADUC_FACILITY_EXTENSION_UPDATE_CONTENT_HANDLER, 0); - goto done; + return result; + } + + ADUC::StringUtils::STRING_HANDLE_wrapper folderName{ SanitizePathSegment(updateType.c_str()) }; + if (folderName.is_null()) + { + result.ExtendedResultCode = ADUC_ERC_NOMEM; + return result; } // Try to find cached handler. @@ -229,13 +249,10 @@ ExtensionManager::LoadUpdateContentHandlerExtension(const std::string& updateTyp goto done; } - folderName = FolderNameFromHandlerId(updateType.c_str()); - path = STRING_construct_sprintf("%s/%s", ADUC_UPDATE_CONTENT_HANDLER_EXTENSION_DIR, STRING_c_str(folderName)); - result = LoadExtensionLibrary( updateType.c_str(), ADUC_UPDATE_CONTENT_HANDLER_EXTENSION_DIR, - STRING_c_str(folderName), + folderName.c_str(), ADUC_UPDATE_CONTENT_HANDLER_REG_FILENAME, "CreateUpdateContentHandlerExtension", ADUC_FACILITY_EXTENSION_UPDATE_CONTENT_HANDLER, @@ -250,10 +267,10 @@ ExtensionManager::LoadUpdateContentHandlerExtension(const std::string& updateTyp dlerror(); // Clear any existing error // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - createUpdateContentHandlerExtension = - reinterpret_cast(dlsym(libHandle, "CreateUpdateContentHandlerExtension")); + createUpdateContentHandlerExtensionFn = reinterpret_cast( + dlsym(libHandle, CONTENT_HANDLER__CreateUpdateContentHandlerExtension__EXPORT_SYMBOL)); - if (createUpdateContentHandlerExtension == nullptr) + if (createUpdateContentHandlerExtensionFn == nullptr) { Log_Error("The specified function doesn't exist. %s\n", dlerror()); result.ExtendedResultCode = @@ -263,15 +280,15 @@ ExtensionManager::LoadUpdateContentHandlerExtension(const std::string& updateTyp try { - *handler = createUpdateContentHandlerExtension(ADUC_Logging_GetLevel()); + *handler = createUpdateContentHandlerExtensionFn(ADUC_Logging_GetLevel()); } catch (const std::exception& ex) { - Log_Debug("An exception occurred while creating update handler: %s", ex.what()); + Log_Error("An exception occurred while creating update handler: %s", ex.what()); } catch (...) { - Log_Debug("Unknown exception occurred while creating update handler for '%s'", updateType.c_str()); + Log_Error("Unknown exception occurred while creating update handler for '%s'", updateType.c_str()); } if (*handler == nullptr) @@ -280,6 +297,43 @@ ExtensionManager::LoadUpdateContentHandlerExtension(const std::string& updateTyp goto done; } + Log_Debug("Determining contract version for '%s'.", updateType.c_str()); + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + getContractInfoFn = + reinterpret_cast(dlsym(libHandle, CONTENT_HANDLER__GetContractInfo__EXPORT_SYMBOL)); + + if (getContractInfoFn == nullptr) + { + Log_Info( + "No '" CONTENT_HANDLER__GetContractInfo__EXPORT_SYMBOL "' symbol for '%s'. Defaulting V1.0", + updateType.c_str()); + + contractInfo.majorVer = ADUC_V1_CONTRACT_MAJOR_VER; + contractInfo.minorVer = ADUC_V1_CONTRACT_MINOR_VER; + } + else + { + result = getContractInfoFn(&contractInfo); + if (IsAducResultCodeFailure(result.ResultCode)) + { + Log_Error( + "'%s' extension call ERC: %08x", + CONTENT_HANDLER__GetContractInfo__EXPORT_SYMBOL, + result.ExtendedResultCode); + result.ExtendedResultCode = ADUC_ERC_UPDATE_CONTENT_HANDLER_GET_CONTRACT_INFO_CALL_FAILURE; + goto done; + } + + Log_Debug( + "Got %d.%d contract version for '%s' content handler", + contractInfo.majorVer, + contractInfo.minorVer, + updateType.c_str()); + } + + (*handler)->SetContractInfo(contractInfo); + Log_Debug("Caching new content handler for '%s'.", updateType.c_str()); _contentHandlers.emplace(updateType, *handler); @@ -295,9 +349,6 @@ ExtensionManager::LoadUpdateContentHandlerExtension(const std::string& updateTyp } } - STRING_delete(folderName); - STRING_delete(path); - return result; } @@ -367,8 +418,10 @@ void ExtensionManager::Uninit() ADUC_Result ExtensionManager::LoadContentDownloaderLibrary(void** contentDownloaderLibrary) { ADUC_Result result = { ADUC_Result_Failure }; - static const char* functionNames[] = { "Download", "Initialize" }; + static const char* functionNames[] = { CONTENT_DOWNLOADER__Initialize__EXPORT_SYMBOL, + CONTENT_DOWNLOADER__Download__EXPORT_SYMBOL }; void* extensionLib = nullptr; + GET_CONTRACT_INFO_PROC getContractInfoFn = nullptr; if (_contentDownloader != nullptr) { @@ -406,6 +459,27 @@ ADUC_Result ExtensionManager::LoadContentDownloaderLibrary(void** contentDownloa } } + Log_Debug("Determining contract version for content downloader."); + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + getContractInfoFn = reinterpret_cast( + dlsym(extensionLib, CONTENT_DOWNLOADER__GetContractInfo__EXPORT_SYMBOL)); + if (getContractInfoFn == nullptr) + { + _contentDownloaderContractVersion.majorVer = ADUC_V1_CONTRACT_MAJOR_VER; + _contentDownloaderContractVersion.minorVer = ADUC_V1_CONTRACT_MINOR_VER; + Log_Debug("No " CONTENT_DOWNLOADER__GetContractInfo__EXPORT_SYMBOL + "export. Defaulting to V1 contract for content downloader"); + } + else + { + result = getContractInfoFn(&_contentDownloaderContractVersion); + Log_Debug( + "Got Contract %d.%d for content downloader", + _contentDownloaderContractVersion.majorVer, + _contentDownloaderContractVersion.minorVer); + } + *contentDownloaderLibrary = _contentDownloader = extensionLib; result = { ADUC_Result_Success }; @@ -421,6 +495,19 @@ ADUC_Result ExtensionManager::SetContentDownloaderLibrary(void* contentDownloade return result; } +ADUC_Result ExtensionManager::GetContentDownloaderContractVersion(ADUC_ExtensionContractInfo* contractInfo) +{ + *contractInfo = _contentDownloaderContractVersion; + return ADUC_Result{ ADUC_GeneralResult_Success, 0 }; +} + +ADUC_Result ExtensionManager::GetComponentEnumeratorContractVersion(ADUC_ExtensionContractInfo* contractInfo) +{ + ADUC_Result result = { ADUC_Result_Success }; + *contractInfo = _componentEnumeratorContractVersion; + return result; +} + bool ExtensionManager::IsComponentsEnumeratorRegistered() { void* extensionLib = nullptr; @@ -433,7 +520,8 @@ ADUC_Result ExtensionManager::LoadComponentEnumeratorLibrary(void** componentEnu ADUC_Result result = { ADUC_Result_Failure }; void* mainFunc = nullptr; void* extensionLib = nullptr; - const char* requiredFunction = "GetAllComponents"; + const char* requiredFunction = COMPONENT_ENUMERATOR__GetAllComponents__EXPORT_SYMBOL; + GET_CONTRACT_INFO_PROC getContractInfoFn = nullptr; if (_componentEnumerator != nullptr) { @@ -475,6 +563,26 @@ ADUC_Result ExtensionManager::LoadComponentEnumeratorLibrary(void** componentEnu goto done; } + Log_Debug("Determining contract version for component enumerator."); + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + getContractInfoFn = reinterpret_cast( + dlsym(extensionLib, COMPONENT_ENUMERATOR__GetContractInfo__EXPORT_SYMBOL)); + if (getContractInfoFn == nullptr) + { + _componentEnumeratorContractVersion.majorVer = ADUC_V1_CONTRACT_MAJOR_VER; + _componentEnumeratorContractVersion.minorVer = ADUC_V1_CONTRACT_MINOR_VER; + Log_Debug("default to V1 contract for component enumerator"); + } + else + { + getContractInfoFn(&_componentEnumeratorContractVersion); + Log_Debug( + "contract %d.%d for component enumerator", + _componentEnumeratorContractVersion.majorVer, + _componentEnumeratorContractVersion.minorVer); + } + *componentEnumerator = _componentEnumerator = extensionLib; result = { ADUC_Result_Success }; @@ -494,26 +602,41 @@ void ExtensionManager::_FreeComponentsDataString(char* componentsJson) goto done; } - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - freeComponentsDataStringProc = reinterpret_cast( - dlsym(lib, "FreeComponentsDataString")); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - - if (freeComponentsDataStringProc == nullptr) + if (ExtensionManager::_componentEnumeratorContractVersion.majorVer == ADUC_V1_CONTRACT_MAJOR_VER + && ExtensionManager::_componentEnumeratorContractVersion.minorVer == ADUC_V1_CONTRACT_MINOR_VER) { - result = { .ResultCode = ADUC_Result_Failure, - .ExtendedResultCode = ADUC_ERC_COMPONENT_ENUMERATOR_FREECOMPONENTSDATASTRING_NOTIMP }; + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + freeComponentsDataStringProc = reinterpret_cast(dlsym( + lib, + COMPONENT_ENUMERATOR__FreeComponentsDataString__EXPORT_SYMBOL)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - goto done; - } + if (freeComponentsDataStringProc == nullptr) + { + result = { .ResultCode = ADUC_Result_Failure, + .ExtendedResultCode = ADUC_ERC_COMPONENT_ENUMERATOR_FREECOMPONENTSDATASTRING_NOTIMP }; - try - { - freeComponentsDataStringProc(componentsJson); + goto done; + } + + try + { + freeComponentsDataStringProc(componentsJson); + } + catch (...) + { + result = { .ResultCode = ADUC_Result_Failure, + .ExtendedResultCode = ADUC_ERC_COMPONENT_ENUMERATOR_EXCEPTION_FREECOMPONENTSDATASTRING }; + goto done; + } } - catch (...) + else { - result = { .ResultCode = ADUC_Result_Failure, - .ExtendedResultCode = ADUC_ERC_COMPONENT_ENUMERATOR_EXCEPTION_FREECOMPONENTSDATASTRING }; + Log_Error( + "Unsupported contract %d.%d", + ExtensionManager::_componentEnumeratorContractVersion.majorVer, + ExtensionManager::_componentEnumeratorContractVersion.minorVer); + result.ResultCode = ADUC_Result_Failure; + result.ExtendedResultCode = ADUC_ERC_COMPONENT_ENUMERATOR_UNSUPPORTED_CONTRACT_VERSION; goto done; } @@ -532,8 +655,9 @@ void ExtensionManager::_FreeComponentsDataString(char* componentsJson) */ ADUC_Result ExtensionManager::GetAllComponents(std::string& outputComponentsData) { + static GetAllComponentsProc _getAllComponents = nullptr; + void* lib = nullptr; - GetAllComponentsProc _getAllComponents = nullptr; char* components = nullptr; outputComponentsData = ""; @@ -544,33 +668,51 @@ ADUC_Result ExtensionManager::GetAllComponents(std::string& outputComponentsData goto done; } - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - _getAllComponents = reinterpret_cast(dlsym(lib, "GetAllComponents")); - if (_getAllComponents == nullptr) + if (ExtensionManager::_componentEnumeratorContractVersion.majorVer == ADUC_V1_CONTRACT_MAJOR_VER + && ExtensionManager::_componentEnumeratorContractVersion.minorVer == ADUC_V1_CONTRACT_MINOR_VER) { - result = { .ResultCode = ADUC_Result_Failure, - .ExtendedResultCode = ADUC_ERC_COMPONENT_ENUMERATOR_GETALLCOMPONENTS_NOTIMP }; - goto done; - } + if (_getAllComponents == nullptr) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + _getAllComponents = reinterpret_cast( + dlsym(lib, COMPONENT_ENUMERATOR__GetAllComponents__EXPORT_SYMBOL)); + if (_getAllComponents == nullptr) + { + result = { .ResultCode = ADUC_Result_Failure, + .ExtendedResultCode = ADUC_ERC_COMPONENT_ENUMERATOR_GETALLCOMPONENTS_NOTIMP }; + goto done; + } + } - try - { - components = _getAllComponents(); + try + { + components = _getAllComponents(); + } + catch (...) + { + result = { .ResultCode = ADUC_Result_Failure, + .ExtendedResultCode = ADUC_ERC_COMPONENT_ENUMERATOR_EXCEPTION_GETALLCOMPONENTS }; + goto done; + } + + if (components != nullptr) + { + outputComponentsData = components; + _FreeComponentsDataString(components); + } } - catch (...) + else { - result = { .ResultCode = ADUC_Result_Failure, - .ExtendedResultCode = ADUC_ERC_COMPONENT_ENUMERATOR_EXCEPTION_GETALLCOMPONENTS }; + Log_Error( + "Unsupported contract version %d.%d", + ExtensionManager::_componentEnumeratorContractVersion.majorVer, + ExtensionManager::_componentEnumeratorContractVersion.minorVer); + result.ResultCode = ADUC_GeneralResult_Failure; + result.ExtendedResultCode = ADUC_ERC_COMPONENT_ENUMERATOR_UNSUPPORTED_CONTRACT_VERSION; goto done; } - if (components != nullptr) - { - outputComponentsData = components; - _FreeComponentsDataString(components); - } - - result = { ADUC_GeneralResult_Success }; + result = { ADUC_GeneralResult_Success, 0 }; done: return result; @@ -596,7 +738,8 @@ ADUC_Result ExtensionManager::SelectComponents(const std::string& selector, std: } // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - _selectComponents = reinterpret_cast(dlsym(lib, "SelectComponents")); + _selectComponents = + reinterpret_cast(dlsym(lib, COMPONENT_ENUMERATOR__SelectComponents__EXPORT_SYMBOL)); if (_selectComponents == nullptr) { result = { .ResultCode = ADUC_Result_Failure, @@ -637,8 +780,20 @@ ADUC_Result ExtensionManager::InitializeContentDownloader(const char* initialize goto done; } + if (ExtensionManager::_contentDownloaderContractVersion.majorVer != ADUC_V1_CONTRACT_MAJOR_VER + && ExtensionManager::_contentDownloaderContractVersion.minorVer != ADUC_V1_CONTRACT_MINOR_VER) + { + Log_Error( + "Unsupported contract version %d.%d", + ExtensionManager::_contentDownloaderContractVersion.majorVer, + ExtensionManager::_contentDownloaderContractVersion.minorVer); + result.ResultCode = ADUC_GeneralResult_Failure; + result.ExtendedResultCode = ADUC_ERC_CONTENT_DOWNLOADER_UNSUPPORTED_CONTRACT_VERSION; + goto done; + } + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - _initialize = reinterpret_cast(dlsym(lib, "Initialize")); + _initialize = reinterpret_cast(dlsym(lib, CONTENT_DOWNLOADER__Initialize__EXPORT_SYMBOL)); if (_initialize == nullptr) { result = { .ResultCode = ADUC_Result_Failure, @@ -663,23 +818,19 @@ ADUC_Result ExtensionManager::InitializeContentDownloader(const char* initialize ADUC_Result ExtensionManager::Download( const ADUC_FileEntity* entity, - const char* workflowId, - const char* workFolder, - unsigned int retryTimeout, + WorkflowHandle workflowHandle, + ExtensionManager_Download_Options* options, ADUC_DownloadProgressCallback downloadProgressCallback) { void* lib = nullptr; DownloadProc downloadProc = nullptr; char* components = nullptr; SHAversion algVersion; - std::stringstream childManifestFile; - ADUC_Result result; - try - { - childManifestFile << workFolder << "/" << entity->TargetFilename; - } - catch (...) + ADUC_Result result = { .ResultCode = ADUC_Result_Failure, .ExtendedResultCode = 0 }; + ADUC::StringUtils::STRING_HANDLE_wrapper targetUpdateFilePath{ nullptr }; + + if (!workflow_get_entity_workfolder_filepath(workflowHandle, entity, targetUpdateFilePath.address_of())) { Log_Error("Cannot construct child manifest file path."); result = { .ResultCode = ADUC_Result_Failure, @@ -693,8 +844,20 @@ ADUC_Result ExtensionManager::Download( goto done; } + if (ExtensionManager::_contentDownloaderContractVersion.majorVer != ADUC_V1_CONTRACT_MAJOR_VER + && ExtensionManager::_contentDownloaderContractVersion.minorVer != ADUC_V1_CONTRACT_MINOR_VER) + { + Log_Error( + "Unsupported contract version %d.%d", + ExtensionManager::_contentDownloaderContractVersion.majorVer, + ExtensionManager::_contentDownloaderContractVersion.minorVer); + result.ResultCode = ADUC_GeneralResult_Failure; + result.ExtendedResultCode = ADUC_ERC_CONTENT_DOWNLOADER_UNSUPPORTED_CONTRACT_VERSION; + goto done; + } + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - downloadProc = reinterpret_cast(dlsym(lib, "Download")); + downloadProc = reinterpret_cast(dlsym(lib, CONTENT_DOWNLOADER__Download__EXPORT_SYMBOL)); if (downloadProc == nullptr) { result = { .ResultCode = ADUC_Result_Failure, @@ -707,7 +870,7 @@ ADUC_Result ExtensionManager::Download( { Log_Error( "FileEntity for %s has unsupported hash type %s", - childManifestFile.str().c_str(), + targetUpdateFilePath.c_str(), ADUC_HashUtils_GetHashType(entity->Hash, entity->HashCount, 0)); result.ExtendedResultCode = ADUC_ERC_CONTENT_DOWNLOADER_FILE_HASH_TYPE_NOT_SUPPORTED; goto done; @@ -715,9 +878,9 @@ ADUC_Result ExtensionManager::Download( // If file exists and has a valid hash, then skip download. // Otherwise, delete an existing file, then download. - Log_Debug("Check whether '%s' has already been download into the work folder.", childManifestFile.str().c_str()); + Log_Debug("Check whether '%s' has already been download into the work folder.", targetUpdateFilePath.c_str()); - if (access(childManifestFile.str().c_str(), F_OK) == 0) + if (access(targetUpdateFilePath.c_str(), F_OK) == 0) { char* hashValue = ADUC_HashUtils_GetHashValue(entity->Hash, entity->HashCount, 0 /* index */); if (hashValue == nullptr) @@ -730,12 +893,12 @@ ADUC_Result ExtensionManager::Download( // If target file exists, validate file hash. // If file is valid, then skip the download. bool validHash = ADUC_HashUtils_IsValidFileHash( - childManifestFile.str().c_str(), hashValue, algVersion, false /* suppressErrorLog */); + targetUpdateFilePath.c_str(), hashValue, algVersion, false /* suppressErrorLog */); if (!validHash) { // Delete existing file. - if (remove(childManifestFile.str().c_str()) != 0) + if (remove(targetUpdateFilePath.c_str()) != 0) { Log_Error("Cannot delete existing file that has invalid hash."); result.ExtendedResultCode = ADUC_ERC_CONTENT_DOWNLOADER_CANNOT_DELETE_EXISTING_FILE; @@ -749,7 +912,83 @@ ADUC_Result ExtensionManager::Download( try { - result = downloadProc(entity, workflowId, workFolder, retryTimeout, downloadProgressCallback); + const char* workflowId = workflow_peek_id(workflowHandle); + cstr_wrapper workFolder{ workflow_get_workfolder(workflowHandle) }; + + result = { .ResultCode = ADUC_Result_Failure, .ExtendedResultCode = 0 }; + + // First, attempt to produce the update using download handler if + // download handler exists in the entity (metadata). + if (!IsNullOrEmpty(entity->DownloadHandlerId)) + { + DownloadHandlerFactory* factory = DownloadHandlerFactory::GetInstance(); + DownloadHandlerPlugin* plugin = factory->LoadDownloadHandler(entity->DownloadHandlerId); + if (plugin == nullptr) + { + Log_Warn("Load Download Handler %s failed", entity->DownloadHandlerId); + + workflow_set_success_erc( + workflowHandle, ADUC_ERC_DOWNLOAD_HANDLER_EXTENSION_MANAGER_CREATE_FAILURE_CREATE); + } + else + { + ADUC_ExtensionContractInfo contractInfo{}; + + Log_Debug("Getting contract info for download handler '%s'.", entity->DownloadHandlerId); + + result = plugin->GetContractInfo(&contractInfo); + + if (IsAducResultCodeFailure(result.ResultCode)) + { + Log_Error( + "GetContractInfo failed for download handler '%s': result 0x%08x, erc 0x%08x", + entity->DownloadHandlerId, + result.ResultCode, + result.ExtendedResultCode); + goto done; + } + + Log_Debug( + "Downloadhandler '%s' Contract Version: %d.%d", + entity->DownloadHandlerId, + contractInfo.majorVer, + contractInfo.minorVer); + + if (contractInfo.majorVer == ADUC_V1_CONTRACT_MAJOR_VER + && contractInfo.minorVer == ADUC_V1_CONTRACT_MINOR_VER) + { + result = plugin->ProcessUpdate(workflowHandle, entity, targetUpdateFilePath.c_str()); + if (IsAducResultCodeFailure(result.ResultCode)) + { + Log_Warn( + "Download handler failed to produce update: result 0x%08x, erc 0x%08x", + result.ResultCode, + result.ExtendedResultCode); + + workflow_set_success_erc(workflowHandle, result.ExtendedResultCode); + } + } + else + { + Log_Error("Unsupported contract %d.%d", contractInfo.majorVer, contractInfo.minorVer); + + result.ResultCode = ADUC_GeneralResult_Failure; + result.ExtendedResultCode = + ADUC_ERC_DOWNLOAD_HANDLER_EXTENSION_MANAGER_UNSUPPORTED_CONTRACT_VERSION; + + goto done; + } + } + } + + // If no download handlers specified, or download handler failed to produce the target file. + if (IsAducResultCodeFailure(result.ResultCode) + || result.ResultCode == ADUC_Result_Download_Handler_RequiredFullDownload) + { + // Either download handler id did not exist, or download handler failed and doing fallback here. + result = + downloadProc(entity, workflowId, workFolder.get(), options->retryTimeout, downloadProgressCallback); + } } catch (...) { @@ -761,18 +1000,25 @@ ADUC_Result ExtensionManager::Download( if (IsAducResultCodeSuccess(result.ResultCode)) { if (!ADUC_HashUtils_IsValidFileHash( - childManifestFile.str().c_str(), - ADUC_HashUtils_GetHashValue(entity->Hash, entity->HashCount, 0 /* index */), + targetUpdateFilePath.c_str(), + ADUC_HashUtils_GetHashValue(entity->Hash, entity->HashCount, 0), algVersion, - true)) + false)) { result = { .ResultCode = ADUC_Result_Failure, - .ExtendedResultCode = ADUC_ERC_CONTENT_DOWNLOADER_DOWNLOAD_EXCEPTION }; + .ExtendedResultCode = ADUC_ERC_CONTENT_DOWNLOADER_INVALID_FILE_HASH }; + + workflow_set_success_erc(workflowHandle, result.ExtendedResultCode); + goto done; } } + result.ResultCode = ADUC_GeneralResult_Success; + result.ExtendedResultCode = 0; + done: + return result; } @@ -785,12 +1031,11 @@ ADUC_Result ExtensionManager_InitializeContentDownloader(const char* initializeD ADUC_Result ExtensionManager_Download( const ADUC_FileEntity* entity, - const char* workflowId, - const char* workFolder, - unsigned int retryTimeout, + ADUC_WorkflowHandle workflowHandle, + ExtensionManager_Download_Options* options, ADUC_DownloadProgressCallback downloadProgressCallback) { - return ExtensionManager::Download(entity, workflowId, workFolder, retryTimeout, downloadProgressCallback); + return ExtensionManager::Download(entity, workflowHandle, options, downloadProgressCallback); } /** diff --git a/src/extensions/inc/aduc/component_enumerator_extension.hpp b/src/extensions/inc/aduc/component_enumerator_extension.hpp index db41d7107..840c00b2f 100644 --- a/src/extensions/inc/aduc/component_enumerator_extension.hpp +++ b/src/extensions/inc/aduc/component_enumerator_extension.hpp @@ -9,10 +9,11 @@ #ifndef _COMPONENT_ENUMERATOR_EXTENSION_HPP_ #define _COMPONENT_ENUMERATOR_EXTENSION_HPP_ +#include #include #include -extern "C" { +EXTERN_C_BEGIN /** * @brief Select component(s) that contain property or properties matching specified in @p selectorJson string. @@ -134,6 +135,6 @@ typedef char* (*GetAllComponentsProc)(); */ typedef void (*FreeComponentsDataStringProc)(char* string); -} // extern "c" +EXTERN_C_END #endif // _COMPONENT_ENUMERATOR_EXTENSION_HPP_ diff --git a/src/extensions/inc/aduc/content_downloader_extension.hpp b/src/extensions/inc/aduc/content_downloader_extension.hpp index d4d7a668b..a5308688b 100644 --- a/src/extensions/inc/aduc/content_downloader_extension.hpp +++ b/src/extensions/inc/aduc/content_downloader_extension.hpp @@ -10,12 +10,17 @@ #include "aduc/adu_core_exports.h" -extern "C" { +EXTERN_C_BEGIN typedef ADUC_Result (*InitializeProc)(const char* initializeData); -typedef ADUC_Result (*DownloadProc)(const ADUC_FileEntity* entity, const char* workflowId, const char* workFolder, unsigned int retryTimeout, ADUC_DownloadProgressCallback downloadProgressCallback); +typedef ADUC_Result (*DownloadProc)( + const ADUC_FileEntity* entity, + const char* workflowId, + const char* workFolder, + unsigned int retryTimeout, + ADUC_DownloadProgressCallback downloadProgressCallback); -} +EXTERN_C_END #endif // ADUC_CONTENT_DOWNLOADER_EXTENSION_HPP diff --git a/src/extensions/inc/aduc/content_handler.hpp b/src/extensions/inc/aduc/content_handler.hpp index 0bdf71b32..08c2ebcdf 100644 --- a/src/extensions/inc/aduc/content_handler.hpp +++ b/src/extensions/inc/aduc/content_handler.hpp @@ -8,17 +8,9 @@ #ifndef ADUC_CONTENT_HANDLER_HPP #define ADUC_CONTENT_HANDLER_HPP -#ifdef __cplusplus -# define EXTERN_C_BEGIN \ - extern "C" \ - { -# define EXTERN_C_END } -#else -# define EXTERN_C_BEGIN -# define EXTERN_C_END -#endif - -#include "aduc/result.h" +#include +#include +#include // Forward declation. struct tagADUC_WorkflowData; @@ -37,8 +29,10 @@ class ContentHandler ContentHandler& operator=(ContentHandler&&) = delete; virtual ADUC_Result Download(const tagADUC_WorkflowData* workflowData) = 0; + virtual ADUC_Result Backup(const tagADUC_WorkflowData* workflowData) = 0; virtual ADUC_Result Install(const tagADUC_WorkflowData* workflowData) = 0; virtual ADUC_Result Apply(const tagADUC_WorkflowData* workflowData) = 0; + virtual ADUC_Result Restore(const tagADUC_WorkflowData* workflowData) = 0; virtual ADUC_Result Cancel(const tagADUC_WorkflowData* workflowData) = 0; virtual ADUC_Result IsInstalled(const tagADUC_WorkflowData* workflowData) = 0; @@ -46,8 +40,21 @@ class ContentHandler { } + void SetContractInfo(const ADUC_ExtensionContractInfo& info) + { + contractInfo = info; + } + + ADUC_ExtensionContractInfo GetContractInfo() const + { + return contractInfo; + } + protected: ContentHandler() = default; + +private: + ADUC_ExtensionContractInfo contractInfo{}; }; #endif // ADUC_CONTENT_HANDLER_HPP diff --git a/src/extensions/inc/aduc/exports/extension_common_export_symbols.h b/src/extensions/inc/aduc/exports/extension_common_export_symbols.h new file mode 100644 index 000000000..a9d6b2b5d --- /dev/null +++ b/src/extensions/inc/aduc/exports/extension_common_export_symbols.h @@ -0,0 +1,38 @@ +/** + * @file extension_common_export_symbols.h + * @brief The common function export symbols used by specific extension export symbols headers. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#ifndef EXTENSION_COMMON_EXPORT_SYMBOLS_H +#define EXTENSION_COMMON_EXPORT_SYMBOLS_H + +// +// Contract Info Symbols +// Any symbols in this section will exist in shared library of each extension +// type. +// +// It is only optional in pre-V1 legacy code. +// All extension implementations going forward should have this symbol +// available to be called. +// +// This symbol macro should not be used directly but referred to by +// other per-extension macros below, typically always the first one per +// extension type. +// + +// +// Version 1.0 Signatures and associated function export symbols +// + +/** + * @brief GetContractInfo - Gets the extension contract info that the extension implements. + * @param[out] contractInfo The output parameter the contract info. + * @returns ADUC_Result The result of the call. + * @details ADUC_Result GetContractInfo(ADUC_ExtensionContractInfo* contractInfo); + */ +#define GetContractInfo__EXPORT_SYMBOL "GetContractInfo" + +#endif // EXTENSION_COMMON_EXPORT_SYMBOLS_H diff --git a/src/extensions/inc/aduc/exports/extension_component_enumerator_export_symbols.h b/src/extensions/inc/aduc/exports/extension_component_enumerator_export_symbols.h new file mode 100644 index 000000000..6bad97dd1 --- /dev/null +++ b/src/extensions/inc/aduc/exports/extension_component_enumerator_export_symbols.h @@ -0,0 +1,68 @@ +/** + * @file extension_component_enumerator_export_symbols.h + * @brief The function export symbols for component enumerator extensions. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#ifndef EXTENSION_COMPONENT_ENUMERATOR_EXPORT_SYMBOLS_H +#define EXTENSION_COMPONENT_ENUMERATOR_EXPORT_SYMBOLS_H + +#include // for GetContractInfo__EXPORT_SYMBOL + +// +// Component Enumerator Extension export V1 symbols. +// These are the V1 symbols that must be implemented by Component Enumerator Extensions. +// + +// +// Version 1.0 Signatures and associated function export symbols +// + +/** + * @brief GetContractInfo - Gets the extension contract info that the extension implements. + * @param[out] contractInfo The output parameter the contract info. + * @returns ADUC_Result The result of the call. + * @details ADUC_Result GetContractInfo(ADUC_ExtensionContractInfo* contractInfo); + */ +#define COMPONENT_ENUMERATOR__GetContractInfo__EXPORT_SYMBOL GetContractInfo__EXPORT_SYMBOL + +/** + * @brief Returns all components information in JSON format. + * @param includeProperties Indicates whether to include optional component's properties in the output string. + * @return Returns a serialized json data contains components information. Caller must call FreeComponentsDataString function + * when done with the returned string. + * @details char* GetAllComponents() + */ +#define COMPONENT_ENUMERATOR__GetAllComponents__EXPORT_SYMBOL "GetAllComponents" + +/** + * @brief Select component(s) that contain property or properties matching specified in @p selectorJson string. + * + * Example input json: + * - Select all components belong to a 'Motors' group + * "{\"group\":\"Motors\"}" + * + * - Select a component with name equals 'left-motor' + * "{\"name\":\"left-motor\"}" + * + * - Select components matching specified class (manufature/model) + * "{\"manufacturer\":\"Contoso\",\"model\":\"USB-Motor-0001\"}" + * + * @param selectorJson A stringified json containing one or more properties use for components selection. + * @return Returns a serialized json data containing components information. + * Caller must call FreeString function when done with the returned string. + * @details char* SelectComponents(const char* selectorJson) + */ +#define COMPONENT_ENUMERATOR__SelectComponents__EXPORT_SYMBOL "SelectComponents" + +/** + * @brief Frees the components data string allocated by GetAllComponents. + * + * @param string The string previously returned by GetAllComponents. + * @details void FreeComponentsDataString(char* string) + */ +#define COMPONENT_ENUMERATOR__FreeComponentsDataString__EXPORT_SYMBOL "FreeComponentsDataString" + +#endif // EXTENSION_COMPONENT_ENUMERATOR_EXPORT_SYMBOLS_H diff --git a/src/extensions/inc/aduc/exports/extension_content_downloader_export_symbols.h b/src/extensions/inc/aduc/exports/extension_content_downloader_export_symbols.h new file mode 100644 index 000000000..14cb648d8 --- /dev/null +++ b/src/extensions/inc/aduc/exports/extension_content_downloader_export_symbols.h @@ -0,0 +1,48 @@ +#ifndef EXTENSION_CONTENT_DOWNLOADER_EXPORT_SYMBOLS_H +#define EXTENSION_CONTENT_DOWNLOADER_EXPORT_SYMBOLS_H + +#include // for GetContractInfo__EXPORT_SYMBOL + +// +// Content Downloader Extension export V1 symbols. +// These are the V1 symbols that must be implemented by a content downloader extension. +// + +/** + * @brief Gets the extension contract info. + * + * @param[out] contractInfo The extension contract info. + * @return ADUC_Result The result. + * @details ADUC_Result GetContractInfo(ADUC_ExtensionContractInfo* contractInfo) + */ +#define CONTENT_DOWNLOADER__GetContractInfo__EXPORT_SYMBOL GetContractInfo__EXPORT_SYMBOL + +/** + * @brief Initializes the content downloader. + * + * @param initializeData The initialization data. + * @return ADUC_Result The result. + * @details ADUC_Result Initialize(const char* initializeData) + */ +#define CONTENT_DOWNLOADER__Initialize__EXPORT_SYMBOL "Initialize" + +/** + * @brief The download export. + * + * @param entity The file entity. + * @param workflowId The workflow id. + * @param workFolder The work folder for the update payloads. + * @param retryTimeout The retry timeout. + * @param downloadProgressCallback The download progress callback function. + * @return ADUC_Result The result. + * @details +ADUC_Result Download( + const ADUC_FileEntity* entity, + const char* workflowId, + const char* workFolder, + unsigned int retryTimeout, + ADUC_DownloadProgressCallback downloadProgressCallback) + */ +#define CONTENT_DOWNLOADER__Download__EXPORT_SYMBOL "Download" + +#endif // EXTENSION_CONTENT_DOWNLOADER_EXPORT_SYMBOLS_H diff --git a/src/extensions/inc/aduc/exports/extension_content_handler_export_symbols.h b/src/extensions/inc/aduc/exports/extension_content_handler_export_symbols.h new file mode 100644 index 000000000..33829875c --- /dev/null +++ b/src/extensions/inc/aduc/exports/extension_content_handler_export_symbols.h @@ -0,0 +1,26 @@ +#ifndef EXTENSION_CONTENT_HANDLER_EXPORT_SYMBOLS +#define EXTENSION_CONTENT_HANDLER_EXPORT_SYMBOLS + +#include // for GetContractInfo__EXPORT_SYMBOL + +// +// Update Content Handler Extension export V1 symbols. +// These are the V1.0 symbols that must be implemented by update content handlers (AKA step handlers). +// + +/** + * @brief GetContractInfo - Gets the extension contract info that the extension implements. + * @param[out] contractInfo The output parameter the contract info. + * @returns ADUC_Result The result of the call. + * @details ADUC_Result GetContractInfo(ADUC_ExtensionContractInfo* contractInfo); + */ +#define CONTENT_HANDLER__GetContractInfo__EXPORT_SYMBOL GetContractInfo__EXPORT_SYMBOL + +/** + * @brief Instantiates an Update Content Handler. + * @return ContentHandler* The created instance. + * @details ContentHandler* CreateUpdateContentHandlerExtension(ADUC_LOG_SEVERITY logLevel) + */ +#define CONTENT_HANDLER__CreateUpdateContentHandlerExtension__EXPORT_SYMBOL "CreateUpdateContentHandlerExtension" + +#endif // EXTENSION_CONTENT_HANDLER_EXPORT_SYMBOLS diff --git a/src/extensions/inc/aduc/exports/extension_download_handler_export_symbols.h b/src/extensions/inc/aduc/exports/extension_download_handler_export_symbols.h new file mode 100644 index 000000000..842353c32 --- /dev/null +++ b/src/extensions/inc/aduc/exports/extension_download_handler_export_symbols.h @@ -0,0 +1,62 @@ +/** + * @file extension_download_handler_export_symbols.h + * @brief The function export symbols for download handler extensions. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#ifndef EXTENSION_DOWNLOAD_HANDLER_EXPORT_SYMBOLS_H +#define EXTENSION_DOWNLOAD_HANDLER_EXPORT_SYMBOLS_H + +#include // for GetContractInfo__EXPORT_SYMBOL + +// +// The Download Handler Extension export V1 symbols. +// These are the V1 symbols that must be implemented by Download Handler Extensions. +// +#define DOWNLOAD_HANDLER__GetContractInfo__EXPORT_SYMBOL GetContractInfo__EXPORT_SYMBOL + +/** + * @brief One-time initialization for the download handler. + * + * @param logLevel The desired loglevel if logging is used. + * @details void Initialize(ADUC_LOG_SEVERITY logLevel) + */ +#define DOWNLOAD_HANDLER__Initialize__EXPORT_SYMBOL "Initialize" + +/** + * @brief Cleanup logic before library is unloaded. + * @details void Cleanup() + */ +#define DOWNLOAD_HANDLER__Cleanup__EXPORT_SYMBOL "Cleanup" + +/** + * @brief Processes the target update from FileEntity metadata at the given output filepath. + * For this download handler, each relatedFile in the FileEntity metadata represents a delta update, + * which is much smaller than the target update content. It attempts to download the delta update and + * produce the target update using the delta processor. If successful, it tells the agent to skip download; + * otherwise, it tells the agent that a full download is required. + * + * @param[in] workflowHandle The workflow handle. + * @param[in] fileEntity The FileEntity metadata of the update content and its related files. + * @param[in] targetUpdateFilePath The target update path to write the update content when returning ADUC_Result_Download_Handler_SuccessSkipDownload. + * @return ADUC_Result The result. + * Returning ADUC_Result_Download_Handler_SuccessSkipDownload ResultCode tells the agent to skip downloading the update content (since this download handler able to produce it at the targetUpdateFilePath). + * Returning ADUC_Result_Download_Handler_RequiredFullDownload ResultCode tells the agent to download the update content (this download handler did not produce it by other means). + * @details ADUC_Result ProcessUpdate(const ADUC_WorkflowHandle workflowHandle, const ADUC_FileEntity* fileEntity, const char* targetUpdateFilePath) + */ +#define DOWNLOAD_HANDLER__ProcessUpdate__EXPORT_SYMBOL "ProcessUpdate" + +/** + * @brief Called when the update workflow successfully completes. + * In the case of Delta download handler plugin, it moves all the payloads from sandbox to cache + * so that they will available as source updates for future delta updates. + * + * @param[in] workflowHandle The workflow handle. + * @return ADUC_Result The result. + * @details ADUC_Result OnUpdateWorkflowCompleted(const ADUC_WorkflowHandle workflowHandle) + */ +#define DOWNLOAD_HANDLER__OnUpdateWorkflowCompleted__EXPORT_SYMBOL "OnUpdateWorkflowCompleted" + +#endif // EXTENSION_DOWNLOAD_HANDLER_EXPORT_SYMBOLS_H diff --git a/src/extensions/inc/aduc/exports/extension_export_symbols.h b/src/extensions/inc/aduc/exports/extension_export_symbols.h new file mode 100644 index 000000000..cf9546a31 --- /dev/null +++ b/src/extensions/inc/aduc/exports/extension_export_symbols.h @@ -0,0 +1,17 @@ +/** + * @file extension_export_symbols.h + * @brief The common function export symbols used by specific extension export symbols headers. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#ifndef EXTENSION_EXPORT_SYMBOLS_H +#define EXTENSION_EXPORT_SYMBOLS_H + +#include "aduc/exports/extension_component_enumerator_export_symbols.h" +#include "aduc/exports/extension_content_downloader_export_symbols.h" +#include "aduc/exports/extension_content_handler_export_symbols.h" +#include "aduc/exports/extension_download_handler_export_symbols.h" + +#endif // EXTENSION_EXPORT_SYMBOLS_H diff --git a/src/extensions/shared_lib/CMakeLists.txt b/src/extensions/shared_lib/CMakeLists.txt new file mode 100644 index 000000000..e7bf83adf --- /dev/null +++ b/src/extensions/shared_lib/CMakeLists.txt @@ -0,0 +1,16 @@ +set (target_name shared_lib) + +include (agentRules) +compileasc99 () + +add_library (${target_name} STATIC "") +add_library (aduc::${target_name} ALIAS ${target_name}) + +# Turn -fPIC on, in order to use this library in a shared library. +set_property (TARGET ${target_name} PROPERTY POSITION_INDEPENDENT_CODE ON) + +target_include_directories (${target_name} PUBLIC inc ${ADUC_EXPORT_INCLUDES}) + +target_sources (${target_name} PRIVATE src/shared_lib.cpp inc/aduc/plugin_call_helper.hpp) + +target_link_libraries (${target_name} PRIVATE aduc::hash_utils ${CMAKE_DL_LIBS}) diff --git a/src/extensions/shared_lib/inc/aduc/plugin_call_helper.hpp b/src/extensions/shared_lib/inc/aduc/plugin_call_helper.hpp new file mode 100644 index 000000000..abb8d454d --- /dev/null +++ b/src/extensions/shared_lib/inc/aduc/plugin_call_helper.hpp @@ -0,0 +1,122 @@ +/** + * @file plugin_call_helper.hpp + * @brief header for helper functions when calling plugin export functions. + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#ifndef PLUGIN_CALL_HELPER +#define PLUGIN_CALL_HELPER + +#include "aduc/shared_lib.hpp" +#include + +template +struct IntToType +{ + enum + { + value = v + }; +}; + +/** + * @brief Calls the resolved symbol that does not return an ADUC_Result. + * + * @tparam FunctionSignature The function signature of the resolved function symbol. + * @tparam Arguments The arguments to pass to the function. Omit if none. + * @param resolvedSym The resolved symbol + * @param outResult unused. + * @param args The arguments to pass to the export function. + */ +template +void CallExportHandlerInternal(IntToType, void* resolvedSym, ADUC_Result* outResult, Arguments... args) +{ + UNREFERENCED_PARAMETER(outResult); + + auto fn = reinterpret_cast(resolvedSym); + fn(args...); +} + +/** + * @brief Calls the resolved symbol and outputs the resulting ADUC_Result. + * + * @tparam FunctionSignature The function signature of the resolved function symbol. + * @tparam Arguments The arguments to pass to the function. Omit if none. + * @param resolvedSym The resolved symbol + * @param[out] outResult The out result to set with the results of export function call. + * @param args The arguments to pass to the export function. + */ +template +void CallExportHandlerInternal(IntToType, void* resolvedSym, ADUC_Result* outResult, Arguments... args) +{ + auto fn = reinterpret_cast(resolvedSym); + *outResult = fn(args...); +} + +/** + * @brief Casts the resolved symbol to provided compile-time function signature and calls it wit hthe provided arguments. + * Sets the outResult with resulting ADUC_Result if ExportReturnsAducResult Boolean template parameter is set to true and + * outResult is not nullptr at runtime. + * + * @tparam FunctionSignature The function signature of the resolved function symbol. + * @tparam ExportReturnsAducResult Whether the function signature returns an + * ADUC_Result or not. If it does, then it will set the contents of outResult + * at runtime if outResult is not nullptr. + * @tparam Arguments The arguments to pass to the function. Omit if none. + * @param resolvedSymbol The resolved symbol. + * @param[out] outResult Optional. The resulting ADUC_Result from calling the export function. + * @param args The arguments to pass to the export function. + */ +template +void CallExportHandler(void* resolvedSymbol, ADUC_Result* outResult, Arguments... args) +{ + CallExportHandlerInternal(IntToType(), resolvedSymbol, outResult, args...); +} + +/** + * @brief Calls an export function public symbol on a shared library with + * compile-time verified FunctionSignature of the export function and + * compile-time switching based on if the export returns and ADUC_Result or not. + * + * @tparam FunctionSignature The function signature. + * @tparam ExportReturnsAducResult Whether the function signature returns an + * ADUC_Result or not. If it does, then it will set the contents of outResult + * at runtime. + * @tparam Arguments The arguments to pass to the function. Omit if none. + * @param exportSymbol The public export symbol to call on the shared library. + * @param lib The shared library. + * @param[out] outResult Optional. The ADUC_Result out to set if + * ExportReturnsAducResult is true. + * @param args The variadic arguments to pass on to the invoked export function. + */ +template +static void CallExport(const char* exportSymbol, const aduc::SharedLib& lib, ADUC_Result* outResult, Arguments... args) +{ + void* resolvedSymbol = nullptr; + + try + { + Log_Debug("Looking up symbol '%s'", exportSymbol); + resolvedSymbol = lib.GetSymbol(exportSymbol); + + if (resolvedSymbol == nullptr) + { + Log_Error("Could not resolve export symbol '%s'", exportSymbol); + } + else + { + CallExportHandler(resolvedSymbol, outResult, args...); + } + } + catch (const std::exception& ex) + { + Log_Error("Exception calling symbol '%s': %s", exportSymbol, ex.what()); + } + catch (...) + { + Log_Error("Non std exception when calling symbol '%s'.", exportSymbol); + } +} + +#endif // PLUGIN_CALL_HELPER diff --git a/src/extensions/shared_lib/inc/aduc/shared_lib.hpp b/src/extensions/shared_lib/inc/aduc/shared_lib.hpp new file mode 100644 index 000000000..92622269e --- /dev/null +++ b/src/extensions/shared_lib/inc/aduc/shared_lib.hpp @@ -0,0 +1,31 @@ +/** + * @file shared_lib.hpp + * @brief header for aduc::shared_lib class. + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#ifndef ADUC_SHARED_LIB +#define ADUC_SHARED_LIB + +#include +#include + +namespace aduc +{ +class SharedLib +{ +public: + SharedLib(const std::string& libPath); + ~SharedLib(); + + void EnsureSymbols(std::vector symbols) const; + void* GetSymbol(const std::string& symbol) const; + +private: + void* libHandle; +}; + +} // namespace aduc + +#endif // ADUC_SHARED_LIB diff --git a/src/extensions/shared_lib/src/shared_lib.cpp b/src/extensions/shared_lib/src/shared_lib.cpp new file mode 100644 index 000000000..5f68f666f --- /dev/null +++ b/src/extensions/shared_lib/src/shared_lib.cpp @@ -0,0 +1,58 @@ +/** + * @file shared_lib.cpp + * @brief Implementation for utility class that wraps loading a dynamic shared library and allows ensuring/getting symbols. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include "aduc/shared_lib.hpp" +#include "aduc/hash_utils.h" + +#include // std::for_each +#include // dlopen +#include // std::runtime_error + +namespace aduc +{ +SharedLib::SharedLib(const std::string& libPath) +{ + dlerror(); // clear + void* handle = dlopen(libPath.c_str(), RTLD_LAZY); + if (nullptr == handle) + { + char* error = dlerror(); + throw std::runtime_error(error != nullptr ? error : "dlopen"); + } + + libHandle = handle; +} + +SharedLib::~SharedLib() +{ + if (libHandle != nullptr) + { + dlclose(libHandle); + libHandle = nullptr; + } +} + +void SharedLib::EnsureSymbols(std::vector symbols) const +{ + std::for_each(symbols.begin(), symbols.end(), [this](const std::string& sym) { GetSymbol(sym); }); +} + +void* SharedLib::GetSymbol(const std::string& symbol) const +{ + dlerror(); // clear + void* sym = dlsym(libHandle, symbol.c_str()); + if (sym == nullptr) + { + char* error = dlerror(); + throw std::runtime_error(error != nullptr ? error : "dlsym"); + } + + return sym; +} + +} // namespace aduc diff --git a/src/extensions/step_handlers/CMakeLists.txt b/src/extensions/step_handlers/CMakeLists.txt new file mode 100644 index 000000000..e2b9bb5f3 --- /dev/null +++ b/src/extensions/step_handlers/CMakeLists.txt @@ -0,0 +1,12 @@ +add_subdirectory (apt_handler) +add_subdirectory (script_handler) +add_subdirectory (simulator_handler) +add_subdirectory (swupdate_handler) +add_subdirectory (swupdate_handler_v2) + +# +# Note: add more update content handler extensions here. +# +# e.g. +# add_subdirectory (my_handler) +# diff --git a/src/content_handlers/apt_handler/CMakeLists.txt b/src/extensions/step_handlers/apt_handler/CMakeLists.txt similarity index 73% rename from src/content_handlers/apt_handler/CMakeLists.txt rename to src/extensions/step_handlers/apt_handler/CMakeLists.txt index 415a58bd0..0b3875519 100644 --- a/src/content_handlers/apt_handler/CMakeLists.txt +++ b/src/extensions/step_handlers/apt_handler/CMakeLists.txt @@ -5,7 +5,7 @@ set (target_name microsoft_apt_1) set (SOURCE_ALL src/apt_handler.cpp src/apt_parser.cpp) # -# Create a shared library. +# Create a SHARED library. # add_library (${target_name} SHARED ${SOURCE_ALL}) @@ -19,8 +19,7 @@ target_include_directories ( PRIVATE ${PROJECT_SOURCE_DIR}/inc ${ADU_EXTENSION_INCLUDES} ${ADU_SHELL_INCLUDES} - ${ADUC_EXPORT_INCLUDES} - ${ADUC_TYPES_INCLUDES}) + ${ADUC_EXPORT_INCLUDES}) get_filename_component ( ADUC_INSTALLEDCRITERIA_FILE_PATH @@ -33,26 +32,17 @@ target_compile_definitions ( target_link_libraries ( ${target_name} - PUBLIC - aduc::content_handlers - aduc::workflow_data_utils - PRIVATE aduc::c_utils + PUBLIC aduc::logging + PRIVATE aduc::adu_types + aduc::contract_utils aduc::extension_manager - aduc::exception_utils aduc::installed_criteria_utils - aduc::logging aduc::process_utils - aduc::string_utils - aduc::system_utils + aduc::workflow_data_utils aduc::workflow_utils Parson::parson) -if ((NOT - ${ADUC_PLATFORM_LAYER} - STREQUAL - "simulator" - ) - AND ADUC_BUILD_UNIT_TESTS) +if (ADUC_BUILD_UNIT_TESTS) add_subdirectory (tests) diff --git a/src/content_handlers/apt_handler/README.md b/src/extensions/step_handlers/apt_handler/README.md similarity index 78% rename from src/content_handlers/apt_handler/README.md rename to src/extensions/step_handlers/apt_handler/README.md index 83e5d590a..891dbb086 100644 --- a/src/content_handlers/apt_handler/README.md +++ b/src/extensions/step_handlers/apt_handler/README.md @@ -25,21 +25,17 @@ Package-based updates are targeted updates that alter only a specific component ````json { - "name": "contoso-iot-edge", - "version": "1.0.0.0", - "packages": [ - { - "name" : "thermocontrol", - "version" : "1.0.1" - }, + "name":"tree-apt-manifest", + "version":"1.0", + "packages":[ { - "name" : "tempreport", - "version" : "2.0.0" + "name":"tree", + "version":"1.7.0-5" } ] } ```` -More example APT manifest files can be found [here](../../../docs/sample-artifacts/) - For more details, see [Device Update APT Manifest](https://docs.microsoft.com/en-us/azure/iot-hub-device-update/device-update-apt-manifest) + +More example APT manifest files can be found [here](../../../docs/tutorials) diff --git a/src/content_handlers/apt_handler/inc/aduc/apt_handler.hpp b/src/extensions/step_handlers/apt_handler/inc/aduc/apt_handler.hpp similarity index 84% rename from src/content_handlers/apt_handler/inc/aduc/apt_handler.hpp rename to src/extensions/step_handlers/apt_handler/inc/aduc/apt_handler.hpp index 49d362c8f..60fe95beb 100644 --- a/src/content_handlers/apt_handler/inc/aduc/apt_handler.hpp +++ b/src/extensions/step_handlers/apt_handler/inc/aduc/apt_handler.hpp @@ -11,7 +11,7 @@ #include "aduc/apt_handler.hpp" #include "aduc/content_handler.hpp" -#include "aduc/logging.h" +#include "aduc/logging.h" // ADUC_LOG_SEVERITY #include "aduc/result.h" #include "apt_parser.hpp" @@ -23,6 +23,14 @@ EXTERN_C_BEGIN */ ContentHandler* CreateUpdateContentHandlerExtension(ADUC_LOG_SEVERITY logLevel); +/** + * @brief Gets the extension contract info. + * + * @param[out] contractInfo The extension contract info. + * @return ADUC_Result The result. + */ +ADUC_Result GetContractInfo(ADUC_ExtensionContractInfo* contractInfo); + EXTERN_C_END /** @@ -43,12 +51,13 @@ class AptHandlerImpl : public ContentHandler ~AptHandlerImpl() override; ADUC_Result Download(const tagADUC_WorkflowData* workflowData) override; + ADUC_Result Backup(const tagADUC_WorkflowData* workflowData) override; ADUC_Result Install(const tagADUC_WorkflowData* workflowData) override; ADUC_Result Apply(const tagADUC_WorkflowData* workflowData) override; + ADUC_Result Restore(const tagADUC_WorkflowData* workflowData) override; ADUC_Result Cancel(const tagADUC_WorkflowData* workflowData) override; ADUC_Result IsInstalled(const tagADUC_WorkflowData* workflowData) override; - protected: AptHandlerImpl() { diff --git a/src/content_handlers/apt_handler/inc/aduc/apt_parser.hpp b/src/extensions/step_handlers/apt_handler/inc/aduc/apt_parser.hpp similarity index 99% rename from src/content_handlers/apt_handler/inc/aduc/apt_parser.hpp rename to src/extensions/step_handlers/apt_handler/inc/aduc/apt_parser.hpp index 6187a4d90..6e19e4544 100644 --- a/src/content_handlers/apt_handler/inc/aduc/apt_parser.hpp +++ b/src/extensions/step_handlers/apt_handler/inc/aduc/apt_parser.hpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include diff --git a/src/content_handlers/apt_handler/src/apt_handler.cpp b/src/extensions/step_handlers/apt_handler/src/apt_handler.cpp similarity index 67% rename from src/content_handlers/apt_handler/src/apt_handler.cpp rename to src/extensions/step_handlers/apt_handler/src/apt_handler.cpp index 1a95f0b96..d7075b1f5 100644 --- a/src/content_handlers/apt_handler/src/apt_handler.cpp +++ b/src/extensions/step_handlers/apt_handler/src/apt_handler.cpp @@ -19,6 +19,7 @@ #include "aduc/installed_criteria_utils.hpp" #include "aduc/logging.h" #include "aduc/process_utils.hpp" +#include "aduc/string_c_utils.h" #include "aduc/types/update_content.h" #include "aduc/workflow_data_utils.h" #include "aduc/workflow_utils.h" @@ -32,10 +33,21 @@ namespace adushconst = Adu::Shell::Const; +/* external linkage */ +extern ExtensionManager_Download_Options Default_ExtensionManager_Download_Options; + EXTERN_C_BEGIN +///////////////////////////////////////////////////////////////////////////// +// BEGIN Shared Library Export Functions +// +// These are the function symbols that the device update agent will +// lookup and call. +// + /** * @brief Instantiates an Update Content Handler for 'microsoft/apt:1' update type. + * @return ContentHandler* The created instance. */ ContentHandler* CreateUpdateContentHandlerExtension(ADUC_LOG_SEVERITY logLevel) { @@ -58,6 +70,23 @@ ContentHandler* CreateUpdateContentHandlerExtension(ADUC_LOG_SEVERITY logLevel) return nullptr; } +/** + * @brief Gets the extension contract info. + * + * @param[out] contractInfo The extension contract info. + * @return ADUC_Result The result. + */ +ADUC_Result GetContractInfo(ADUC_ExtensionContractInfo* contractInfo) +{ + contractInfo->majorVer = ADUC_V1_CONTRACT_MAJOR_VER; + contractInfo->minorVer = ADUC_V1_CONTRACT_MINOR_VER; + return ADUC_Result{ ADUC_GeneralResult_Success, 0 }; +} + +// +// END Shared Library Export Functions +///////////////////////////////////////////////////////////////////////////// + EXTERN_C_END /** @@ -105,36 +134,53 @@ ADUC_Result AptHandlerImpl::ParseContent(const std::string& aptManifestFile, std * * @return ADUC_Result The result of the download. */ -ADUC_Result AptHandlerImpl::Download(const tagADUC_WorkflowData* workflowData) +ADUC_Result AptHandlerImpl::Download(const ADUC_WorkflowData* workflowData) { - ADUC_Result result = { ADUC_Result_Failure }; + ADUC_Result result = { .ResultCode = ADUC_Result_Failure, .ExtendedResultCode = 0 }; std::stringstream aptManifestFilename; std::unique_ptr aptContent{ nullptr }; ADUC_WorkflowHandle handle = workflowData->WorkflowHandle; + int fileCount = 0; + char* installedCriteria = nullptr; + + if (workflow_is_cancel_requested(handle)) + { + return this->Cancel(workflowData); + } // For 'microsoft/apt:1', we're expecting 1 payload file. - int fileCount = workflow_get_update_files_count(handle); + fileCount = workflow_get_update_files_count(handle); if (fileCount != 1) { Log_Error("APT packages expecting one file. (%d)", fileCount); - return ADUC_Result{ ADUC_Result_Failure, ADUC_ERC_APT_HANDLER_PACKAGE_PREPARE_FAILURE_WRONG_FILECOUNT }; + return ADUC_Result{ .ResultCode = ADUC_Result_Failure, + .ExtendedResultCode = ADUC_ERC_APT_HANDLER_PACKAGE_PREPARE_FAILURE_WRONG_FILECOUNT }; } char* workFolder = workflow_get_workfolder(handle); - char* workflowId = workflow_get_id(handle); - ADUC_FileEntity* fileEntity = nullptr; if (!workflow_get_update_file(handle, 0, &fileEntity)) { - result = { ADUC_Result_Failure, ADUC_ERC_APT_HANDLER_GET_FILEENTITY_FAILURE }; + result = { .ResultCode = ADUC_Result_Failure, + .ExtendedResultCode = ADUC_ERC_APT_HANDLER_GET_FILEENTITY_FAILURE }; + goto done; + } + + installedCriteria = workflow_get_installed_criteria(handle); + if (IsNullOrEmpty(installedCriteria)) + { + workflow_set_result_details(handle, "Property 'installedCriteria' in handlerProperties is missing or empty."); + result = { .ResultCode = ADUC_Result_Failure, + .ExtendedResultCode = ADUC_ERC_APT_HANDLER_MISSING_INSTALLED_CRITERIA }; goto done; } aptManifestFilename << workFolder << "/" << fileEntity->TargetFilename; // Download the APT manifest file. - result = ExtensionManager::Download(fileEntity, workflowId, workFolder, DO_RETRY_TIMEOUT_DEFAULT, nullptr); + result = ExtensionManager::Download( + fileEntity, workflowData->WorkflowHandle, &Default_ExtensionManager_Download_Options, nullptr); workflow_free_file_entity(fileEntity); fileEntity = nullptr; @@ -207,7 +253,7 @@ ADUC_Result AptHandlerImpl::Download(const tagADUC_WorkflowData* workflowData) if (!aptOutput.empty()) { - Log_Info(aptOutput.c_str()); + Log_Info("\n\nadu-shell logs\n================\n\n%s", aptOutput.c_str()); } } catch (const std::exception& de) @@ -225,10 +271,10 @@ ADUC_Result AptHandlerImpl::Download(const tagADUC_WorkflowData* workflowData) } } - result = { ADUC_Result_Download_Success }; + result = { .ResultCode = ADUC_Result_Download_Success, .ExtendedResultCode = 0 }; done: - workflow_free_string(workflowId); + workflow_free_string(installedCriteria); workflow_free_string(workFolder); workflow_free_file_entity(fileEntity); @@ -240,21 +286,27 @@ ADUC_Result AptHandlerImpl::Download(const tagADUC_WorkflowData* workflowData) * * @return ADUC_Result The result of the install. */ -ADUC_Result AptHandlerImpl::Install(const tagADUC_WorkflowData* workflowData) +ADUC_Result AptHandlerImpl::Install(const ADUC_WorkflowData* workflowData) { std::string aptOutput; int aptExitCode = -1; - ADUC_Result result = { ADUC_Result_Download_Success }; + ADUC_Result result = { .ResultCode = ADUC_Result_Download_Success, .ExtendedResultCode = 0 }; ADUC_FileEntity* fileEntity = nullptr; ADUC_WorkflowHandle handle = workflowData->WorkflowHandle; char* workFolder = workflow_get_workfolder(handle); - char* workflowId = workflow_get_id(handle); std::stringstream aptManifestFilename; std::unique_ptr aptContent; + if (workflow_is_cancel_requested(handle)) + { + result = this->Cancel(workflowData); + goto done; + } + if (!workflow_get_update_file(handle, 0, &fileEntity)) { - result = { ADUC_Result_Failure, ADUC_ERC_APT_HANDLER_GET_FILEENTITY_FAILURE }; + result = { .ResultCode = ADUC_Result_Failure, + .ExtendedResultCode = ADUC_ERC_APT_HANDLER_GET_FILEENTITY_FAILURE }; goto done; } @@ -295,7 +347,7 @@ ADUC_Result AptHandlerImpl::Install(const tagADUC_WorkflowData* workflowData) if (!aptOutput.empty()) { - Log_Info(aptOutput.c_str()); + Log_Info("\n\nadu-shell logs\n================\n\n%s", aptOutput.c_str()); } } catch (const std::exception& de) @@ -312,10 +364,9 @@ ADUC_Result AptHandlerImpl::Install(const tagADUC_WorkflowData* workflowData) goto done; } - result = { ADUC_Result_Install_Success }; + result = { .ResultCode = ADUC_Result_Install_Success, .ExtendedResultCode = 0 }; done: - workflow_free_string(workflowId); workflow_free_string(workFolder); workflow_free_file_entity(fileEntity); return result; @@ -326,9 +377,9 @@ ADUC_Result AptHandlerImpl::Install(const tagADUC_WorkflowData* workflowData) * * @return ADUC_Result The result of the apply. */ -ADUC_Result AptHandlerImpl::Apply(const tagADUC_WorkflowData* workflowData) +ADUC_Result AptHandlerImpl::Apply(const ADUC_WorkflowData* workflowData) { - ADUC_Result result = { ADUC_Result_Apply_Success }; + ADUC_Result result = { .ResultCode = ADUC_Result_Apply_Success, .ExtendedResultCode = 0 }; ADUC_WorkflowHandle handle = workflowData->WorkflowHandle; char* installedCriteria = workflow_get_installed_criteria(handle); char* workFolder = workflow_get_workfolder(handle); @@ -336,6 +387,12 @@ ADUC_Result AptHandlerImpl::Apply(const tagADUC_WorkflowData* workflowData) std::stringstream aptManifestFilename; ADUC_FileEntity* entity = nullptr; + if (workflow_is_cancel_requested(handle)) + { + result = this->Cancel(workflowData); + goto done; + } + if (!PersistInstalledCriteria(ADUC_INSTALLEDCRITERIA_FILE_PATH, installedCriteria)) { result = { .ResultCode = ADUC_Result_Failure, @@ -363,12 +420,12 @@ ADUC_Result AptHandlerImpl::Apply(const tagADUC_WorkflowData* workflowData) { Log_Debug("The install task completed successfully, DU Agent restart is required for this update."); workflow_request_immediate_agent_restart(handle); - result = { .ResultCode = ADUC_Result_Apply_RequiredImmediateAgentRestart }; + result = { .ResultCode = ADUC_Result_Apply_RequiredImmediateAgentRestart, .ExtendedResultCode = 0 }; goto done; } else { - result = { .ResultCode = ADUC_Result_Apply_Success }; + result = { .ResultCode = ADUC_Result_Apply_Success, .ExtendedResultCode = 0 }; } Log_Info("Apply succeeded"); @@ -385,11 +442,29 @@ ADUC_Result AptHandlerImpl::Apply(const tagADUC_WorkflowData* workflowData) * * @return ADUC_Result The result of the cancel. */ -ADUC_Result AptHandlerImpl::Cancel(const tagADUC_WorkflowData* workflowData) +ADUC_Result AptHandlerImpl::Cancel(const ADUC_WorkflowData* workflowData) { - UNREFERENCED_PARAMETER(workflowData); - // For APT Update, cancel is not supported. - return ADUC_Result{ ADUC_Result_Cancel_UnableToCancel }; + ADUC_Result result = { .ResultCode = ADUC_Result_Cancel_Success, .ExtendedResultCode = 0 }; + ADUC_WorkflowHandle handle = workflowData->WorkflowHandle; + ADUC_WorkflowHandle stepWorkflowHandle = nullptr; + + const char* workflowId = workflow_peek_id(handle); + int workflowLevel = workflow_get_level(handle); + int workflowStep = workflow_get_step_index(handle); + + Log_Info( + "Requesting cancel operation (workflow id '%s', level %d, step %d).", workflowId, workflowLevel, workflowStep); + if (!workflow_request_cancel(handle)) + { + Log_Error( + "Cancellation request failed. (workflow id '%s', level %d, step %d)", + workflowId, + workflowLevel, + workflowStep); + result.ResultCode = ADUC_Result_Cancel_UnableToCancel; + } + + return result; } /** @@ -400,10 +475,45 @@ ADUC_Result AptHandlerImpl::Cancel(const tagADUC_WorkflowData* workflowData) * * @return ADUC_Result */ -ADUC_Result AptHandlerImpl::IsInstalled(const tagADUC_WorkflowData* workflowData) +ADUC_Result AptHandlerImpl::IsInstalled(const ADUC_WorkflowData* workflowData) { + ADUC_Result result = { .ResultCode = ADUC_Result_IsInstalled_NotInstalled, .ExtendedResultCode = 0 }; char* installedCriteria = ADUC_WorkflowData_GetInstalledCriteria(workflowData); - ADUC_Result result = GetIsInstalled(ADUC_INSTALLEDCRITERIA_FILE_PATH, installedCriteria); + if (installedCriteria == nullptr) + { + Log_Error("installedCriteria is null."); + goto done; + } + + result = GetIsInstalled(ADUC_INSTALLEDCRITERIA_FILE_PATH, installedCriteria); + +done: workflow_free_string(installedCriteria); return result; } + +/** + * @brief Backup implementation for APT Handler. + * + * @return ADUC_Result The result of the backup. + */ +ADUC_Result AptHandlerImpl::Backup(const tagADUC_WorkflowData* workflowData) +{ + UNREFERENCED_PARAMETER(workflowData); + ADUC_Result result = { .ResultCode = ADUC_Result_Backup_Success_Unsupported, .ExtendedResultCode = 0 }; + Log_Info("Apt update backup & restore is not supported. (no-op)"); + return result; +} + +/** + * @brief Restore implementation for APT Handler. + * + * @return ADUC_Result The result of the restore. + */ +ADUC_Result AptHandlerImpl::Restore(const tagADUC_WorkflowData* workflowData) +{ + UNREFERENCED_PARAMETER(workflowData); + ADUC_Result result = { .ResultCode = ADUC_Result_Restore_Success_Unsupported, .ExtendedResultCode = 0 }; + Log_Info("Apt update backup & restore is not supported. (no-op)"); + return result; +} diff --git a/src/content_handlers/apt_handler/src/apt_parser.cpp b/src/extensions/step_handlers/apt_handler/src/apt_parser.cpp similarity index 100% rename from src/content_handlers/apt_handler/src/apt_parser.cpp rename to src/extensions/step_handlers/apt_handler/src/apt_parser.cpp diff --git a/src/content_handlers/apt_handler/tests/CMakeLists.txt b/src/extensions/step_handlers/apt_handler/tests/CMakeLists.txt similarity index 93% rename from src/content_handlers/apt_handler/tests/CMakeLists.txt rename to src/extensions/step_handlers/apt_handler/tests/CMakeLists.txt index 1dbe96613..2a015a3bd 100644 --- a/src/content_handlers/apt_handler/tests/CMakeLists.txt +++ b/src/extensions/step_handlers/apt_handler/tests/CMakeLists.txt @@ -39,7 +39,9 @@ target_compile_definitions ( target_link_libraries ( ${PROJECT_NAME} - PRIVATE aduc::exception_utils + PRIVATE aduc::agent_orchestration + aduc::contract_utils + aduc::exception_utils aduc::extension_manager aduc::installed_criteria_utils aduc::process_utils diff --git a/src/content_handlers/apt_handler/tests/apt_parser_ut.cpp b/src/extensions/step_handlers/apt_handler/tests/apt_parser_ut.cpp similarity index 100% rename from src/content_handlers/apt_handler/tests/apt_parser_ut.cpp rename to src/extensions/step_handlers/apt_handler/tests/apt_parser_ut.cpp diff --git a/src/content_handlers/apt_handler/tests/main.cpp b/src/extensions/step_handlers/apt_handler/tests/main.cpp similarity index 100% rename from src/content_handlers/apt_handler/tests/main.cpp rename to src/extensions/step_handlers/apt_handler/tests/main.cpp diff --git a/src/content_handlers/apt_handler/tests/sample_apt_manifest.json b/src/extensions/step_handlers/apt_handler/tests/sample_apt_manifest.json similarity index 100% rename from src/content_handlers/apt_handler/tests/sample_apt_manifest.json rename to src/extensions/step_handlers/apt_handler/tests/sample_apt_manifest.json diff --git a/src/content_handlers/script_handler/CMakeLists.txt b/src/extensions/step_handlers/script_handler/CMakeLists.txt similarity index 51% rename from src/content_handlers/script_handler/CMakeLists.txt rename to src/extensions/step_handlers/script_handler/CMakeLists.txt index cc104e805..3d66f8817 100644 --- a/src/content_handlers/script_handler/CMakeLists.txt +++ b/src/extensions/step_handlers/script_handler/CMakeLists.txt @@ -2,26 +2,19 @@ cmake_minimum_required (VERSION 3.5) set (target_name microsoft_script_1) -set (SOURCE_ALL src/script_handler.cpp) - -set (SCRIPT_HANDLER_INCLUDES ${CMAKE_CURRENT_SOURCE_DIR}/inc) - -add_library (${target_name} SHARED ${SOURCE_ALL}) +add_library (${target_name} MODULE "") add_library (aduc::${target_name} ALIAS ${target_name}) -target_include_directories ( - ${target_name} - PUBLIC inc - PRIVATE ${PROJECT_SOURCE_DIR}/inc - ${ADUC_TYPES_INCLUDES} - ${ADUC_EXPORT_INCLUDES} - ${ADU_SHELL_INCLUDES} - ${ADU_EXTENSION_INCLUDES}) +target_sources (${target_name} PRIVATE src/script_handler.cpp) + +target_include_directories (${target_name} PUBLIC inc ${ADUC_EXPORT_INCLUDES} ${ADU_SHELL_INCLUDES}) target_link_libraries ( ${target_name} - PRIVATE aduc::c_utils + PRIVATE aduc::adu_core_export_helpers + aduc::c_utils + aduc::contract_utils aduc::exception_utils aduc::extension_utils aduc::extension_manager @@ -30,8 +23,6 @@ target_link_libraries ( aduc::string_utils aduc::system_utils aduc::workflow_data_utils - aduc::workflow_utils - -zdefs - ) + aduc::workflow_utils) install (TARGETS ${target_name} LIBRARY DESTINATION ${ADUC_EXTENSIONS_INSTALL_FOLDER}) diff --git a/src/content_handlers/script_handler/README.md b/src/extensions/step_handlers/script_handler/README.md similarity index 93% rename from src/content_handlers/script_handler/README.md rename to src/extensions/step_handlers/script_handler/README.md index 25a70e591..c3e374470 100644 --- a/src/content_handlers/script_handler/README.md +++ b/src/extensions/step_handlers/script_handler/README.md @@ -4,9 +4,9 @@ Script Handler Extension (or, in short, **Script Handler**) handler can be used ## Overview -For some update scenarios, you may want to run additional set of commands before, during, or after, installing a software update on your device. This can be achieved using an **Inline Step** with `microsoft/script:1` handler type. +For some update scenarios, you may want to run additional set of commands before, during, or after, installing a software update on your device. This can be achieved using an **Inline Step** with `microsoft/script:1` handler type. -By default, Device Update Agent workflow invokes **Script Handler** to process `microsoft/script:/1` steps. +By default, Device Update Agent workflow invokes **Script Handler** to process `microsoft/script:/1` steps. > For more information about **Inline Step**, see [Update Manifest V4 Schema](../../../docs/agent-reference/update-manifest-v4-schema.md) @@ -123,11 +123,11 @@ The current implementation of the Script Handler requires following values in th ## Specify A Script Filename -From an [example steps data](#example-update-manifest-json-data) above, notice the `handlerProperties.scriptFileName` property values, which are the same as file names in `files` array. +From an [example steps data](#example-update-manifest-json-data) above, notice the `handlerProperties.scriptFileName` property values, which are the same as file names in `files` array. At runtime, the Script Handler will download every files listed in `files` array, into a `workfolder` (e.g., **/var/lib/adu/downloads/workflow_id_1234567890/**). ->**Note** | Learn more about how the Script Handler passes `workfolder` path to the script in [Default Workflow Options and Arguments](#default-workflow-options-and-arguments) section below. +>**Note** | Learn more about how the Script Handler passes `workfolder` path to the script in [Default Workflow Options and Arguments](#default-workflow-options-and-arguments) section below. Later on, the Script Handler will invoke the specified script file, in a forked-process, to perform various tasks, such as `download` (additional file), `install` an update, `apply` the installed update, `cancel` the update flow if needed, or perform `is-installed` evaluation. @@ -156,9 +156,9 @@ For example (see **arguments** value below): ### Arguments Value Format -The `arguments` value contains a space-delimited `options` and `arguments` that will be processed by the Script Handler. These `options` may be interpreted by the Script Handler, then, replaced by, or populated with runtime variables. +The `arguments` value contains a space-delimited `options` and `arguments` that will be processed by the Script Handler. These `options` may be interpreted by the Script Handler, then, replaced by, or populated with runtime variables. -Additionally, the Script Handler will append some default runtime workflow related values, such as, `workfolder`, `result-file`, `installed-criteria`, etc. to the options and arguments list. +Additionally, the Script Handler will append some default runtime workflow related values, such as, `workfolder`, `result-file`, `installed-criteria`, etc. to the options and arguments list. You can specify any options and arguments that relevant to the script in this 'arguments' string, as long as they don't conflict with [reserved options](#reserved-options) or [default workflow runtime options and arguments](#default-workflow-options-and-arguments) listed below. @@ -183,7 +183,7 @@ The following options and arguments are automatically pass to the script. |--action-is-install| none |Script Handler will append this option to the `arguments` list to indicate that the script is invoked as part of the `isInstalled` inquiry.
As part of the Agent-Orchestrated workflow, sometimes, Device Update Agent will invoke the Script Handler's `IsInstalled` function to determine wither the current Step has been **installed**.
For Step that does not install any software, **isinstalled** is equivalent to **applied**. E.g., "Is the step has been applied on the device or components?"| | --work-folder | <"work_folder_path">| A fully qualified path to `workfolder` (a.k.a. `sandbox folder`). This is a folder used for storing downloaded file, workflow result file, and any other temp files.

Script Handler will append this option to the arguments list when invoking the script. | | --result-file | <"result_file_path"> |A fully qualified path to the workflow task result. The result file contains a ADUC_Result data in a JSON. The Script Handler will read ADUC_Result data in this file and continue or abort the update workflow based on the `resultCode`. If the result file does not exist, or can not be opened, the update workflow will be aborted.

Script Handler will append this option to the arguments list when invoking the script.| -| --installed-criteria | <"installed_criteria_string">| Pass-through value to the script.| +| --installed-criteria | <"installed_criteria_string">| Pass-through value to the script.| #### Options for Components Runtime Variables @@ -227,13 +227,13 @@ The following instruction step contains various reserved options that would be r }, ``` -Following are the mapping between the `step.handlersProperties.arguments` value and the actual arguments that **StepHandler** pass to the script at runtime. +Following are the mapping between the `step.handlersProperties.arguments` value and the actual arguments that **StepHandler** pass to the script at runtime. > Note: for this example, let's assumed that the Script Handler is processing an update workflow (`install`) for following component information provided by a registered `Component Enumerator` extension ```json { "components": [ - + ..., { @@ -254,7 +254,7 @@ Following are the mapping between the `step.handlersProperties.arguments` value ... ] } -``` +``` | handleProperties.arguments
(option) | handleProperties.arguments
(arguments)| Value pass to the script at runtime
(*after processed by Script Handler*) | Note |----|----|----|---| @@ -273,12 +273,12 @@ Following are the mapping between the `step.handlersProperties.arguments` value From the post-processed values in the table above, this is what the script invoking command looks like: ```sh -contoso-motor-installscript.sh - --firmware-file motor-firmware-1.2.json - --component-name "left-motor" --component-group "motors" - --component-prop path "/usr/local/contoso-devices/vacuum-1/motors/contoso-motor-serial-00000" - --action-install - --work-folder "/var/lib/adu/downloads/workflow_id_1234567890/" +contoso-motor-installscript.sh + --firmware-file motor-firmware-1.2.json + --component-name "left-motor" --component-group "motors" + --component-prop path "/usr/local/contoso-devices/vacuum-1/motors/contoso-motor-serial-00000" + --action-install + --work-folder "/var/lib/adu/downloads/workflow_id_1234567890/" --result-file "/var/lib/adu/downloads/workflow_id_1234567890/aduc_result.json" --installed-criteria": "1.2" @@ -286,7 +286,7 @@ contoso-motor-installscript.sh ### Example Script -To get started, you can take a look at the [example-installscript.sh](./examples/example-installscript.sh) to see how the script can be implemented. +To get started, you can take a look at the [example-installscript.sh](./examples/example-installscript.sh) to see how the script can be implemented. > IMPORTANT: Please note that this is for demonstration purposes only. The example script is provided AS-IS. You should not use it for your production project. @@ -294,6 +294,30 @@ To get started, you can take a look at the [example-installscript.sh](./examples The script, must write an ADUC_Result data into a designated result file (specified by --result-file option) +> IMPORTANT: Make sure to modify the default behavior of writing result file. If agent detects ResultCode and ExtendedResultCode of 0, then for discoverability, it will replace the ExtendedResultCode with ADUC_ERC_SCRIPT_HANDLER_INSTALL_FAILURE_SCRIPT_RESULT_EXTENDEDRESULTCODE_ZERO that has numeric code of 81054976 (decimal) and 0x30500206 (hexadecimal). + +Example contents of result file for Success: + +```json +{ + "resultCode": 1, + "extendedResultCode": 0, + "resultDetails": "" +} +``` + +Example contents of result file for Failure: + +```json +{ + "resultCode": 0, + "extendedResultCode": , + "resultDetails": "some optional description" +} +``` + +#### Example + The following is excerpt from an example script: ```sh @@ -352,3 +376,4 @@ InstallUpdate() { ... ``` + diff --git a/src/content_handlers/script_handler/examples/example-installscript.sh b/src/extensions/step_handlers/script_handler/examples/example-installscript.sh similarity index 95% rename from src/content_handlers/script_handler/examples/example-installscript.sh rename to src/extensions/step_handlers/script_handler/examples/example-installscript.sh index a29013768..4f81e3832 100644 --- a/src/content_handlers/script_handler/examples/example-installscript.sh +++ b/src/extensions/step_handlers/script_handler/examples/example-installscript.sh @@ -10,9 +10,9 @@ ret_val=0 # Ensure we don't end the user's terminal session if invoked from source ("."). if [[ $0 != "${BASH_SOURCE[0]}" ]]; then - ret=return + ret='return' else - ret=exit + ret='exit' fi # Output formatting. @@ -102,52 +102,51 @@ PARAMS= # _timestamp= -update_timestamp() -{ +update_timestamp() { # See https://man7.org/linux/man-pages/man1/date.1.html _timestamp="$(date +'%Y/%m/%d:%H%M%S')" } -log_debug(){ - if [ $log_level -gt 0 ]; then +log_debug() { + if [ $log_level -gt 0 ]; then return fi log "$log_debug_pref" "$@" } -log_info(){ - if [ $log_level -gt 1 ]; then +log_info() { + if [ $log_level -gt 1 ]; then return fi log "$log_info_pref" "$@" } -log_warn(){ - if [ $log_level -gt 2 ]; then +log_warn() { + if [ $log_level -gt 2 ]; then return fi log "$log_warn_pref" "$@" } -log_error(){ - if [ $log_level -gt 3 ]; then +log_error() { + if [ $log_level -gt 3 ]; then return fi log "$log_error_pref" "$@" } -log(){ +log() { update_timestamp - if [ -z $log_file ]; then + if [ -z "$log_file" ]; then echo -e "[$_timestamp]" "$@" >&1 else - echo "[$_timestamp]" "$@" >> $log_file + echo "[$_timestamp]" "$@" >> "$log_file" fi } -output(){ +output() { update_timestamp - if [ -z $output_file ]; then + if [ -z "$output_file" ]; then echo "[$_timestamp]" "$@" >&1 else echo "[$_timestamp]" "$@" >> "$output_file" @@ -157,9 +156,9 @@ output(){ # # Write result json string to result file. # -result(){ +result() { # NOTE: don't insert timestamp in result file. - if [ -z $result_file ]; then + if [ -z "$result_file" ]; then echo "$@" >&1 else echo "$@" > "$result_file" @@ -197,7 +196,7 @@ print_help() { echo "--action-apply Perform 'apply' action." echo "--action-cancel Perform 'cancel' action." echo "" - echo "--install-error-policy Indicates how to proceed when an error occurs. Default \"abort\"" + echo '--install-error-policy Indicates how to proceed when an error occurs. Default "abort"' echo " options: abort, continue" echo "--install-reboot-policy Indicates whether a device reboot is required, after install completed successfully." echo " Optios:" @@ -425,7 +424,7 @@ while [[ $1 != "" ]]; do error "--firmware-file parameter is mandatory." $ret 1 fi - firmware_file="$1"; + firmware_file="$1" echo "firmware file: $firmware_file" shift ;; @@ -436,7 +435,7 @@ while [[ $1 != "" ]]; do error "--work-folder parameter is mandatory." $ret 1 fi - workfolder="$1"; + workfolder="$1" echo "Workfolder: $workfolder" shift ;; @@ -452,7 +451,7 @@ while [[ $1 != "" ]]; do error "--out-file parameter is mandatory." $ret 1 fi - output_file="$1"; + output_file="$1" # #Create output file path. @@ -473,7 +472,7 @@ while [[ $1 != "" ]]; do error "--result-file parameter is mandatory." $ret 1 fi - result_file="$1"; + result_file="$1" # #Create result file path. # @@ -492,7 +491,7 @@ while [[ $1 != "" ]]; do error "--log-file parameter is mandatory." $ret 1 fi - log_file="$1"; + log_file="$1" shift ;; --log-level) @@ -563,7 +562,7 @@ done # ADUC_Result_IsInstalled_Installed = 900, /**< Succeeded and content is installed. */ # ADUC_Result_IsInstalled_NotInstalled = 901, /**< Succeeded and content is not installed */ # -IsInstalled(){ +IsInstalled() { resultCode=0 extendedResultCode=0 resultDetails="" @@ -587,7 +586,7 @@ IsInstalled(){ output "Result:" "$aduc_result_json" # Write ADUC_Result to result file. - result "$aduc_result_json" + result "$aduc_result_json" $ret $ret_val @@ -620,7 +619,7 @@ DownloadUpdateArtifacts() { output "Result:" "$aduc_result_json" # Write ADUC_Result to result file. - result "$aduc_result_json" + result "$aduc_result_json" $ret $ret_val } @@ -657,7 +656,7 @@ InstallUpdate() { output "Result:" "$aduc_result_json" # Write ADUC_Result to result file. - result "$aduc_result_json" + result "$aduc_result_json" $ret $ret_val } @@ -688,7 +687,7 @@ ApplyUpdate() { output "Result:" "$aduc_result_json" # Write ADUC_Result to result file. - result "$aduc_result_json" + result "$aduc_result_json" $ret $ret_val } @@ -708,13 +707,13 @@ SimulatePreInstallSuccess() { output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } SimulatePostInstallSuccess() { -output "Simulating post-instll step success." + output "Simulating post-instll step success." ret_val=0 # ADUC_Result_Apply_Success = 700 resultCode=700 @@ -728,12 +727,12 @@ output "Simulating post-instll step success." output "Result:" "$mock_result" # Write ADUC_Result to result file. - result "$mock_result" + result "$mock_result" $ret $ret_val } -CancelUpdate(){ +CancelUpdate() { ret_val=0 $ret $ret_val } @@ -777,4 +776,3 @@ if [ -n "$do_cancel_action" ]; then fi $ret $ret_val - diff --git a/src/content_handlers/script_handler/images/script-handler-overview.svg b/src/extensions/step_handlers/script_handler/images/script-handler-overview.svg similarity index 100% rename from src/content_handlers/script_handler/images/script-handler-overview.svg rename to src/extensions/step_handlers/script_handler/images/script-handler-overview.svg diff --git a/src/content_handlers/script_handler/inc/aduc/script_handler.hpp b/src/extensions/step_handlers/script_handler/inc/aduc/script_handler.hpp similarity index 89% rename from src/content_handlers/script_handler/inc/aduc/script_handler.hpp rename to src/extensions/step_handlers/script_handler/inc/aduc/script_handler.hpp index de60b1743..03b5177b0 100644 --- a/src/content_handlers/script_handler/inc/aduc/script_handler.hpp +++ b/src/extensions/step_handlers/script_handler/inc/aduc/script_handler.hpp @@ -33,13 +33,15 @@ class ScriptHandlerImpl : public ContentHandler ~ScriptHandlerImpl() override = default; ADUC_Result Download(const tagADUC_WorkflowData* workflowData) override; + ADUC_Result Backup(const tagADUC_WorkflowData* workflowData) override; ADUC_Result Install(const tagADUC_WorkflowData* workflowData) override; ADUC_Result Apply(const tagADUC_WorkflowData* workflowData) override; + ADUC_Result Restore(const tagADUC_WorkflowData* workflowData) override; ADUC_Result Cancel(const tagADUC_WorkflowData* workflowData) override; ADUC_Result IsInstalled(const tagADUC_WorkflowData* workflowData) override; static ADUC_Result PrepareScriptArguments( - const ADUC_WorkflowHandle workflowHandle, + ADUC_WorkflowHandle workflowHandle, std::string resultFilePath, std::string workFolder, std::string& scriptFilePath, diff --git a/src/content_handlers/script_handler/src/script_handler.cpp b/src/extensions/step_handlers/script_handler/src/script_handler.cpp similarity index 85% rename from src/content_handlers/script_handler/src/script_handler.cpp rename to src/extensions/step_handlers/script_handler/src/script_handler.cpp index f99ec8998..8322c77ac 100644 --- a/src/content_handlers/script_handler/src/script_handler.cpp +++ b/src/extensions/step_handlers/script_handler/src/script_handler.cpp @@ -6,26 +6,16 @@ * Licensed under the MIT License. */ #include "aduc/script_handler.hpp" - -#include "aduc/adu_core_exports.h" #include "aduc/extension_manager.hpp" #include "aduc/logging.h" -#include "aduc/process_utils.hpp" +#include "aduc/process_utils.hpp" // ADUC_LaunchChildProcess #include "aduc/string_c_utils.h" // IsNullOrEmpty -#include "aduc/string_utils.hpp" -#include "aduc/system_utils.h" -#include "aduc/types/workflow.h" -#include "aduc/workflow_data_utils.h" -#include "aduc/workflow_utils.h" +#include "aduc/string_utils.hpp" // ADUC::StringUtils::Split +#include "aduc/system_utils.h" // ADUC_SystemUtils_MkSandboxDirRecursive +#include "aduc/types/adu_core.h" // ADUC_Result_* +#include "aduc/workflow_data_utils.h" // ADUC_WorkflowData_GetWorkFolder +#include "aduc/workflow_utils.h" // workflow_* #include "adushell_const.hpp" - -#include -#include // for mallocAndStrcpy_s -#include // STRING_* -#include -#include -#include -#include #include #include #include @@ -36,8 +26,16 @@ namespace adushconst = Adu::Shell::Const; EXTERN_C_BEGIN +///////////////////////////////////////////////////////////////////////////// +// BEGIN Shared Library Export Functions +// +// These are the function symbols that the device update agent will +// lookup and call. +// + /** * @brief Instantiates an Update Content Handler for 'microsoft/script:1' update type. + * @return ContentHandler* The created instance. */ ContentHandler* CreateUpdateContentHandlerExtension(ADUC_LOG_SEVERITY logLevel) { @@ -59,6 +57,23 @@ ContentHandler* CreateUpdateContentHandlerExtension(ADUC_LOG_SEVERITY logLevel) return nullptr; } +/** + * @brief Gets the extension contract info. + * + * @param[out] contractInfo The extension contract info. + * @return ADUC_Result The result. + */ +ADUC_Result GetContractInfo(ADUC_ExtensionContractInfo* contractInfo) +{ + contractInfo->majorVer = ADUC_V1_CONTRACT_MAJOR_VER; + contractInfo->minorVer = ADUC_V1_CONTRACT_MINOR_VER; + return ADUC_Result{ ADUC_GeneralResult_Success, 0 }; +} + +// +// END Shared Library Export Functions +///////////////////////////////////////////////////////////////////////////// + EXTERN_C_END // Forward declarations. @@ -129,7 +144,11 @@ static ADUC_Result Script_Handler_DownloadPrimaryScriptFile(ADUC_WorkflowHandle try { - result = ExtensionManager::Download(entity, workflowId, workFolder, DO_RETRY_TIMEOUT_DEFAULT, nullptr); + ExtensionManager_Download_Options downloadOptions = { + .retryTimeout = DO_RETRY_TIMEOUT_DEFAULT, + }; + + result = ExtensionManager::Download(entity, handle, &downloadOptions, nullptr); } catch (...) { @@ -202,7 +221,11 @@ ADUC_Result ScriptHandlerImpl::Download(const tagADUC_WorkflowData* workflowData try { - result = ExtensionManager::Download(entity, workflowId, workFolder, DO_RETRY_TIMEOUT_DEFAULT, nullptr); + ExtensionManager_Download_Options downloadOptions = { + .retryTimeout = DO_RETRY_TIMEOUT_DEFAULT, + }; + + result = ExtensionManager::Download(entity, workflowHandle, &downloadOptions, nullptr); } catch (...) { @@ -496,7 +519,6 @@ static ADUC_Result ScriptHandler_PerformAction(const std::string& action, const { Log_Info("Action (%s) begin", action.c_str()); ADUC_Result result = { ADUC_GeneralResult_Failure }; - STRING_HANDLE resultDetails; std::string scriptFilePath; std::vector args; @@ -531,7 +553,9 @@ static ADUC_Result ScriptHandler_PerformAction(const std::string& action, const // If any install-item reported that the update is already installed on the // selected component, we will skip the 'apply' phase, and then skip the // remaining install-item(s). - if (result.ResultCode == ADUC_Result_Install_Skipped_UpdateAlreadyInstalled) + // Also, don't continue if WorkflowHandle is NULL in the ADUInterface_Connected->HandleStartupWorkflowData flow. + if (result.ResultCode == ADUC_Result_Install_Skipped_UpdateAlreadyInstalled + || workflowData->WorkflowHandle == nullptr) { goto done; } @@ -590,6 +614,12 @@ static ADUC_Result ScriptHandler_PerformAction(const std::string& action, const workflow_set_result_details( workflowData->WorkflowHandle, json_object_get_string(actionResultObject, "resultDetails")); + if (IsAducResultCodeFailure(result.ResultCode) && result.ExtendedResultCode == 0) + { + Log_Warn("Script result had non-actionable ExtendedResultCode of 0."); + result.ExtendedResultCode = ADUC_ERC_SCRIPT_HANDLER_INSTALL_FAILURE_SCRIPT_RESULT_EXTENDEDRESULTCODE_ZERO; + } + Log_Info( "Action (%s) done - returning rc:%d, erc:0x%X, rd:%s", action.c_str(), @@ -639,6 +669,11 @@ ADUC_Result ScriptHandlerImpl::PerformAction(const std::string& action, const ta return ScriptHandler_PerformAction(action, workflowData); } +static ADUC_Result DoCancel(const tagADUC_WorkflowData* workflowData) +{ + return ScriptHandler_PerformAction("--action-cancel", workflowData); +} + /** * @brief Performs 'Apply' task. * @return ADUC_Result The result return from script execution. @@ -655,7 +690,26 @@ ADUC_Result ScriptHandlerImpl::Apply(const tagADUC_WorkflowData* workflowData) */ ADUC_Result ScriptHandlerImpl::Cancel(const tagADUC_WorkflowData* workflowData) { - ADUC_Result result = PerformAction("--action-cancel", workflowData); + ADUC_Result result = { .ResultCode = ADUC_Result_Cancel_Success, .ExtendedResultCode = 0 }; + ADUC_WorkflowHandle handle = workflowData->WorkflowHandle; + ADUC_WorkflowHandle stepWorkflowHandle = nullptr; + + const char* workflowId = workflow_peek_id(handle); + int workflowLevel = workflow_get_level(handle); + int workflowStep = workflow_get_step_index(handle); + + Log_Info( + "Requesting cancel operation (workflow id '%s', level %d, step %d).", workflowId, workflowLevel, workflowStep); + if (!workflow_request_cancel(handle)) + { + Log_Error( + "Cancellation request failed. (workflow id '%s', level %d, step %d)", + workflowId, + workflowLevel, + workflowStep); + result.ResultCode = ADUC_Result_Cancel_UnableToCancel; + } + return result; } @@ -672,3 +726,25 @@ ADUC_Result ScriptHandlerImpl::IsInstalled(const tagADUC_WorkflowData* workflowD } return result; } + +/** + * @brief Performs 'Backup' task. + * @return ADUC_Result The result (always success) + */ +ADUC_Result ScriptHandlerImpl::Backup(const tagADUC_WorkflowData* workflowData) +{ + ADUC_Result result = { ADUC_Result_Backup_Success_Unsupported }; + Log_Info("Script handler backup & restore is not supported. (no-op)"); + return result; +} + +/** + * @brief Performs 'Restore' task. + * @return ADUC_Result The result (always success) + */ +ADUC_Result ScriptHandlerImpl::Restore(const tagADUC_WorkflowData* workflowData) +{ + ADUC_Result result = { ADUC_Result_Restore_Success_Unsupported }; + Log_Info("Script handler backup & restore is not supported. (no-op)"); + return result; +} diff --git a/src/content_handlers/script_handler/tests/CMakeLists.txt b/src/extensions/step_handlers/script_handler/tests/CMakeLists.txt similarity index 100% rename from src/content_handlers/script_handler/tests/CMakeLists.txt rename to src/extensions/step_handlers/script_handler/tests/CMakeLists.txt diff --git a/src/content_handlers/script_handler/tests/main.cpp b/src/extensions/step_handlers/script_handler/tests/main.cpp similarity index 100% rename from src/content_handlers/script_handler/tests/main.cpp rename to src/extensions/step_handlers/script_handler/tests/main.cpp diff --git a/src/content_handlers/script_handler/tests/script_handler_ut.cpp b/src/extensions/step_handlers/script_handler/tests/script_handler_ut.cpp similarity index 100% rename from src/content_handlers/script_handler/tests/script_handler_ut.cpp rename to src/extensions/step_handlers/script_handler/tests/script_handler_ut.cpp diff --git a/src/content_handlers/simulator_handler/CMakeLists.txt b/src/extensions/step_handlers/simulator_handler/CMakeLists.txt similarity index 81% rename from src/content_handlers/simulator_handler/CMakeLists.txt rename to src/extensions/step_handlers/simulator_handler/CMakeLists.txt index 176a0480e..7c1bfbaf9 100644 --- a/src/content_handlers/simulator_handler/CMakeLists.txt +++ b/src/extensions/step_handlers/simulator_handler/CMakeLists.txt @@ -17,10 +17,10 @@ target_include_directories ( ${target_name} PUBLIC inc PRIVATE ${PROJECT_SOURCE_DIR}/inc - ${ADUC_TYPES_INCLUDES} - ${ADUC_EXPORT_INCLUDES} + ${ADU_EXTENSION_INCLUDES} ${ADU_SHELL_INCLUDES} - ${ADU_EXTENSION_INCLUDES}) + ${ADUC_EXPORT_INCLUDES} + ${ADUC_TYPES_INCLUDES}) get_filename_component ( ADUC_INSTALLEDCRITERIA_FILE_PATH @@ -35,13 +35,11 @@ target_compile_definitions ( target_link_libraries ( ${target_name} - PUBLIC aduc::content_handlers - PRIVATE aduc::logging - aduc::agent_workflow + PRIVATE aduc::agent_workflow + aduc::contract_utils + aduc::logging aduc::workflow_utils - Parson::parson - -zdefs - ) + Parson::parson) install (TARGETS ${target_name} LIBRARY DESTINATION ${ADUC_EXTENSIONS_INSTALL_FOLDER}) diff --git a/src/content_handlers/simulator_handler/README.md b/src/extensions/step_handlers/simulator_handler/README.md similarity index 100% rename from src/content_handlers/simulator_handler/README.md rename to src/extensions/step_handlers/simulator_handler/README.md diff --git a/src/content_handlers/simulator_handler/du-simulator-data-template.jsonc b/src/extensions/step_handlers/simulator_handler/du-simulator-data-template.jsonc similarity index 82% rename from src/content_handlers/simulator_handler/du-simulator-data-template.jsonc rename to src/extensions/step_handlers/simulator_handler/du-simulator-data-template.jsonc index 05198080c..e03638312 100644 --- a/src/content_handlers/simulator_handler/du-simulator-data-template.jsonc +++ b/src/extensions/step_handlers/simulator_handler/du-simulator-data-template.jsonc @@ -69,5 +69,21 @@ "extendedResultCode" : 0, "resultDetails" : "" } + }, + // + // For 'backup' function, only one result will be returned. + // + "backup" : { + "resultCode" : 1000, // ADUC_Result_Install_Success + "extendedResultCode" : 0, + "resultDetails" : "" + }, + // + // For 'backup' function, only one result will be returned. + // + "restore" : { + "resultCode" : 1100, // ADUC_Result_Install_Success + "extendedResultCode" : 0, + "resultDetails" : "" } } diff --git a/src/content_handlers/simulator_handler/inc/aduc/simulator_handler.hpp b/src/extensions/step_handlers/simulator_handler/inc/aduc/simulator_handler.hpp similarity index 92% rename from src/content_handlers/simulator_handler/inc/aduc/simulator_handler.hpp rename to src/extensions/step_handlers/simulator_handler/inc/aduc/simulator_handler.hpp index 353f99ae0..d6dcbb4cf 100644 --- a/src/content_handlers/simulator_handler/inc/aduc/simulator_handler.hpp +++ b/src/extensions/step_handlers/simulator_handler/inc/aduc/simulator_handler.hpp @@ -39,8 +39,10 @@ class SimulatorHandlerImpl : public ContentHandler ~SimulatorHandlerImpl() override; ADUC_Result Download(const tagADUC_WorkflowData* workflowData) override; + ADUC_Result Backup(const tagADUC_WorkflowData* workflowData) override; ADUC_Result Install(const tagADUC_WorkflowData* workflowData) override; ADUC_Result Apply(const tagADUC_WorkflowData* workflowData) override; + ADUC_Result Restore(const tagADUC_WorkflowData* workflowData) override; ADUC_Result Cancel(const tagADUC_WorkflowData* workflowData) override; ADUC_Result IsInstalled(const tagADUC_WorkflowData* workflowData) override; diff --git a/src/content_handlers/simulator_handler/src/simulator_handler.cpp b/src/extensions/step_handlers/simulator_handler/src/simulator_handler.cpp similarity index 88% rename from src/content_handlers/simulator_handler/src/simulator_handler.cpp rename to src/extensions/step_handlers/simulator_handler/src/simulator_handler.cpp index fb2a796b6..6784d80e3 100644 --- a/src/content_handlers/simulator_handler/src/simulator_handler.cpp +++ b/src/extensions/step_handlers/simulator_handler/src/simulator_handler.cpp @@ -16,6 +16,55 @@ EXTERN_C_BEGIN +///////////////////////////////////////////////////////////////////////////// +// BEGIN Shared Library Export Functions +// +// These are the function symbols that the device update agent will +// lookup and call. +// + +/** + * @brief Instantiates an Simulator Update Content Handler + * @return ContentHandler* The created instance. + */ +ContentHandler* CreateUpdateContentHandlerExtension(ADUC_LOG_SEVERITY logLevel) +{ + ADUC_Logging_Init(logLevel, "simulator-handler"); + Log_Info("Instantiating a Simulator Update Content Handler"); + try + { + return SimulatorHandlerImpl::CreateContentHandler(); + } + catch (const std::exception& e) + { + const char* what = e.what(); + Log_Error("Unhandled std exception: %s", what); + } + catch (...) + { + Log_Error("Unhandled exception"); + } + + return nullptr; +} + +/** + * @brief Gets the extension contract info. + * + * @param[out] contractInfo The extension contract info. + * @return ADUC_Result The result. + */ +ADUC_Result GetContractInfo(ADUC_ExtensionContractInfo* contractInfo) +{ + contractInfo->majorVer = ADUC_V1_CONTRACT_MAJOR_VER; + contractInfo->minorVer = ADUC_V1_CONTRACT_MINOR_VER; + return ADUC_Result{ ADUC_GeneralResult_Success, 0 }; +} + +// +// END Shared Library Export Functions +///////////////////////////////////////////////////////////////////////////// + /** * @brief Retrieve system temporary path with a subfolder. * @@ -99,30 +148,6 @@ char* _StringFormat(const char* fmt, ...) return outputStr; } -/** - * @brief Instantiates an Simulator Update Content Handler - */ -ContentHandler* CreateUpdateContentHandlerExtension(ADUC_LOG_SEVERITY logLevel) -{ - ADUC_Logging_Init(logLevel, "simulator-handler"); - Log_Info("Instantiating a Simulator Update Content Handler"); - try - { - return SimulatorHandlerImpl::CreateContentHandler(); - } - catch (const std::exception& e) - { - const char* what = e.what(); - Log_Error("Unhandled std exception: %s", what); - } - catch (...) - { - Log_Error("Unhandled exception"); - } - - return nullptr; -} - EXTERN_C_END /** @@ -328,6 +353,16 @@ ADUC_Result SimulatorActionHelper( return result; } +/** + * @brief Mock implementation of backup + * @return ADUC_Result Return result from simulator data file if specified. + * Otherwise, return ADUC_Result_Backup_Success. + */ +ADUC_Result SimulatorHandlerImpl::Backup(const tagADUC_WorkflowData* workflowData) +{ + return SimulatorActionHelper(workflowData, ADUC_Result_Backup_Success, "backup", nullptr); +} + /** * @brief Mock implementation of install * @return ADUC_Result Return result from simulator data file if specified. @@ -338,6 +373,16 @@ ADUC_Result SimulatorHandlerImpl::Install(const tagADUC_WorkflowData* workflowDa return SimulatorActionHelper(workflowData, ADUC_Result_Install_Success, "install", nullptr); } +/** + * @brief Mock implementation of restore + * @return ADUC_Result Return result from simulator data file if specified. + * Otherwise, return ADUC_Result_Restore_Success. + */ +ADUC_Result SimulatorHandlerImpl::Restore(const tagADUC_WorkflowData* workflowData) +{ + return SimulatorActionHelper(workflowData, ADUC_Result_Restore_Success, "restore", nullptr); +} + /** * @brief Mock implementation of apply * @return ADUC_Result Return result from simulator data file if specified. diff --git a/src/content_handlers/simulator_handler/tests/CMakeLists.txt b/src/extensions/step_handlers/simulator_handler/tests/CMakeLists.txt similarity index 89% rename from src/content_handlers/simulator_handler/tests/CMakeLists.txt rename to src/extensions/step_handlers/simulator_handler/tests/CMakeLists.txt index ad7f8f50c..a9e75b054 100644 --- a/src/content_handlers/simulator_handler/tests/CMakeLists.txt +++ b/src/extensions/step_handlers/simulator_handler/tests/CMakeLists.txt @@ -14,11 +14,7 @@ include (agentRules) compileasc99 () disablertti () -set ( - sources - main.cpp - simulator_handler_unit_tests.cpp - ../src/simulator_handler.cpp) +set (sources main.cpp simulator_handler_unit_tests.cpp ../src/simulator_handler.cpp) find_package (Catch2 REQUIRED) find_package (azure_c_shared_utility REQUIRED) @@ -39,8 +35,7 @@ target_include_directories ( target_link_libraries ( ${PROJECT_NAME} PUBLIC aduc::extension_manager - PRIVATE aduc::c_utils - aduc::content_handlers + PRIVATE aduc::contract_utils aduc::exception_utils aduc::extension_utils aduc::logging diff --git a/src/content_handlers/simulator_handler/tests/main.cpp b/src/extensions/step_handlers/simulator_handler/tests/main.cpp similarity index 100% rename from src/content_handlers/simulator_handler/tests/main.cpp rename to src/extensions/step_handlers/simulator_handler/tests/main.cpp diff --git a/src/content_handlers/simulator_handler/tests/simulator_handler_unit_tests.cpp b/src/extensions/step_handlers/simulator_handler/tests/simulator_handler_unit_tests.cpp similarity index 89% rename from src/content_handlers/simulator_handler/tests/simulator_handler_unit_tests.cpp rename to src/extensions/step_handlers/simulator_handler/tests/simulator_handler_unit_tests.cpp index 2d5b99f3f..911808814 100644 --- a/src/content_handlers/simulator_handler/tests/simulator_handler_unit_tests.cpp +++ b/src/extensions/step_handlers/simulator_handler/tests/simulator_handler_unit_tests.cpp @@ -176,6 +176,24 @@ const char* isInstalled_notInstalled = R"( })" R"(})"; +const char* backupSucceed1000 = + R"({)" + R"( "backup" : {)" + R"( "resultCode" : 1000,)" + R"( "extendedResultCode" : 0,)" + R"( "resultDetails" : "Mock backup succeeded - 1000")" + R"( })" + R"(})"; + +const char* restoreSucceed1100 = + R"({)" + R"( "restore" : {)" + R"( "resultCode" : 1100,)" + R"( "extendedResultCode" : 0,)" + R"( "resultDetails" : "Mock restore succeeded - 1100")" + R"( })" + R"(})"; + // clang-format on TEST_CASE("Download Succeeded 500") @@ -482,3 +500,51 @@ TEST_CASE("IsInstalled - notInstalled") workflow_free(handle); } + +TEST_CASE("Backup Succeeded 1000") +{ + SimulatorHandlerDataFile simData(backupSucceed1000); + + ADUC_WorkflowHandle handle = nullptr; + ADUC_Result result = workflow_init(action_process_deployment, false, &handle); + CHECK(result.ResultCode != 0); + CHECK(result.ExtendedResultCode == 0); + + ADUC_WorkflowData testWorkflow{}; + testWorkflow.WorkflowHandle = handle; + + std::unique_ptr simHandler{ CreateUpdateContentHandlerExtension(ADUC_LOG_DEBUG) }; + + result = simHandler->Backup(&testWorkflow); + testWorkflow.WorkflowHandle = nullptr; + + CHECK(result.ResultCode == 1000); + CHECK(result.ExtendedResultCode == 0); + CHECK_THAT(workflow_peek_result_details(handle), Equals("Mock backup succeeded - 1000")); + + workflow_free(handle); +} + +TEST_CASE("Restore Succeeded 1100") +{ + SimulatorHandlerDataFile simData(restoreSucceed1100); + + ADUC_WorkflowHandle handle = nullptr; + ADUC_Result result = workflow_init(action_process_deployment, false, &handle); + CHECK(result.ResultCode != 0); + CHECK(result.ExtendedResultCode == 0); + + ADUC_WorkflowData testWorkflow{}; + testWorkflow.WorkflowHandle = handle; + + std::unique_ptr simHandler{ CreateUpdateContentHandlerExtension(ADUC_LOG_DEBUG) }; + + result = simHandler->Restore(&testWorkflow); + testWorkflow.WorkflowHandle = nullptr; + + CHECK(result.ResultCode == 1100); + CHECK(result.ExtendedResultCode == 0); + CHECK_THAT(workflow_peek_result_details(handle), Equals("Mock restore succeeded - 1100")); + + workflow_free(handle); +} \ No newline at end of file diff --git a/src/content_handlers/swupdate_handler/CMakeLists.txt b/src/extensions/step_handlers/swupdate_handler/CMakeLists.txt similarity index 83% rename from src/content_handlers/swupdate_handler/CMakeLists.txt rename to src/extensions/step_handlers/swupdate_handler/CMakeLists.txt index aa411aa0a..065db2352 100644 --- a/src/content_handlers/swupdate_handler/CMakeLists.txt +++ b/src/extensions/step_handlers/swupdate_handler/CMakeLists.txt @@ -4,7 +4,7 @@ set (target_name microsoft_swupdate_1) set (SOURCE_ALL src/swupdate_handler.cpp) -add_library (${target_name} SHARED ${SOURCE_ALL}) +add_library (${target_name} MODULE ${SOURCE_ALL}) add_library (aduc::${target_name} ALIAS ${target_name}) @@ -19,17 +19,16 @@ target_include_directories ( target_link_libraries ( ${target_name} - PRIVATE aduc::c_utils - aduc::exception_utils + PRIVATE aduc::adu_core_export_helpers + aduc::c_utils + aduc::contract_utils aduc::extension_manager aduc::logging aduc::process_utils aduc::string_utils aduc::system_utils aduc::workflow_data_utils - aduc::workflow_utils - -zdefs - ) + aduc::workflow_utils) target_compile_definitions (${target_name} PRIVATE ADUC_VERSION_FILE="${ADUC_VERSION_FILE}" ADUC_LOG_FOLDER="${ADUC_LOG_FOLDER}") diff --git a/src/content_handlers/swupdate_handler/README.md b/src/extensions/step_handlers/swupdate_handler/README.md similarity index 100% rename from src/content_handlers/swupdate_handler/README.md rename to src/extensions/step_handlers/swupdate_handler/README.md diff --git a/src/content_handlers/swupdate_handler/inc/aduc/swupdate_handler.hpp b/src/extensions/step_handlers/swupdate_handler/inc/aduc/swupdate_handler.hpp similarity index 92% rename from src/content_handlers/swupdate_handler/inc/aduc/swupdate_handler.hpp rename to src/extensions/step_handlers/swupdate_handler/inc/aduc/swupdate_handler.hpp index 599a1d47e..3174dbc34 100644 --- a/src/content_handlers/swupdate_handler/inc/aduc/swupdate_handler.hpp +++ b/src/extensions/step_handlers/swupdate_handler/inc/aduc/swupdate_handler.hpp @@ -42,8 +42,10 @@ class SWUpdateHandlerImpl : public ContentHandler ~SWUpdateHandlerImpl() override; ADUC_Result Download(const tagADUC_WorkflowData* workflowData) override; + ADUC_Result Backup(const tagADUC_WorkflowData* workflowData) override; ADUC_Result Install(const tagADUC_WorkflowData* workflowData) override; ADUC_Result Apply(const tagADUC_WorkflowData* workflowData) override; + ADUC_Result Restore(const tagADUC_WorkflowData* workflowData) override; ADUC_Result Cancel(const tagADUC_WorkflowData* workflowData) override; ADUC_Result IsInstalled(const tagADUC_WorkflowData* workflowData) override; diff --git a/src/content_handlers/swupdate_handler/src/swupdate_handler.cpp b/src/extensions/step_handlers/swupdate_handler/src/swupdate_handler.cpp similarity index 74% rename from src/content_handlers/swupdate_handler/src/swupdate_handler.cpp rename to src/extensions/step_handlers/swupdate_handler/src/swupdate_handler.cpp index 4e83d4a76..df5976714 100644 --- a/src/content_handlers/swupdate_handler/src/swupdate_handler.cpp +++ b/src/extensions/step_handlers/swupdate_handler/src/swupdate_handler.cpp @@ -41,8 +41,17 @@ namespace adushconst = Adu::Shell::Const; EXTERN_C_BEGIN + +///////////////////////////////////////////////////////////////////////////// +// BEGIN Shared Library Export Functions +// +// These are the function symbols that the device update agent will +// lookup and call. +// + /** * @brief Instantiates an Update Content Handler for 'microsoft/swupdate:1' update type. + * @return ContentHandler* The created instance. */ ContentHandler* CreateUpdateContentHandlerExtension(ADUC_LOG_SEVERITY logLevel) { @@ -64,6 +73,24 @@ ContentHandler* CreateUpdateContentHandlerExtension(ADUC_LOG_SEVERITY logLevel) return nullptr; } + +/** + * @brief Gets the extension contract info. + * + * @param[out] contractInfo The extension contract info. + * @return ADUC_Result The result. + */ +ADUC_Result GetContractInfo(ADUC_ExtensionContractInfo* contractInfo) +{ + contractInfo->majorVer = ADUC_V1_CONTRACT_MAJOR_VER; + contractInfo->minorVer = ADUC_V1_CONTRACT_MINOR_VER; + return ADUC_Result{ ADUC_GeneralResult_Success, 0 }; +} + +// +// END Shared Library Export Functions +///////////////////////////////////////////////////////////////////////////// + EXTERN_C_END /** @@ -99,19 +126,25 @@ ADUC_Result SWUpdateHandlerImpl::Download(const tagADUC_WorkflowData* workflowDa ADUC_Result result = { ADUC_Result_Failure }; ADUC_FileEntity* entity = nullptr; ADUC_WorkflowHandle workflowHandle = workflowData->WorkflowHandle; - char* workflowId = workflow_get_id(workflowHandle); char* workFolder = workflow_get_workfolder(workflowHandle); int fileCount = 0; char* updateType = workflow_get_update_type(workflowHandle); char* updateName = nullptr; unsigned int updateTypeVersion = 0; - bool updateTypeOk = ADUC_ParseUpdateType(updateType, &updateName, &updateTypeVersion); + bool updateTypeOk = false; + + if (workflow_is_cancel_requested(workflowHandle)) + { + result = this->Cancel(workflowData); + goto done; + } + updateTypeOk = ADUC_ParseUpdateType(updateType, &updateName, &updateTypeVersion); if (!updateTypeOk) { Log_Error("SWUpdate packages download failed. Unknown Handler Version (UpdateDateType:%s)", updateType); - result.ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_FAILURE_UNKNOW_UPDATE_VERSION; + result.ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_FAILURE_UNKNOWN_UPDATE_VERSION; goto done; } @@ -133,16 +166,21 @@ ADUC_Result SWUpdateHandlerImpl::Download(const tagADUC_WorkflowData* workflowDa if (!workflow_get_update_file(workflowHandle, 0, &entity)) { - result.ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_DOWNLOADE_BAD_FILE_ENTITY; + result.ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_BAD_FILE_ENTITY; goto done; } updateFilename << workFolder << "/" << entity->TargetFilename; - result = ExtensionManager::Download(entity, workflowId, workFolder, DO_RETRY_TIMEOUT_DEFAULT, nullptr); + { + ExtensionManager_Download_Options downloadOptions = { + .retryTimeout = DO_RETRY_TIMEOUT_DEFAULT, + }; + + result = ExtensionManager::Download(entity, workflowHandle, &downloadOptions, nullptr); + } done: - workflow_free_string(workflowId); workflow_free_string(workFolder); workflow_free_file_entity(entity); @@ -174,6 +212,12 @@ ADUC_Result SWUpdateHandlerImpl::Install(const tagADUC_WorkflowData* workflowDat goto done; } + if (workflow_is_cancel_requested(workflowHandle)) + { + result = this->Cancel(workflowData); + goto done; + } + if (!workflow_get_update_file(workflowHandle, 0, &entity)) { result.ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_INSTALL_FAILURE_BAD_FILE_ENTITY; @@ -200,6 +244,7 @@ ADUC_Result SWUpdateHandlerImpl::Install(const tagADUC_WorkflowData* workflowDat args.emplace_back(data.str().c_str()); args.emplace_back(adushconst::target_log_folder_opt); + args.emplace_back(ADUC_LOG_FOLDER); std::string output; @@ -232,7 +277,14 @@ ADUC_Result SWUpdateHandlerImpl::Install(const tagADUC_WorkflowData* workflowDat ADUC_Result SWUpdateHandlerImpl::Apply(const tagADUC_WorkflowData* workflowData) { ADUC_Result result = { ADUC_Result_Failure }; - char* workFolder = workflow_get_workfolder(workflowData->WorkflowHandle); + char* workFolder = NULL; + + if (workflow_is_cancel_requested(workflowData->WorkflowHandle)) + { + return this->Cancel(workflowData); + } + + workFolder = workflow_get_workfolder(workflowData->WorkflowHandle); Log_Info("Applying data from %s", workFolder); // Execute the install command with "-a" to apply the install by telling @@ -261,6 +313,12 @@ ADUC_Result SWUpdateHandlerImpl::Apply(const tagADUC_WorkflowData* workflowData) } // Cancel requested? + if (workflow_is_cancel_requested(workflowData->WorkflowHandle)) + { + result = this->Cancel(workflowData); + goto done; + } + if (workflow_get_operation_cancel_requested(workflowData->WorkflowHandle)) { CancelApply(ADUC_LOG_FOLDER); @@ -270,8 +328,11 @@ ADUC_Result SWUpdateHandlerImpl::Apply(const tagADUC_WorkflowData* workflowData) workflow_free_string(workFolder); // Always require a reboot after successful apply - workflow_request_immediate_reboot(workflowData->WorkflowHandle); - result = { ADUC_Result_Apply_RequiredImmediateReboot }; + if (IsAducResultCodeSuccess(result.ResultCode)) + { + workflow_request_immediate_reboot(workflowData->WorkflowHandle); + result = { ADUC_Result_Apply_RequiredImmediateReboot }; + } return result; } @@ -287,8 +348,27 @@ ADUC_Result SWUpdateHandlerImpl::Apply(const tagADUC_WorkflowData* workflowData) */ ADUC_Result SWUpdateHandlerImpl::Cancel(const tagADUC_WorkflowData* workflowData) { - UNREFERENCED_PARAMETER(workflowData); - return ADUC_Result{ ADUC_Result_Cancel_Success }; + ADUC_Result result = { .ResultCode = ADUC_Result_Cancel_Success, .ExtendedResultCode = 0 }; + ADUC_WorkflowHandle handle = workflowData->WorkflowHandle; + ADUC_WorkflowHandle stepWorkflowHandle = nullptr; + + const char* workflowId = workflow_peek_id(handle); + int workflowLevel = workflow_get_level(handle); + int workflowStep = workflow_get_step_index(handle); + + Log_Info( + "Requesting cancel operation (workflow id '%s', level %d, step %d).", workflowId, workflowLevel, workflowStep); + if (!workflow_request_cancel(handle)) + { + Log_Error( + "Cancellation request failed. (workflow id '%s', level %d, step %d)", + workflowId, + workflowLevel, + workflowStep); + result.ResultCode = ADUC_Result_Cancel_UnableToCancel; + } + + return result; } /** @@ -342,9 +422,16 @@ std::string SWUpdateHandlerImpl::ReadValueFromFile(const std::string& filePath) */ ADUC_Result SWUpdateHandlerImpl::IsInstalled(const tagADUC_WorkflowData* workflowData) { - char* installedCriteria = ADUC_WorkflowData_GetInstalledCriteria(workflowData); ADUC_Result result; + char* installedCriteria = ADUC_WorkflowData_GetInstalledCriteria(workflowData); + if (installedCriteria == nullptr) + { + Log_Error("Missing installedCriteria."); + result = { ADUC_Result_Failure, ADUC_ERC_SWUPDATE_HANDLER_MISSING_INSTALLED_CRITERIA }; + return result; + } + std::string version{ ReadValueFromFile(ADUC_VERSION_FILE) }; if (version.empty()) { @@ -399,3 +486,39 @@ static ADUC_Result CancelApply(const char* logFolder) Log_Info("Apply was cancelled"); return ADUC_Result{ ADUC_Result_Failure_Cancelled }; } + +/** + * @brief Backup implementation for swupdate. + * Calls into the swupdate wrapper script to perform backup. + * For swupdate, no operation is required. + * + * @return ADUC_Result The result of the backup. + * It will always return ADUC_Result_Backup_Success. + */ +ADUC_Result SWUpdateHandlerImpl::Backup(const tagADUC_WorkflowData* workflowData) +{ + UNREFERENCED_PARAMETER(workflowData); + ADUC_Result result = { ADUC_Result_Backup_Success }; + Log_Info("SWUpdate doesn't require a specific operation to backup. (no-op) "); + return result; +} + +/** + * @brief Restore implementation for swupdate. + * Calls into the swupdate wrapper script to perform restore. + * Will flip bootloader flag to boot into the previous partition for A/B update. + * + * @return ADUC_Result The result of the restore. + */ +ADUC_Result SWUpdateHandlerImpl::Restore(const tagADUC_WorkflowData* workflowData) +{ + UNREFERENCED_PARAMETER(workflowData); + ADUC_Result result = { ADUC_Result_Restore_Success }; + ADUC_Result cancel_result = CancelApply(ADUC_LOG_FOLDER); + if (cancel_result.ResultCode != ADUC_Result_Failure_Cancelled) + { + result = { .ResultCode = ADUC_Result_Failure, + .ExtendedResultCode = ADUC_ERC_UPPERLEVEL_WORKFLOW_FAILED_RESTORE_FAILED }; + } + return result; +} diff --git a/src/extensions/step_handlers/swupdate_handler_v2/CMakeLists.txt b/src/extensions/step_handlers/swupdate_handler_v2/CMakeLists.txt new file mode 100644 index 000000000..47d0f6d69 --- /dev/null +++ b/src/extensions/step_handlers/swupdate_handler_v2/CMakeLists.txt @@ -0,0 +1,54 @@ +cmake_minimum_required (VERSION 3.5) + +set (target_name microsoft_swupdate_2) + +set (SWUPDATE_HANDLER_CONF_FILE "swupdate-handler-config.json") + +set ( + ADUC_SWUPDATE_HANDLER_CONF_FILE_PATH + "${ADUC_CONF_FOLDER}/${SWUPDATE_HANDLER_CONF_FILE}" + CACHE STRING "Path to the SWUpdate handler configuration file.") + +add_library (${target_name} SHARED "") + +add_library (aduc::${target_name} ALIAS ${target_name}) + +find_package (Parson REQUIRED) + +target_sources (${target_name} PRIVATE src/handler_create.cpp src/swupdate_handler_v2.cpp) + +target_include_directories ( + ${target_name} + PUBLIC inc + PRIVATE ${PROJECT_SOURCE_DIR}/inc + ${ADUC_TYPES_INCLUDES} + ${ADUC_EXPORT_INCLUDES} + ${ADU_SHELL_INCLUDES} + ${ADU_EXTENSION_INCLUDES}) + +target_link_libraries ( + ${target_name} + PRIVATE aduc::c_utils + aduc::contract_utils + aduc::exception_utils + aduc::extension_manager + aduc::logging + aduc::process_utils + aduc::string_utils + aduc::system_utils + aduc::workflow_data_utils + aduc::workflow_utils + Parson::parson) + +target_compile_definitions ( + ${target_name} + PRIVATE ADUC_VERSION_FILE="${ADUC_VERSION_FILE}" ADUC_LOG_FOLDER="${ADUC_LOG_FOLDER}" + ADUC_SWUPDATE_HANDLER_CONF_FILE_PATH="${ADUC_SWUPDATE_HANDLER_CONF_FILE_PATH}") + +install (TARGETS ${target_name} LIBRARY DESTINATION ${ADUC_EXTENSIONS_INSTALL_FOLDER}) + +if (ADUC_BUILD_UNIT_TESTS) + + add_subdirectory (tests) + +endif () diff --git a/src/extensions/step_handlers/swupdate_handler_v2/README.md b/src/extensions/step_handlers/swupdate_handler_v2/README.md new file mode 100644 index 000000000..9e0d83e87 --- /dev/null +++ b/src/extensions/step_handlers/swupdate_handler_v2/README.md @@ -0,0 +1,109 @@ +# SWUpdate Handler + +## What is SWUpdate? + +[SWUpdate](https://github.com/sbabic/swupdate) is a Linux Update agent with the goal to provide an efficient and safe way to update an embedded Linux system in field. SWUpdate supports local and OTA updates, multiple update strategies and it is designed with security in mind. + +## What is SWUpdate Handler? + +SWUpdate Handler is a Device Update Agent extension that provides a local software update capability on an embedded device with payloads delivered over the air via Device Update for IoT Hub. + +See [How To Implement A Custom Content Handler Extension](../../../docs/agent-reference/how-to-implement-custom-update-handler.md) for more information. + +## SWUpdate Handler Goals + +While SWUpdate supports many features (see [SWUpdate feature list](https://github.com/sbabic/swupdate#features) ), the main goals of this SWUpdate Handler are to demonstrate: + +- how to perform A/B system update on an embedded linux device. +- how to execute SWUpdate command, with pass-through options, on an embedded linux device. +- how to deliver multiple .swu files to a device, then install those files onto multiple peripherals. + +> NOTE | for A/B system update, most of the example scripts and update artifacts in this document will used to demonstrate how to deliver an update to a Raspberry Pi device that running a sample reference Yocto (warrior) image with built-in Device Update Agent service. + +## Architecture + +### High-level Overview of Device Update Agent Workflow + +![SWUpdate Handler Overview Diagram](./images/highlevel-overview-swupdate-handler-workflow.svg) + +## How To Use SWUpdate Handler + +### Registering SWUpdate Handler extension + +To enable SWUpdate on the device, the handler must be registered to support **'microsoft/swupdate:2'**. + +Note that this version of SWUpdate Handler is different from the previous version published as part of the 2022 Public Preview Refresh release. The new handler is not backward compatible with previous versions. Hence, we recommend using a new update-type (**'microsoft/swupdate:2'**) + +Following command can be run manually on the device: + +```sh +sudo /usr/bin/AducIotAgent --update-type 'microsoft/swupdate:2' --register-content-handler +``` + +### Setting SWUpdate Handler Properties + +Using [Multi Step Ordered Exeuction (MSOE)](), you can create a step that perform various swupdate tasks on the embedded device. + +The SWUpdate Handler support following handler properties (`handlerProperties`) : + +| Name | Type | Description | +|---|---|---| +| scriptFileName | string | Name of a script file that perform additional logics related to A/B system update. This property should be specified only when performing A/B System Update.

See [A/B Update Script](#ab-update-script) below for more information. | +| arguments | string | A space delimited options and arguments that will be passed directly to SWUpdate command. +| installedCriteria | string | String interpreted by the specified `scriptFileName` to determine if the update completed successfully.
This value will be passed to the underlying update script in this format: `--installed-criteria ` | + +#### List of Supported handlerProperties.arguments + +| Name | Type | Required | Description | +|---|---|---|---| +|--swu-file| string | yes | Name of the image or software (.swu) file to be installed by swupdate. + +#### List of SWUpdate runtime options + +| Name | Type | Description | +|---|---|---| +|--workfolder| string | Full path to a work (sandbox) folder used by an Agent when performing update-related tasks. | +|--output-file|string|Full path to an output file that swupdate script should write to| +|--log-file|string| Full path to a log file that swupdate script should write to. (This is different that Agent's log file)| +|--result-file|string|Full path to an ADUC_Result file that swupdate script must write the end result of the update tasks to. If this file does not exist or cannot be parsed, the task will be considered failed.| +|--action-download,
--action-install,
--action-apply,
--action-cancel,
--action-is-installed| (no arguments)| An option indicates the current update task. + +### Example: A/B Update Script + +In [./tests/testdata](./tests/testdata/) folder you will find an example script file that can be invoked to perform various update related tasks, such as: + +- download additional files. +- install the update (.swu file). +- apply the update (e.g., reboot the device into the updated partition) +- cancel the update. +- check whether the device meets an installed criteria specified in the update manifest. + +#### How SWUpdate Handler Invokes Update Script + +![SWUpdate script options and arguments](./images/swupdate-script-options-and-arguments.svg) + +1. SWUpdate prepares options and arguments from the following sources: + - swupdate-handler-config.json (static data in .json file) + - an agent workflow context (runtime data), such as, `--work-folder ` + - handlerProperties.swuFileName (static data in update manifest). This value will be converted to a script option as `--swu-file `. + - handlerProperties.arguments (static data in update manifest) + - SWUpdate log, output, and result file path. +2. SWUpdate then invoke an underlying update script as 'root' (using adu-shell as a broker). +3. After the script is completed, SWUpdate handler collects data from log file, output file, and result file, then proceed an Agent workflow accordingly. + +#### Evaluating 'installedCriteria' + +For this example, to determine whether the new image has been installed on the device successfully, the SWUpdate Handler will run the specified `scriptFileName` with `--action-is-installed ''` option. + +For example: + +```sh + +/adu/downloads/workflow-01234567/example-a-b-update.sh --action-is-installed --installed-criteria '8.0.1.0001' + +``` + +In this [example update script](./tests/testdata/adu-yocto-ab-rootfs-update/example-a-b-update.sh), it simply compares the given 'installedCriteria' value (8.0.1.0001) to a content of `adu-version` file (located at /etc/adu-version folder). + +The algorithm for evaluating `installedCriteria` can be 100% customized to fit your device design and requirements. + diff --git a/src/extensions/step_handlers/swupdate_handler_v2/images/highlevel-overview-swupdate-handler-workflow.svg b/src/extensions/step_handlers/swupdate_handler_v2/images/highlevel-overview-swupdate-handler-workflow.svg new file mode 100644 index 000000000..0602cea11 --- /dev/null +++ b/src/extensions/step_handlers/swupdate_handler_v2/images/highlevel-overview-swupdate-handler-workflow.svg @@ -0,0 +1,3 @@ + + +
swupdate process
(run as root)
swupdate process...
Local storage
Local storage
/var/lib/adu/downloads
/var/lib/adu/downloads
write logs
write logs
adu-shell process
(run as root)
adu-shell process...
adu-shell process
(run as root)
adu-shell process...
DU Agent Process
(run as 'adu')
DU Agent Process...
Invoke
Invoke
Download
Download
SWUpdate Handler
SWUpdate Handler
Invoke
Invoke
DU Agent Workflow
DU Agent Workflow
Invoke
Invoke
adu-shell
adu-shell
Invoke
Invoke
example-swupdate-script.sh
example-swupdate-script.sh
example-swupdate-script.sh
example...
Invoke
Invoke
SWUpdate
SWUpdate
fw_setenv
(run as root)
fw_setenv...
Invoke
Invoke
fw_setenv
fw_setenv
Invoke
Invoke
/etc/fw_env.config
/etc/fw_env.config
/adu/logs/swupdate.log
/adu/logs/swupdate.l...
set uboot env var
set uboot env var
aduc_result.json
aduc_result.json
swupdate handler logs
swupdate handler logs
save results
save results
write logs
write logs
read result
read result
/adu/swupdate-handler-config.json
/adu/swupdate-handle...
read configs
read configs
.SWU
.SWU
Download
Download
Text is not SVG - cannot display
\ No newline at end of file diff --git a/src/extensions/step_handlers/swupdate_handler_v2/images/swupdate-script-options-and-arguments.svg b/src/extensions/step_handlers/swupdate_handler_v2/images/swupdate-script-options-and-arguments.svg new file mode 100644 index 000000000..06f6874a1 --- /dev/null +++ b/src/extensions/step_handlers/swupdate_handler_v2/images/swupdate-script-options-and-arguments.svg @@ -0,0 +1,3 @@ + + +
adu-shell process
(run as root)
adu-shell process...
adu-shell process
(run as root)
adu-shell process...
DU Agent Process
(run as 'adu')
DU Agent Process...
Prepare script options and
arguments then invoke...
Prepare script options and...
SWUpdate Handler
SWUpdate Handler
Invoke
(passing WorkflowHandle & action)
Invoke...
DU Agent Workflow
DU Agent Workflow
Invoke
Invoke
adu-shell
adu-shell
example-swupdate-script.sh
example-swupdate-script.sh
parse
update manifest
parse...
/adu/swupdate-handler-config.json
{
    "--public-key-file" : "/adukey/public.pem",
    "--swu-log-file": "/adu/logs/swupdate.log",
}
/adu/swupdate-handler-config.json{...
read configs
read configs
...
"steps" : [ {
    ...
    "handlerProperties" : {
      "scriptFileName":"example-swupdate-script.sh",
"swuFileName" : "my-update.swu",
      "arguments": "--my-option-1 my-arg-1",
"installCriteria": "8.0.1.1"
      }
...
}, ... ]
......

WorkflowHandle contains:

"workFolder" : "/adu/downloads/workflowid_0001/",

...

WorkflowHandle contains:...

example-swupdate-script.sh --swu-file my-update.swu --my-option-1 my-arg-1 --installed-criteria "8.0.1.1" --public-key-file "/adukey/public.pem" --swu-log-file "/adu/logs/swupdate.log" --work-folder "/adu/downloads/workflowid_0001" --action-install --result-file "/adu/downloads/workflowid_0001/aducresult.json" --output-file " "/adu/downloads/workflowid_0001/output.txt" 

example-swupdate-script.sh --swu-file my-update.swu --my-o...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/src/extensions/step_handlers/swupdate_handler_v2/inc/aduc/swupdate_handler_v2.hpp b/src/extensions/step_handlers/swupdate_handler_v2/inc/aduc/swupdate_handler_v2.hpp new file mode 100644 index 000000000..c7f29f21b --- /dev/null +++ b/src/extensions/step_handlers/swupdate_handler_v2/inc/aduc/swupdate_handler_v2.hpp @@ -0,0 +1,63 @@ +/** + * @file swupdate_handler.hpp + * @brief Defines SWUpdateHandlerImpl. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ +#ifndef ADUC_SWUPDATE_HANDLER_HPP +#define ADUC_SWUPDATE_HANDLER_HPP + +#include "aduc/content_handler.hpp" +#include +#include +#include +#include + +typedef void* ADUC_WorkflowHandle; + +/** + * @class SWUpdateHandlerImpl + * @brief The swupdate specific implementation of ContentHandler interface. + */ +class SWUpdateHandlerImpl : public ContentHandler +{ +public: + static ContentHandler* CreateContentHandler(); + + // Delete copy ctor, copy assignment, move ctor and move assignment operators. + SWUpdateHandlerImpl(const SWUpdateHandlerImpl&) = delete; + SWUpdateHandlerImpl& operator=(const SWUpdateHandlerImpl&) = delete; + SWUpdateHandlerImpl(SWUpdateHandlerImpl&&) = delete; + SWUpdateHandlerImpl& operator=(SWUpdateHandlerImpl&&) = delete; + + ~SWUpdateHandlerImpl() override; + + ADUC_Result Download(const tagADUC_WorkflowData* workflowData) override; + ADUC_Result Backup(const tagADUC_WorkflowData* workflowData) override; + ADUC_Result Install(const tagADUC_WorkflowData* workflowData) override; + ADUC_Result Apply(const tagADUC_WorkflowData* workflowData) override; + ADUC_Result Cancel(const tagADUC_WorkflowData* workflowData) override; + ADUC_Result Restore(const tagADUC_WorkflowData* workflowData) override; + ADUC_Result IsInstalled(const tagADUC_WorkflowData* workflowData) override; + + static std::string ReadValueFromFile(const std::string& filePath); + static ADUC_Result ReadConfig(const std::string& configFile, std::unordered_map& values); + + static ADUC_Result PrepareCommandArguments( + const ADUC_WorkflowHandle workflowHandle, + std::string resultFilePath, + std::string workFolder, + std::string& commandFilePath, + std::vector& args); + +protected: + // Protected constructor, must call CreateContentHandler factory method or from derived simulator class + SWUpdateHandlerImpl() + { + } + + static ADUC_Result PerformAction(const std::string& action, const tagADUC_WorkflowData* workflowData); +}; + +#endif // ADUC_SWUPDATE_HANDLER_HPP diff --git a/src/extensions/step_handlers/swupdate_handler_v2/src/handler_create.cpp b/src/extensions/step_handlers/swupdate_handler_v2/src/handler_create.cpp new file mode 100644 index 000000000..617cf1467 --- /dev/null +++ b/src/extensions/step_handlers/swupdate_handler_v2/src/handler_create.cpp @@ -0,0 +1,65 @@ +/** + * @file handler_create.cpp + * @brief Create update content handler extension. + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include +#include +#include +#include + +EXTERN_C_BEGIN + +///////////////////////////////////////////////////////////////////////////// +// BEGIN Shared Library Export Functions +// +// These are the function symbols that the device update agent will +// lookup and call. +// + +/** + * @brief Instantiates an Update Content Handler for 'microsoft/swupdate:2' update type. + * @return ContentHandler* The created instance. + */ +ContentHandler* CreateUpdateContentHandlerExtension(ADUC_LOG_SEVERITY logLevel) +{ + ADUC_Logging_Init(logLevel, "swupdate-handler-v2"); + Log_Info("Instantiating an Update Content Handler for 'microsoft/swupdate:2'"); + try + { + return SWUpdateHandlerImpl::CreateContentHandler(); + } + catch (const std::exception& e) + { + const char* what = e.what(); + Log_Error("Unhandled std exception: %s", what); + } + catch (...) + { + Log_Error("Unhandled exception"); + } + + return nullptr; +} + +/** + * @brief Gets the extension contract info. + * + * @param[out] contractInfo The extension contract info. + * @return ADUC_Result The result. + */ +ADUC_Result GetContractInfo(ADUC_ExtensionContractInfo* contractInfo) +{ + contractInfo->majorVer = ADUC_V1_CONTRACT_MAJOR_VER; + contractInfo->minorVer = ADUC_V1_CONTRACT_MINOR_VER; + return ADUC_Result{ ADUC_GeneralResult_Success, 0 }; +} + +// +// END Shared Library Export Functions +///////////////////////////////////////////////////////////////////////////// + +EXTERN_C_END diff --git a/src/extensions/step_handlers/swupdate_handler_v2/src/swupdate_handler_v2.cpp b/src/extensions/step_handlers/swupdate_handler_v2/src/swupdate_handler_v2.cpp new file mode 100644 index 000000000..533fd4463 --- /dev/null +++ b/src/extensions/step_handlers/swupdate_handler_v2/src/swupdate_handler_v2.cpp @@ -0,0 +1,899 @@ +/** + * @file swupdate_handler_v2.cpp + * @brief Implementation of ContentHandler API for swupdate wrapper script. + * + * The wrapper script must be delivered to a device as part of the update payloads. + * Script options and arguments can be specified in: + * - swupdate-handler-config.json + * - Update manifest's instructions-step's handlerProperties['arguments'] + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ +#include "aduc/swupdate_handler_v2.hpp" + +#include "aduc/adu_core_exports.h" +#include "aduc/extension_manager.hpp" +#include "aduc/logging.h" +#include "aduc/process_utils.hpp" +#include "aduc/string_c_utils.h" +#include "aduc/string_utils.hpp" +#include "aduc/system_utils.h" +#include "aduc/types/update_content.h" +#include "aduc/workflow_data_utils.h" +#include "aduc/workflow_utils.h" +#include "adushell_const.hpp" + +#include +#include +#include +#include + +#include + +#define HANDLER_PROPERTIES_SCRIPT_FILENAME "scriptFileName" +#define HANDLER_PROPERTIES_SWU_FILENAME "swuFileName" + +namespace adushconst = Adu::Shell::Const; + +/* external linkage */ +extern ExtensionManager_Download_Options Default_ExtensionManager_Download_Options; + +struct JSONValueDeleter +{ + void operator()(JSON_Value* value) + { + json_value_free(value); + } +}; + +using AutoFreeJsonValue_t = std::unique_ptr; + +/** + * @brief Destructor for the SWUpdate Handler Impl class. + */ +SWUpdateHandlerImpl::~SWUpdateHandlerImpl() // override +{ + ADUC_Logging_Uninit(); +} + +/** + * @brief Downloads a main script file into a sandbox folder. + * The 'handlerProperties["scriptFileName"]' contains the main script file name. + * + * @param handle A workflow object. + * @return ADUC_Result + */ +static ADUC_Result SWUpdate_Handler_DownloadScriptFile(ADUC_WorkflowHandle handle) +{ + ADUC_Result result = { ADUC_Result_Failure }; + char* workFolder = nullptr; + ADUC_FileEntity* entity = nullptr; + int fileCount = workflow_get_update_files_count(handle); + int createResult = 0; + // Download the main script file. + const char* scriptFileName = + workflow_peek_update_manifest_handler_properties_string(handle, HANDLER_PROPERTIES_SCRIPT_FILENAME); + if (scriptFileName == nullptr) + { + result.ResultCode = ADUC_Result_Failure; + result.ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_MISSING_SCRIPT_FILE_NAME; + goto done; + } + + if (fileCount <= 1) + { + result.ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_FAILURE_WRONG_FILECOUNT; + goto done; + } + + if (!workflow_get_update_file_by_name(handle, scriptFileName, &entity)) + { + result.ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_FAILURE_GET_SCRIPT_FILE_ENTITY; + goto done; + } + + workFolder = workflow_get_workfolder(handle); + + createResult = ADUC_SystemUtils_MkSandboxDirRecursive(workFolder); + if (createResult != 0) + { + Log_Error("Unable to create folder %s, error %d", workFolder, createResult); + result = { ADUC_Result_Failure, ADUC_ERC_SWUPDATE_HANDLER_CREATE_SANDBOX_FAILURE }; + goto done; + } + + try + { + result = ExtensionManager::Download(entity, handle, &Default_ExtensionManager_Download_Options, nullptr); + } + catch (...) + { + result.ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_PRIMARY_FILE_FAILURE_UNKNOWNEXCEPTION; + } + + workflow_free_file_entity(entity); + entity = nullptr; + +done: + workflow_free_string(workFolder); + return result; +} + +/** + * @brief Perform a workflow action. If @p prepareArgsOnly is true, only prepare data, but not actually + * perform any action. + * + * @param action Indicate an action to perform. This can be '--action-download', '--action-install', + * '--action-apply', "--action-cancel", and "--action-is-installed". + * @param workflowData An object containing workflow data. + * @param prepareArgsOnly Boolean indicates whether to prepare action data only. + * @param[out] scriptFilePath Output string contains a script to be run. + * @param[in] args List of options and arguments. + * @param[out] commandLineArgs An output command-line arguments. + * @param[out] scriptOutput If @p prepareArgsOnly is false, this will contains the action output string. + * @return ADUC_Result + */ +ADUC_Result SWUpdateHandler_PerformAction( + const std::string& action, + const tagADUC_WorkflowData* workflowData, + bool prepareArgsOnly, + std::string& scriptFilePath, + std::vector& args, + std::vector& commandLineArgs, + std::string& scriptOutput) +{ + Log_Info("Action (%s) begin", action.c_str()); + ADUC_Result result = { ADUC_GeneralResult_Failure }; + + int exitCode = 0; + commandLineArgs.clear(); + + if (workflowData == nullptr || workflowData->WorkflowHandle == nullptr) + { + result.ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_INSTALL_ERROR_NULL_WORKFLOW; + return result; + } + + char* workFolder = ADUC_WorkflowData_GetWorkFolder(workflowData); + std::string scriptWorkfolder = workFolder; + std::string scriptResultFile = scriptWorkfolder + "/" + "aduc_result.json"; + JSON_Value* actionResultValue = nullptr; + + std::vector aduShellArgs = { adushconst::update_type_opt, + adushconst::update_type_microsoft_script, + adushconst::update_action_opt, + adushconst::update_action_execute }; + + std::stringstream ss; + + result = SWUpdateHandlerImpl::PrepareCommandArguments( + workflowData->WorkflowHandle, scriptResultFile, scriptWorkfolder, scriptFilePath, args); + if (IsAducResultCodeFailure(result.ResultCode)) + { + goto done; + } + + // If any install-item reported that the update is already installed on the + // selected component, we will skip the 'apply' phase, and then skip the + // remaining install-item(s). + // Also, don't continue if WorkflowHandle is NULL in the ADUInterface_Connected->HandleStartupWorkflowData flow. + if (result.ResultCode == ADUC_Result_Install_Skipped_UpdateAlreadyInstalled + || workflowData->WorkflowHandle == nullptr) + { + goto done; + } + + aduShellArgs.emplace_back(adushconst::target_data_opt); + aduShellArgs.emplace_back(scriptFilePath); + commandLineArgs.emplace_back(scriptFilePath); + + aduShellArgs.emplace_back(adushconst::target_options_opt); + aduShellArgs.emplace_back(action.c_str()); + commandLineArgs.emplace_back(action.c_str()); + + for (const auto& a : args) + { + aduShellArgs.emplace_back(adushconst::target_options_opt); + aduShellArgs.emplace_back(a); + commandLineArgs.emplace_back(a); + } + + if (prepareArgsOnly) + { + for (const auto& a : aduShellArgs) + { + if (a[0] != '-') + { + ss << " \"" << a << "\""; + } + else + { + ss << " " << a; + } + } + scriptOutput = ss.str(); + + Log_Debug("Prepare Only! adu-shell Command:\n\n %s", scriptOutput.c_str()); + result = { .ResultCode = ADUC_Result_Success, .ExtendedResultCode = 0 }; + goto done; + } + + exitCode = ADUC_LaunchChildProcess(adushconst::adu_shell, aduShellArgs, scriptOutput); + if (exitCode != 0) + { + int extendedCode = ADUC_ERC_SWUPDATE_HANDLER_CHILD_FAILURE_PROCESS_EXITCODE(exitCode); + Log_Error("Install failed, extendedResultCode:0x%X (exitCode:%d)", extendedCode, exitCode); + result = { .ResultCode = ADUC_Result_Failure, .ExtendedResultCode = extendedCode }; + } + + if (!scriptOutput.empty()) + { + Log_Info(scriptOutput.c_str()); + } + + // Parse result file. + actionResultValue = json_parse_file(scriptResultFile.c_str()); + + if (actionResultValue == nullptr) + { + result = { .ResultCode = ADUC_Result_Failure, + .ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_INSTALL_FAILURE_PARSE_RESULT_FILE }; + workflow_set_result_details( + workflowData->WorkflowHandle, + "The install script doesn't create a result file '%s'.", + scriptResultFile.c_str()); + goto done; + } + else + { + JSON_Object* actionResultObject = json_object(actionResultValue); + result.ResultCode = json_object_get_number(actionResultObject, "resultCode"); + result.ExtendedResultCode = json_object_get_number(actionResultObject, "extendedResultCode"); + const char* details = json_object_get_string(actionResultObject, "resultDetails"); + workflow_set_result_details(workflowData->WorkflowHandle, details); + } + + Log_Info( + "Action (%s) done - returning rc:%d, erc:0x%X, rd:%s", + action.c_str(), + result.ResultCode, + result.ExtendedResultCode, + workflow_peek_result_details(workflowData->WorkflowHandle)); + +done: + if (IsAducResultCodeFailure(result.ResultCode) && workflowData->WorkflowHandle != nullptr) + { + workflow_set_result(workflowData->WorkflowHandle, result); + workflow_set_state(workflowData->WorkflowHandle, ADUCITF_State_Failed); + } + + json_value_free(actionResultValue); + workflow_free_string(workFolder); + return result; +} + +/** + * @brief Creates a new SWUpdateHandlerImpl object and casts to a ContentHandler. + * Note that there is no way to create a SWUpdateHandlerImpl directly. + * + * @return ContentHandler* SimulatorHandlerImpl object as a ContentHandler. + */ +ContentHandler* SWUpdateHandlerImpl::CreateContentHandler() +{ + return new SWUpdateHandlerImpl(); +} + +/** + * @brief Performs 'Download' task. + * + * @return ADUC_Result The result of the download (always success) +*/ +ADUC_Result SWUpdateHandlerImpl::Download(const tagADUC_WorkflowData* workflowData) +{ + Log_Info("SWUpdate handler v2 download task begin."); + + ADUC_WorkflowHandle workflowHandle = workflowData->WorkflowHandle; + char* installedCriteria = nullptr; + char* workFolder = workflow_get_workfolder(workflowData->WorkflowHandle); + ADUC_FileEntity* entity = nullptr; + int fileCount = workflow_get_update_files_count(workflowHandle); + ADUC_Result result = SWUpdate_Handler_DownloadScriptFile(workflowHandle); + + if (IsAducResultCodeFailure(result.ResultCode)) + { + goto done; + } + + // Determine whether to continue downloading the rest. + installedCriteria = workflow_get_installed_criteria(workflowData->WorkflowHandle); + result = IsInstalled(workflowData); + + if (result.ResultCode == ADUC_Result_IsInstalled_Installed) + { + result = { ADUC_Result_Download_Skipped_UpdateAlreadyInstalled }; + goto done; + } + + result = { ADUC_Result_Download_Success }; + + for (int i = 0; i < fileCount; i++) + { + Log_Info("Downloading file #%d", i); + + if (!workflow_get_update_file(workflowHandle, i, &entity)) + { + result = { .ResultCode = ADUC_Result_Failure, + .ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_FAILURE_GET_PAYLOAD_FILE_ENTITY }; + goto done; + } + + try + { + result = ExtensionManager::Download( + entity, workflowHandle, &Default_ExtensionManager_Download_Options, nullptr); + } + catch (...) + { + result = { .ResultCode = ADUC_Result_Failure, + .ExtendedResultCode = + ADUC_ERC_SWUPDATE_HANDLER_DOWNLOAD_PAYLOAD_FILE_FAILURE_UNKNOWNEXCEPTION }; + } + + workflow_free_file_entity(entity); + entity = nullptr; + + if (IsAducResultCodeFailure(result.ResultCode)) + { + Log_Error("Cannot download payload file#%d. (0x%X)", i, result.ExtendedResultCode); + goto done; + } + } + + // Invoke primary script to download additional files, if required. + result = PerformAction("--action-download", workflowData); + +done: + workflow_free_string(workFolder); + workflow_free_file_entity(entity); + workflow_free_string(installedCriteria); + Log_Info("SWUpdate_Handler download task end."); + return result; +} + +/** + * @brief Install implementation for swupdate. + * Calls into the swupdate wrapper script to install an image file. + * + * @return ADUC_Result The result of the install. + */ +ADUC_Result SWUpdateHandlerImpl::Install(const tagADUC_WorkflowData* workflowData) +{ + ADUC_Result result = PerformAction("--action-install", workflowData); + return result; +} + +/** + * @brief Apply implementation for swupdate. + * Calls into the swupdate wrapper script to perform apply. + * Will flip bootloader flag to boot into update partition for A/B update. + * + * @return ADUC_Result The result of the apply. + */ +ADUC_Result SWUpdateHandlerImpl::Apply(const tagADUC_WorkflowData* workflowData) +{ + ADUC_Result result = { ADUC_Result_Failure }; + char* workFolder = workflow_get_workfolder(workflowData->WorkflowHandle); + Log_Info("Applying data from %s", workFolder); + + // Execute the install command with "-a" to apply the install by telling + // the bootloader to boot to the updated partition. + + // This is equivalent to : command << SWUpdateCommand << " -l " << _logFolder << " -a" + + std::string command = adushconst::adu_shell; + std::vector args{ adushconst::update_type_opt, + adushconst::update_type_microsoft_swupdate, + adushconst::update_action_opt, + adushconst::update_action_apply }; + + args.emplace_back(adushconst::target_log_folder_opt); + + // Note: this implementation of SWUpdate Handler is relying on ADUC_LOG_FOLDER value from build pipeline. + // For GA, we should make this configurable in du-config.json. + args.emplace_back(ADUC_LOG_FOLDER); + + std::string output; + + const int exitCode = ADUC_LaunchChildProcess(command, args, output); + + if (exitCode != 0) + { + Log_Error("Apply failed, extendedResultCode = %d", exitCode); + result = { ADUC_Result_Failure, exitCode }; + goto done; + } + + // Cancellation requested after applied? + if (workflow_get_operation_cancel_requested(workflowData->WorkflowHandle)) + { + Cancel(workflowData); + } + +done: + workflow_free_string(workFolder); + + // Always require a reboot after successful apply + result = { ADUC_Result_Apply_RequiredImmediateReboot }; + + return result; +} + +/** + * @brief Cancel implementation for swupdate. + * We don't have many hooks into swupdate to cancel an ongoing install. + * For A/B update pattern, we can cancel apply by reverting the bootloader flag to boot into the original partition. + * We defer the cancellation decision to the device builder by call into the swupdate wrapper script to cancel apply. + * + * @return ADUC_Result The result of the cancel. + */ +ADUC_Result SWUpdateHandlerImpl::Cancel(const tagADUC_WorkflowData* workflowData) +{ + ADUC_Result result = { .ResultCode = ADUC_Result_Cancel_Success, .ExtendedResultCode = 0 }; + ADUC_WorkflowHandle handle = workflowData->WorkflowHandle; + ADUC_WorkflowHandle stepWorkflowHandle = nullptr; + + const char* workflowId = workflow_peek_id(handle); + int workflowLevel = workflow_get_level(handle); + int workflowStep = workflow_get_step_index(handle); + + Log_Info( + "Requesting cancel operation (workflow id '%s', level %d, step %d).", workflowId, workflowLevel, workflowStep); + if (!workflow_request_cancel(handle)) + { + Log_Error( + "Cancellation request failed. (workflow id '%s', level %d, step %d)", + workflowId, + workflowLevel, + workflowStep); + result.ResultCode = ADUC_Result_Cancel_UnableToCancel; + } + + return result; +} + +/** + * @brief Reads a first line of a file, trims trailing whitespace, and returns as string. + * + * @param filePath Path to the file to read value from. + * @return std::string Returns the value from the file. Returns empty string if there was an error. + */ +/*static*/ +std::string SWUpdateHandlerImpl::ReadValueFromFile(const std::string& filePath) +{ + if (filePath.empty()) + { + Log_Error("Empty file path."); + return std::string{}; + } + + if ((filePath.length()) + 1 > PATH_MAX) + { + Log_Error("Path is too long."); + return std::string{}; + } + + std::ifstream file(filePath); + if (!file.is_open()) + { + Log_Error("File %s failed to open, error: %d", filePath.c_str(), errno); + return std::string{}; + } + + std::string result; + std::getline(file, result); + if (file.bad()) + { + Log_Error("Unable to read from file %s, error: %d", filePath.c_str(), errno); + return std::string{}; + } + + // Trim whitespace + ADUC::StringUtils::Trim(result); + return result; +} + +/** + * @brief Check whether the current device state match all desired state in workflow data. + * + * Key concepts: + * - Decouple the business logics from device-specific configurations. + * - Device builder defines the how to evaluate wither the current step can be considered 'completed'. + * Note that the term 'IsInstalled' was carried over from the original design where the agent would + * ask the handler that "Is an 'update' is currently installed on the device.". + * - + * @return ADUC_Result The result based on evaluating the workflow data. + */ +ADUC_Result SWUpdateHandlerImpl::IsInstalled(const tagADUC_WorkflowData* workflowData) +{ + ADUC_Result result = SWUpdate_Handler_DownloadScriptFile(workflowData->WorkflowHandle); + if (IsAducResultCodeSuccess(result.ResultCode)) + { + result = PerformAction("--action-is-installed", workflowData); + } + return result; +} + +/** + * @brief Reads handler configuration from @p configFile + * + * @param values An output dictionary containing handler configurations. + * @return ADUC_Result + */ +ADUC_Result +SWUpdateHandlerImpl::ReadConfig(const std::string& configFile, std::unordered_map& values) +{ + // Fill in the output map. + AutoFreeJsonValue_t rootValue{ json_parse_file(configFile.c_str()) }; + if (!rootValue) + { + return ADUC_Result{ .ResultCode = ADUC_Result_Failure, + .ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_BAD_SWUPDATE_CONFIG_FILE }; + } + + JSON_Object* rootObject = json_value_get_object(rootValue.get()); + + for (size_t i = 0; i < json_object_get_count(rootObject); i++) + { + const char* name = json_object_get_name(rootObject, i); + const char* val = json_value_get_string(json_object_get_value_at(rootObject, i)); + values[name] = val; + } + + return ADUC_Result{ .ResultCode = ADUC_Result_Success, .ExtendedResultCode = 0 }; +} + +/** + * @brief A helper function that return a command file path, and arguments list. + * + * @param workflowHandle A workflow data containing update information and selected component. + * @param resultFilePath A full path of the file containing serialized ADUC_Result value returned by the command. + * @param[out] commandFilePath A output command file path. + * @param[out] args An output command arguments list. + * @return ADUC_Result + */ +ADUC_Result SWUpdateHandlerImpl::PrepareCommandArguments( + ADUC_WorkflowHandle workflowHandle, + std::string resultFilePath, + std::string workFolder, + std::string& commandFilePath, + std::vector& args) +{ + ADUC_Result result = { ADUC_GeneralResult_Failure }; + bool isComponentsAware = false; + ADUC_FileEntity* scriptFileEntity = nullptr; + + const char* selectedComponentsJson = nullptr; + JSON_Value* selectedComponentsValue = nullptr; + JSON_Object* selectedComponentsObject = nullptr; + JSON_Array* componentsArray = nullptr; + int componentCount = 0; + JSON_Object* component = nullptr; + + std::string fileArgs; + std::vector argumentList; + std::unordered_map swupdateConfigs; + + std::stringstream filePath; + std::stringstream swuFilePath; + const char* scriptFileName = nullptr; + const char* swuFileName = nullptr; + + char* installedCriteria = nullptr; + const char* arguments = nullptr; + + bool success = false; + if (workflowHandle == nullptr) + { + result.ExtendedResultCode = ADUC_ERC_UPDATE_CONTENT_HANDLER_INSTALL_FAILURE_NULL_WORKFLOW; + goto done; + } + + installedCriteria = workflow_get_installed_criteria(workflowHandle); + + // Parse componenets list. If the list is empty, nothing to download. + selectedComponentsJson = workflow_peek_selected_components(workflowHandle); + + if (!IsNullOrEmpty(selectedComponentsJson)) + { + isComponentsAware = true; + selectedComponentsValue = json_parse_string(selectedComponentsJson); + if (selectedComponentsValue == nullptr) + { + result.ExtendedResultCode = ADUC_ERC_UPDATE_CONTENT_HANDLER_INSTALL_FAILURE_MISSING_PRIMARY_COMPONENT; + goto done; + } + + selectedComponentsObject = json_value_get_object(selectedComponentsValue); + componentsArray = json_object_get_array(selectedComponentsObject, "components"); + if (componentsArray == nullptr) + { + result.ExtendedResultCode = ADUC_ERC_UPDATE_CONTENT_HANDLER_INSTALL_FAILURE_MISSING_PRIMARY_COMPONENT; + goto done; + } + + // Prepare target component info. + componentCount = json_array_get_count(componentsArray); + + if (componentCount <= 0) + { + result.ResultCode = ADUC_Result_Download_Skipped_NoMatchingComponents; + goto done; + } + + if (componentCount > 1) + { + Log_Error("Expecting only 1 component, but got %d.", componentCount); + result.ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_TOO_MANY_COMPONENTS; + } + + component = json_array_get_object(componentsArray, 0); + if (component == nullptr) + { + result.ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_INVALID_COMPONENTS_DATA; + goto done; + } + } + + // Prepare main script file info. + scriptFileName = + workflow_peek_update_manifest_handler_properties_string(workflowHandle, HANDLER_PROPERTIES_SCRIPT_FILENAME); + if (IsNullOrEmpty(scriptFileName)) + { + result.ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_MISSING_SCRIPT_FILE_NAME; + workflow_set_result_details(workflowHandle, "Missing 'handlerProperies.scriptFileName' property"); + goto done; + } + + filePath << workFolder.c_str() << "/" << scriptFileName; + commandFilePath = filePath.str(); + + // swu file. + swuFileName = + workflow_peek_update_manifest_handler_properties_string(workflowHandle, HANDLER_PROPERTIES_SWU_FILENAME); + if (IsNullOrEmpty(swuFileName)) + { + result.ExtendedResultCode = ADUC_ERC_SWUPDATE_HANDLER_MISSING_SWU_FILE_NAME; + workflow_set_result_details(workflowHandle, "Missing 'handlerProperies.swuFileName' property"); + goto done; + } + + swuFilePath << workFolder.c_str() << "/" << swuFileName; + + args.emplace_back("--swu-file"); + args.emplace_back(swuFilePath.str()); + + // + // Prepare command-line arguments. + // + + // Read arguments from swupdate_handler_config.json + result = ReadConfig(ADUC_SWUPDATE_HANDLER_CONF_FILE_PATH, swupdateConfigs); + if (IsAducResultCodeSuccess(result.ResultCode)) + { + for (const auto& a : swupdateConfigs) + { + args.emplace_back(a.first); + args.emplace_back(a.second); + } + } + + // Add customer specified arguments first. + arguments = workflow_peek_update_manifest_handler_properties_string(workflowHandle, "arguments"); + if (arguments == nullptr) + { + arguments = ""; + } + fileArgs = arguments; + + Log_Info("Parsing handlerProperties.arguments: %s", arguments); + argumentList = ADUC::StringUtils::Split(fileArgs, ' '); + for (int i = 0; i < argumentList.size(); i++) + { + const std::string argument = argumentList[i]; + if (!argument.empty()) + { + if (argument == "--component-id-val" || argument == "${du_component_id}") + { + const char* val = json_object_get_string(component, "id"); + if (val != nullptr) + { + args.emplace_back(val); + } + else + { + args.emplace_back("n/a"); + } + } + else if (argument == "--component-name-val" || argument == "${du_component_name}") + { + const char* val = json_object_get_string(component, "name"); + if (val != nullptr) + { + args.emplace_back(val); + } + else + { + args.emplace_back("n/a"); + } + } + else if (argument == "--component-manufacturer-val" || argument == "${du_component_manufacturer}") + { + const char* val = json_object_get_string(component, "manufacturer"); + if (val != nullptr) + { + args.emplace_back(val); + } + else + { + args.emplace_back("n/a"); + } + } + else if (argument == "--component-model-val" || argument == "${du_component_model}") + { + const char* val = json_object_get_string(component, "model"); + if (val != nullptr) + { + args.emplace_back(val); + } + else + { + args.emplace_back("n/a"); + } + } + else if (argument == "--component-version-val" || argument == "${du_component_version}") + { + const char* val = json_object_get_string(component, "version"); + if (val != nullptr) + { + args.emplace_back(val); + } + else + { + args.emplace_back("n/a"); + } + } + else if (argument == "--component-group-val" || argument == "${du_component_group}") + { + const char* val = json_object_get_string(component, "group"); + if (val != nullptr) + { + args.emplace_back(val); + } + else + { + args.emplace_back("n/a"); + } + } + else if (argument == "--component-prop-val" || argument == "${du_component_prop}") + { + if (i < argumentList.size() - 1) + { + std::string propertyPath = "properties."; + propertyPath += argumentList[i + 1]; + const char* val = json_object_dotget_string(component, propertyPath.c_str()); + if (val != nullptr) + { + args.emplace_back(val); + } + i++; + } + else + { + args.emplace_back("n/a"); + } + } + else + { + args.emplace_back(argument); + } + } + } + + // Default options. + args.emplace_back("--workfolder"); + args.emplace_back(workFolder); + + args.emplace_back("--result-file"); + args.emplace_back(resultFilePath); + + args.emplace_back("--installed-criteria"); + args.emplace_back(installedCriteria); + + result = { ADUC_Result_Success }; + +done: + if (selectedComponentsValue != nullptr) + { + json_value_free(selectedComponentsValue); + } + + workflow_free_string(installedCriteria); + return result; +} + +ADUC_Result SWUpdateHandlerImpl::PerformAction(const std::string& action, const tagADUC_WorkflowData* workflowData) +{ + std::string scriptFilePath; + std::vector args; + std::vector commandLineArgs; + std::string scriptOutput; + + return SWUpdateHandler_PerformAction( + action, workflowData, false, scriptFilePath, args, commandLineArgs, scriptOutput); +} + +/** + * @brief Helper function to perform cancel when we are doing an apply. + * + * @return ADUC_Result The result of the cancel. + */ +static ADUC_Result CancelApply(const char* logFolder) +{ + // Execute the install command with "-r" to reverts the apply by + // telling the bootloader to boot into the current partition + + // This is equivalent to : command << c_installScript << " -l " << logFolder << " -r" + + std::string command = adushconst::adu_shell; + std::vector args{ adushconst::update_type_opt, adushconst::update_type_microsoft_swupdate, + adushconst::update_action_opt, adushconst::update_action_apply, + adushconst::target_log_folder_opt, logFolder }; + + std::string output; + + const int exitCode = ADUC_LaunchChildProcess(command, args, output); + if (exitCode != 0) + { + // If failed to cancel apply, apply should return SuccessRebootRequired. + Log_Error("Failed to cancel Apply, extendedResultCode = %d", exitCode); + return ADUC_Result{ ADUC_Result_Failure, exitCode }; + } + + Log_Info("Apply was cancelled"); + return ADUC_Result{ ADUC_Result_Failure_Cancelled }; +} + +/** + * @brief Backup implementation for swupdate V2. + * Calls into the swupdate wrapper script to perform backup. + * For swupdate, no operation is required. + * + * @return ADUC_Result The result of the backup. + * It will always return ADUC_Result_Backup_Success. + */ +ADUC_Result SWUpdateHandlerImpl::Backup(const tagADUC_WorkflowData* workflowData) +{ + ADUC_Result result = { ADUC_Result_Backup_Success }; + Log_Info("Swupdate doesn't require a specific operation to backup. (no-op) "); + return result; +} + +/** + * @brief Restore implementation for swupdate V2. + * Calls into the swupdate wrapper script to perform restore. + * Will flip bootloader flag to boot into the previous partition for A/B update. + * + * @return ADUC_Result The result of the restore. + */ +ADUC_Result SWUpdateHandlerImpl::Restore(const tagADUC_WorkflowData* workflowData) +{ + ADUC_Result result = { ADUC_Result_Restore_Success }; + ADUC_Result cancel_result = CancelApply(ADUC_LOG_FOLDER); + if (cancel_result.ResultCode != ADUC_Result_Failure_Cancelled) + { + result = { .ResultCode = ADUC_Result_Failure, + .ExtendedResultCode = ADUC_ERC_UPPERLEVEL_WORKFLOW_FAILED_RESTORE_FAILED }; + } + return result; +} diff --git a/src/extensions/step_handlers/swupdate_handler_v2/tests/CMakeLists.txt b/src/extensions/step_handlers/swupdate_handler_v2/tests/CMakeLists.txt new file mode 100644 index 000000000..abb1e2898 --- /dev/null +++ b/src/extensions/step_handlers/swupdate_handler_v2/tests/CMakeLists.txt @@ -0,0 +1,54 @@ +cmake_minimum_required (VERSION 3.5) + +project (swupdate_handler_unit_tests) + +include (agentRules) + +compileasc99 () +disablertti () + +set ( + sources + main.cpp + swupdate_handler_v2_ut.cpp + ../src/handler_create.cpp + ../src/swupdate_handler_v2.cpp + ../../../update_manifest_handlers/steps_handler/src/steps_handler.cpp) + +find_package (Catch2 REQUIRED) + +add_executable (${PROJECT_NAME} ${sources}) + +target_include_directories ( + ${PROJECT_NAME} + PRIVATE ${ADUC_EXPORT_INCLUDES} + ${ADU_EXTENSION_INCLUDES} + ${ADU_SHELL_INCLUDES} + ${PROJECT_SOURCE_DIR}/inc + ${PROJECT_SOURCE_DIR}/../inc + ${PROJECT_SOURCE_DIR}/../../inc + ${PROJECT_SOURCE_DIR}/../../../update_manifest_handlers/steps_handler/inc) + +target_compile_definitions ( + ${PROJECT_NAME} + PRIVATE ADUC_SWUPDATE_HANDLER_CONF_FILE_PATH="${ADUC_SWUPDATE_HANDLER_CONF_FILE_PATH}") + +target_link_libraries ( + ${PROJECT_NAME} + PRIVATE aduc::contract_utils + aduc::c_utils + aduc::exception_utils + aduc::extension_manager + aduc::process_utils + aduc::string_utils + aduc::system_utils + aduc::workflow_data_utils + aduc::workflow_utils + Catch2::Catch2) + +# Ensure that ctest discovers catch2 tests. +# Use catch_discover_tests() rather than add_test() +# See https://github.com/catchorg/Catch2/blob/master/contrib/Catch.cmake +include (CTest) +include (Catch) +catch_discover_tests (${PROJECT_NAME}) diff --git a/src/diagnostics_component/diagnostics_workflow/tests/main.cpp b/src/extensions/step_handlers/swupdate_handler_v2/tests/main.cpp similarity index 74% rename from src/diagnostics_component/diagnostics_workflow/tests/main.cpp rename to src/extensions/step_handlers/swupdate_handler_v2/tests/main.cpp index 27b2ca7f6..f46269436 100644 --- a/src/diagnostics_component/diagnostics_workflow/tests/main.cpp +++ b/src/extensions/step_handlers/swupdate_handler_v2/tests/main.cpp @@ -1,9 +1,10 @@ /** * @file main.cpp - * @brief diagnostics_workflow_ut tests main entry point. + * @brief main for SWUpdate handler unit tests * * @copyright Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + #define CATCH_CONFIG_MAIN #include diff --git a/src/extensions/step_handlers/swupdate_handler_v2/tests/swupdate_handler_v2_ut.cpp b/src/extensions/step_handlers/swupdate_handler_v2/tests/swupdate_handler_v2_ut.cpp new file mode 100644 index 000000000..85b740a4c --- /dev/null +++ b/src/extensions/step_handlers/swupdate_handler_v2/tests/swupdate_handler_v2_ut.cpp @@ -0,0 +1,440 @@ +/** + * @file swupdate_handler_v2_ut.cpp + * @brief SWUpdate handler unit tests + * + * @copyright Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +#include "aduc/extension_manager.hpp" +#include "aduc/process_utils.hpp" +#include "aduc/swupdate_handler_v2.hpp" +#include "aduc/system_utils.h" +#include "aduc/workflow_utils.h" + +#include +using Catch::Matchers::Equals; + +#include +#include + +EXTERN_C_BEGIN + +ContentHandler* CreateUpdateContentHandlerExtension(ADUC_LOG_SEVERITY logLevel); + +EXTERN_C_END + +ADUC_Result SWUpdateHandler_PerformAction( + const std::string& action, + const tagADUC_WorkflowData* workflowData, + bool prepareArgsOnly, + std::string& scriptFilePath, + std::vector& args, + std::vector& commandLineArgs, + std::string& scriptOutput); + +ADUC_Result PrepareStepsWorkflowDataObject(ADUC_WorkflowHandle handle); + +// clang-format off +const char* filecopy_workflow = + R"( { )" + R"( "workflow": { )" + R"( "action": 3, )" + R"( "id": "d19de7fb-11d8-45f7-88e0-03872a591de8" )" + R"( }, )" + R"( "updateManifest": "{\"manifestVersion\":\"4\",\"updateId\":{\"provider\":\"Contoso\",\"name\":\"Virtual-Vacuum\",\"version\":\"30.0\"},\"compatibility\":[{\"deviceManufacturer\":\"contoso\",\"deviceModel\":\"virtual-vacuum-v1\"}],\"instructions\":{\"steps\":[{\"handler\":\"microsoft/swupdate:2\",\"files\":[\"fb7f654eb03c9900a\",\"ff2510f75ca8bf0d3\"],\"handlerProperties\":{\"installedCriteria\":\"grep '^This is swupdate filecopy test version 1.0$' /usr/local/du/tests/swupdate-filecopy-test/mock-update-for-file-copy-test-1.txt\",\"scriptFileName\":\"example-du-swupdate-script.sh\",\"swuFileName\":\"du-agent-swupdate-filecopy-test-1_1.0.swu\"}}]},\"files\":{\"fb7f654eb03c9900a\":{\"fileName\":\"du-agent-swupdate-filecopy-test-1_1.0.swu\",\"sizeInBytes\":1536,\"hashes\":{\"sha256\":\"cWJKtVffvDj9B78lgCqWT/lKMBJ9AQ8UmUh48ad8JHA=\"}},\"ff2510f75ca8bf0d3\":{\"fileName\":\"example-du-swupdate-script.sh\",\"sizeInBytes\":24737,\"hashes\":{\"sha256\":\"Nc08FK/T5bOH07nC4GorKTgope5n3+cyb+Ar6KGaY9I=\"}}},\"createdDateTime\":\"2022-03-28T22:36:07.8445392Z\"}", )" + R"( "updateManifestSignature": "eyJhbGciOiJSUzI1NiIsInNqd2siOiJleUpoYkdjaU9pSlNVekkxTmlJc0ltdHBaQ0k2SWtGRVZTNHlNREEzTURJdVVpSjkuZXlKcmRIa2lPaUpTVTBFaUxDSnVJam9pYkV4bWMwdHZPRmwwWW1Oak1sRXpUalV3VlhSTVNXWlhVVXhXVTBGRlltTm9LMFl2WTJVM1V6Rlpja3BvV0U5VGNucFRaa051VEhCVmFYRlFWSGMwZWxndmRHbEJja0ZGZFhrM1JFRmxWVzVGU0VWamVEZE9hM2QzZVRVdk9IcExaV3AyWTBWWWNFRktMMlV6UWt0SE5FVTBiMjVtU0ZGRmNFOXplSGRQUzBWbFJ6QkhkamwzVjB3emVsUmpUblprUzFoUFJGaEdNMVZRWlVveGIwZGlVRkZ0Y3pKNmJVTktlRUppZEZOSldVbDBiWFpwWTNneVpXdGtWbnBYUm5jdmRrdFVUblZMYXpob2NVczNTRkptYWs5VlMzVkxXSGxqSzNsSVVVa3dZVVpDY2pKNmEyc3plR2d4ZEVWUFN6azRWMHBtZUdKamFsQnpSRTgyWjNwWmVtdFlla05OZW1Fd1R6QkhhV0pDWjB4QlZGUTVUV1k0V1ZCd1dVY3lhblpQWVVSVmIwTlJiakpWWTFWU1RtUnNPR2hLWW5scWJscHZNa3B5SzFVNE5IbDFjVTlyTjBZMFdubFRiMEoyTkdKWVNrZ3lXbEpTV2tab0wzVlRiSE5XT1hkU2JWbG9XWEoyT1RGRVdtbHhhemhJVWpaRVUyeHVabTVsZFRJNFJsUm9SVzF0YjNOVlRUTnJNbGxNYzBKak5FSnZkWEIwTTNsaFNEaFpia3BVTnpSMU16TjFlakU1TDAxNlZIVnFTMmMzVkdGcE1USXJXR0owYmxwRU9XcFVSMkY1U25Sc2FFWmxWeXRJUXpVM1FYUkJSbHBvY1ZsM2VVZHJXQ3M0TTBGaFVGaGFOR0V4VHpoMU1qTk9WVWQxTWtGd04yOU5NVTR3ZVVKS0swbHNUM29pTENKbElqb2lRVkZCUWlJc0ltRnNaeUk2SWxKVE1qVTJJaXdpYTJsa0lqb2lRVVJWTGpJeE1EWXdPUzVTTGxNaWZRLlJLS2VBZE02dGFjdWZpSVU3eTV2S3dsNFpQLURMNnEteHlrTndEdkljZFpIaTBIa2RIZ1V2WnoyZzZCTmpLS21WTU92dXp6TjhEczhybXo1dnMwT1RJN2tYUG1YeDZFLUYyUXVoUXNxT3J5LS1aN2J3TW5LYTNkZk1sbkthWU9PdURtV252RWMyR0hWdVVTSzREbmw0TE9vTTQxOVlMNThWTDAtSEthU18xYmNOUDhXYjVZR08xZXh1RmpiVGtIZkNIU0duVThJeUFjczlGTjhUT3JETHZpVEtwcWtvM3RiSUwxZE1TN3NhLWJkZExUVWp6TnVLTmFpNnpIWTdSanZGbjhjUDN6R2xjQnN1aVQ0XzVVaDZ0M05rZW1UdV9tZjdtZUFLLTBTMTAzMFpSNnNTR281azgtTE1sX0ZaUmh4djNFZFNtR2RBUTNlMDVMRzNnVVAyNzhTQWVzWHhNQUlHWmcxUFE3aEpoZGZHdmVGanJNdkdTSVFEM09wRnEtZHREcEFXbUo2Zm5sZFA1UWxYek5tQkJTMlZRQUtXZU9BYjh0Yjl5aVhsemhtT1dLRjF4SzlseHpYUG9GNmllOFRUWlJ4T0hxTjNiSkVISkVoQmVLclh6YkViV2tFNm4zTEoxbkd5M1htUlVFcER0Umdpa0tBUzZybFhFT0VneXNjIn0.eyJzaGEyNTYiOiJheEhUZkdEa2ZVd0dYMnR2SmpxTmhzU3BDYmtyNVpEcXBQVFd4aE9jN2RnPSJ9.ZilWZQSDM59SFpoqpKk33pp9StovL03E9bGACRrfsdPOCXDSqmGBtQxmztg70BTAVpiH7kMlYj1g--no54STJn8_nvt82LX5HEj1xosypdMVIgsAPzhd8RhDKE8T7agrdR4c46PfephjvL7jLRFJN4ipaQIcMxHYaiMeV4KdHXzf-LMASU0tX_y_eGyEIKLNu5kgGnigu96f7JpQ4cgSq5ScZPqzkHutgsgFKG5pY5lefbxJjlepL5N82Bvwu_ZFkCWvo1YSdpMP4heP10xXiq2GIy3bN0yZHjMOIMt-f8jtLmZV7qEblkym6gmrYJENDjAe2rwh6q7ohGb5u_VtrignqV2ZSJobr4ENSBtCNT6Gtm0ZucQghvdEQ0iyM_XQfmDH2AnW_vqt1ymQYkn8HXV5zoeuse6ly4B8L_SzxQei0wZJcyXY61FarIxSth6qEq9my7Hvv8YAnTSp9tEZMSY9j6jYqryF1EV79sIobczkTIe6k1t_4d_xj8roleTf", )" + R"( "fileUrls": { )" + R"( "fb7f654eb03c9900a": "http://duinstance2--wewilair.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/4d823623494d4a62b7877d58d0d89167/du-agent-swupdate-filecopy-test-1_1.0.swu", )" + R"( "ff2510f75ca8bf0d3": "http://duinstance2--wewilair.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/5daa1107aee443b095f0ac6a4548f4b0/example-du-swupdate-script.sh" )" + R"( } )" + R"( } )"; + +const char* filecopy_workflow_2 = + R"( { )" + R"( "workflow": { )" + R"( "action": 3, )" + R"( "id": "d19de7fb-11d8-45f7-88e0-03872a591de8" )" + R"( }, )" + R"( "updateManifest": "{\"manifestVersion\":\"4\",\"updateId\":{\"provider\":\"Contoso\",\"name\":\"Virtual-Vacuum\",\"version\":\"30.0\"},\"compatibility\":[{\"deviceManufacturer\":\"contoso\",\"deviceModel\":\"virtual-vacuum-v1\"}],\"instructions\":{\"steps\":[{\"handler\":\"microsoft/swupdate:2\",\"files\":[\"fb7f654eb03c9900a\",\"ff2510f75ca8bf0d3\"],\"handlerProperties\":{\"installedCriteria\":\"This is swupdate filecopy test version 1.0\",\"arguments\":\"--software-version-file /tmp/adu/testdata/test-device/vacuum-1/data/mock-update-for-file-copy-test-1.txt\",\"scriptFileName\":\"example-du-swupdate-script.sh\",\"swuFileName\":\"du-agent-swupdate-filecopy-test-1_1.0.swu\"}}]},\"files\":{\"fb7f654eb03c9900a\":{\"fileName\":\"du-agent-swupdate-filecopy-test-1_1.0.swu\",\"sizeInBytes\":1536,\"hashes\":{\"sha256\":\"cWJKtVffvDj9B78lgCqWT/lKMBJ9AQ8UmUh48ad8JHA=\"}},\"ff2510f75ca8bf0d3\":{\"fileName\":\"example-du-swupdate-script.sh\",\"sizeInBytes\":24737,\"hashes\":{\"sha256\":\"Nc08FK/T5bOH07nC4GorKTgope5n3+cyb+Ar6KGaY9I=\"}}},\"createdDateTime\":\"2022-03-28T22:36:07.8445392Z\"}", )" + R"( "updateManifestSignature": "eyJhbGciOiJSUzI1NiIsInNqd2siOiJleUpoYkdjaU9pSlNVekkxTmlJc0ltdHBaQ0k2SWtGRVZTNHlNREEzTURJdVVpSjkuZXlKcmRIa2lPaUpTVTBFaUxDSnVJam9pYkV4bWMwdHZPRmwwWW1Oak1sRXpUalV3VlhSTVNXWlhVVXhXVTBGRlltTm9LMFl2WTJVM1V6Rlpja3BvV0U5VGNucFRaa051VEhCVmFYRlFWSGMwZWxndmRHbEJja0ZGZFhrM1JFRmxWVzVGU0VWamVEZE9hM2QzZVRVdk9IcExaV3AyWTBWWWNFRktMMlV6UWt0SE5FVTBiMjVtU0ZGRmNFOXplSGRQUzBWbFJ6QkhkamwzVjB3emVsUmpUblprUzFoUFJGaEdNMVZRWlVveGIwZGlVRkZ0Y3pKNmJVTktlRUppZEZOSldVbDBiWFpwWTNneVpXdGtWbnBYUm5jdmRrdFVUblZMYXpob2NVczNTRkptYWs5VlMzVkxXSGxqSzNsSVVVa3dZVVpDY2pKNmEyc3plR2d4ZEVWUFN6azRWMHBtZUdKamFsQnpSRTgyWjNwWmVtdFlla05OZW1Fd1R6QkhhV0pDWjB4QlZGUTVUV1k0V1ZCd1dVY3lhblpQWVVSVmIwTlJiakpWWTFWU1RtUnNPR2hLWW5scWJscHZNa3B5SzFVNE5IbDFjVTlyTjBZMFdubFRiMEoyTkdKWVNrZ3lXbEpTV2tab0wzVlRiSE5XT1hkU2JWbG9XWEoyT1RGRVdtbHhhemhJVWpaRVUyeHVabTVsZFRJNFJsUm9SVzF0YjNOVlRUTnJNbGxNYzBKak5FSnZkWEIwTTNsaFNEaFpia3BVTnpSMU16TjFlakU1TDAxNlZIVnFTMmMzVkdGcE1USXJXR0owYmxwRU9XcFVSMkY1U25Sc2FFWmxWeXRJUXpVM1FYUkJSbHBvY1ZsM2VVZHJXQ3M0TTBGaFVGaGFOR0V4VHpoMU1qTk9WVWQxTWtGd04yOU5NVTR3ZVVKS0swbHNUM29pTENKbElqb2lRVkZCUWlJc0ltRnNaeUk2SWxKVE1qVTJJaXdpYTJsa0lqb2lRVVJWTGpJeE1EWXdPUzVTTGxNaWZRLlJLS2VBZE02dGFjdWZpSVU3eTV2S3dsNFpQLURMNnEteHlrTndEdkljZFpIaTBIa2RIZ1V2WnoyZzZCTmpLS21WTU92dXp6TjhEczhybXo1dnMwT1RJN2tYUG1YeDZFLUYyUXVoUXNxT3J5LS1aN2J3TW5LYTNkZk1sbkthWU9PdURtV252RWMyR0hWdVVTSzREbmw0TE9vTTQxOVlMNThWTDAtSEthU18xYmNOUDhXYjVZR08xZXh1RmpiVGtIZkNIU0duVThJeUFjczlGTjhUT3JETHZpVEtwcWtvM3RiSUwxZE1TN3NhLWJkZExUVWp6TnVLTmFpNnpIWTdSanZGbjhjUDN6R2xjQnN1aVQ0XzVVaDZ0M05rZW1UdV9tZjdtZUFLLTBTMTAzMFpSNnNTR281azgtTE1sX0ZaUmh4djNFZFNtR2RBUTNlMDVMRzNnVVAyNzhTQWVzWHhNQUlHWmcxUFE3aEpoZGZHdmVGanJNdkdTSVFEM09wRnEtZHREcEFXbUo2Zm5sZFA1UWxYek5tQkJTMlZRQUtXZU9BYjh0Yjl5aVhsemhtT1dLRjF4SzlseHpYUG9GNmllOFRUWlJ4T0hxTjNiSkVISkVoQmVLclh6YkViV2tFNm4zTEoxbkd5M1htUlVFcER0Umdpa0tBUzZybFhFT0VneXNjIn0.eyJzaGEyNTYiOiJheEhUZkdEa2ZVd0dYMnR2SmpxTmhzU3BDYmtyNVpEcXBQVFd4aE9jN2RnPSJ9.ZilWZQSDM59SFpoqpKk33pp9StovL03E9bGACRrfsdPOCXDSqmGBtQxmztg70BTAVpiH7kMlYj1g--no54STJn8_nvt82LX5HEj1xosypdMVIgsAPzhd8RhDKE8T7agrdR4c46PfephjvL7jLRFJN4ipaQIcMxHYaiMeV4KdHXzf-LMASU0tX_y_eGyEIKLNu5kgGnigu96f7JpQ4cgSq5ScZPqzkHutgsgFKG5pY5lefbxJjlepL5N82Bvwu_ZFkCWvo1YSdpMP4heP10xXiq2GIy3bN0yZHjMOIMt-f8jtLmZV7qEblkym6gmrYJENDjAe2rwh6q7ohGb5u_VtrignqV2ZSJobr4ENSBtCNT6Gtm0ZucQghvdEQ0iyM_XQfmDH2AnW_vqt1ymQYkn8HXV5zoeuse6ly4B8L_SzxQei0wZJcyXY61FarIxSth6qEq9my7Hvv8YAnTSp9tEZMSY9j6jYqryF1EV79sIobczkTIe6k1t_4d_xj8roleTf", )" + R"( "fileUrls": { )" + R"( "fb7f654eb03c9900a": "http://duinstance2--wewilair.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/4d823623494d4a62b7877d58d0d89167/du-agent-swupdate-filecopy-test-1_1.0.swu", )" + R"( "ff2510f75ca8bf0d3": "http://duinstance2--wewilair.b.nlu.dl.adu.microsoft.com/westus2/duinstance2/5daa1107aee443b095f0ac6a4548f4b0/example-du-swupdate-script.sh" )" + R"( } )" + R"( } )"; + +// clang-format on + +/** + * @brief Reads ADUC_Result data from @p resultFile + * + * @param resultFile Path to a file contains serialized json string of an ADUC_Result data. + * @param result Output result + * @return true If success. + */ +bool ReadResultFile(const char* resultFile, ADUC_Result* result) +{ + if (result == nullptr) + { + return false; + } + + JSON_Value* actionResultValue = json_parse_file(resultFile); + if (actionResultValue == nullptr) + { + return false; + } + + JSON_Object* actionResultObject = json_object(actionResultValue); + result->ResultCode = json_object_get_number(actionResultObject, "resultCode"); + result->ExtendedResultCode = json_object_get_number(actionResultObject, "extendedResultCode"); + json_value_free(actionResultValue); + return true; +} + +TEST_CASE("SWUpdate Prepare Arguments Test") +{ + ContentHandler* swupdateHandler = CreateUpdateContentHandlerExtension(ADUC_LOG_DEBUG); + CHECK(swupdateHandler != nullptr); + ExtensionManager::SetUpdateContentHandlerExtension("microsoft/swupdate:2", swupdateHandler); + + // Create test workflow data. + ADUC_WorkflowHandle handle = nullptr; + ADUC_Result result = workflow_init(filecopy_workflow, false, &handle); + + CHECK(result.ResultCode != 0); + CHECK(result.ExtendedResultCode == 0); + + auto filecount = workflow_get_update_files_count(handle); + REQUIRE(filecount == 2); + + result = PrepareStepsWorkflowDataObject(handle); + CHECK(result.ResultCode != 0); + + int childCount = workflow_get_children_count(handle); + CHECK(childCount == 1); + + ADUC_WorkflowHandle stepHandle = workflow_get_child(handle, 0); + CHECK(stepHandle != nullptr); + + // Dummy workflow to hold a childHandle. + ADUC_WorkflowData stepWorkflow = {}; + stepWorkflow.WorkflowHandle = stepHandle; + + std::string scriptFilePath; + std::vector args; + std::vector commandLineArgs; + std::string scriptOutput; + + result = SWUpdateHandler_PerformAction( + "--action-install", &stepWorkflow, true, scriptFilePath, args, commandLineArgs, scriptOutput); + + CHECK(result.ResultCode != 0); + CHECK(result.ExtendedResultCode == 0); + + CHECK_THAT( + scriptFilePath, + Equals("/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8/example-du-swupdate-script.sh")); + CHECK_THAT( + scriptOutput, + Equals( + R"( --update-type "microsoft/script" --update-action "execute" --target-data "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8/example-du-swupdate-script.sh" --target-options --action-install --target-options --swu-file --target-options "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8/du-agent-swupdate-filecopy-test-1_1.0.swu" --target-options --workfolder --target-options "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8" --target-options --result-file --target-options "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8/aduc_result.json" --target-options --installed-criteria --target-options "grep '^This is swupdate filecopy test version 1.0$' /usr/local/du/tests/swupdate-filecopy-test/mock-update-for-file-copy-test-1.txt")")); + args.clear(); + + result = SWUpdateHandler_PerformAction( + "--action-apply", &stepWorkflow, true, scriptFilePath, args, commandLineArgs, scriptOutput); + + CHECK(result.ResultCode != 0); + CHECK(result.ExtendedResultCode == 0); + + CHECK_THAT( + scriptOutput, + Equals( + R"( --update-type "microsoft/script" --update-action "execute" --target-data "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8/example-du-swupdate-script.sh" --target-options --action-apply --target-options --swu-file --target-options "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8/du-agent-swupdate-filecopy-test-1_1.0.swu" --target-options --workfolder --target-options "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8" --target-options --result-file --target-options "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8/aduc_result.json" --target-options --installed-criteria --target-options "grep '^This is swupdate filecopy test version 1.0$' /usr/local/du/tests/swupdate-filecopy-test/mock-update-for-file-copy-test-1.txt")")); + args.clear(); + + result = SWUpdateHandler_PerformAction( + "--action-cancel", &stepWorkflow, true, scriptFilePath, args, commandLineArgs, scriptOutput); + + CHECK(result.ResultCode != 0); + CHECK(result.ExtendedResultCode == 0); + + CHECK_THAT( + scriptOutput, + Equals( + R"( --update-type "microsoft/script" --update-action "execute" --target-data "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8/example-du-swupdate-script.sh" --target-options --action-cancel --target-options --swu-file --target-options "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8/du-agent-swupdate-filecopy-test-1_1.0.swu" --target-options --workfolder --target-options "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8" --target-options --result-file --target-options "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8/aduc_result.json" --target-options --installed-criteria --target-options "grep '^This is swupdate filecopy test version 1.0$' /usr/local/du/tests/swupdate-filecopy-test/mock-update-for-file-copy-test-1.txt")")); + args.clear(); + + result = SWUpdateHandler_PerformAction( + "--action-is-installed", &stepWorkflow, true, scriptFilePath, args, commandLineArgs, scriptOutput); + + CHECK(result.ResultCode != 0); + CHECK(result.ExtendedResultCode == 0); + + CHECK_THAT( + scriptOutput, + Equals( + R"( --update-type "microsoft/script" --update-action "execute" --target-data "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8/example-du-swupdate-script.sh" --target-options --action-is-installed --target-options --swu-file --target-options "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8/du-agent-swupdate-filecopy-test-1_1.0.swu" --target-options --workfolder --target-options "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8" --target-options --result-file --target-options "/var/lib/adu/downloads/d19de7fb-11d8-45f7-88e0-03872a591de8/aduc_result.json" --target-options --installed-criteria --target-options "grep '^This is swupdate filecopy test version 1.0$' /usr/local/du/tests/swupdate-filecopy-test/mock-update-for-file-copy-test-1.txt")")); + args.clear(); + + ExtensionManager::Uninit(); +} + +TEST_CASE("SWUpdate sample script --action-is-installed") +{ + ContentHandler* swupdateHandler = CreateUpdateContentHandlerExtension(ADUC_LOG_DEBUG); + CHECK(swupdateHandler != nullptr); + ExtensionManager::SetUpdateContentHandlerExtension("microsoft/swupdate:2", swupdateHandler); + + ADUC_SystemUtils_RmDirRecursive("/tmp/adu/testdata/test-device"); + + // Create test workflow data. + ADUC_WorkflowHandle handle = nullptr; + ADUC_Result result = workflow_init(filecopy_workflow_2, false, &handle); + CHECK(result.ResultCode != 0); + + workflow_set_workfolder(handle, "/tmp/adu/testdata/swupdate_filecopy"); + + result = PrepareStepsWorkflowDataObject(handle); + CHECK(result.ResultCode != 0); + + ADUC_WorkflowHandle stepHandle = workflow_get_child(handle, 0); + CHECK(stepHandle != nullptr); + + // Dummy workflow to hold a childHandle. + ADUC_WorkflowData stepWorkflow = {}; + stepWorkflow.WorkflowHandle = stepHandle; + + std::string scriptFilePath; + std::vector args; + std::vector commandLineArgs; + std::string scriptOutput; + + result = SWUpdateHandler_PerformAction( + "--action-is-installed", &stepWorkflow, true, scriptFilePath, args, commandLineArgs, scriptOutput); + CHECK(result.ResultCode != 0); + CHECK(result.ExtendedResultCode == 0); + + CHECK_THAT( + scriptOutput, + Equals( + R"( --update-type "microsoft/script" --update-action "execute" --target-data "/tmp/adu/testdata/swupdate_filecopy/example-du-swupdate-script.sh" --target-options --action-is-installed --target-options --swu-file --target-options "/tmp/adu/testdata/swupdate_filecopy/du-agent-swupdate-filecopy-test-1_1.0.swu" --target-options --software-version-file --target-options "/tmp/adu/testdata/test-device/vacuum-1/data/mock-update-for-file-copy-test-1.txt" --target-options --workfolder --target-options "/tmp/adu/testdata/swupdate_filecopy" --target-options --result-file --target-options "/tmp/adu/testdata/swupdate_filecopy/aduc_result.json" --target-options --installed-criteria --target-options "This is swupdate filecopy test version 1.0")")); + + std::string output; + int exitCode = ADUC_LaunchChildProcess(commandLineArgs[0], commandLineArgs, output); + + CHECK(exitCode == 0); + + // Check result file. + bool fileOk = ReadResultFile("/tmp/adu/testdata/swupdate_filecopy/aduc_result.json", &result); + CHECK(fileOk); + CHECK(result.ResultCode == 901); + CHECK(result.ExtendedResultCode == 806359140); // (0x30101064) + + ExtensionManager::Uninit(); +} + +TEST_CASE("SWUpdate sample script --action-download") +{ + ContentHandler* swupdateHandler = CreateUpdateContentHandlerExtension(ADUC_LOG_DEBUG); + CHECK(swupdateHandler != nullptr); + ExtensionManager::SetUpdateContentHandlerExtension("microsoft/swupdate:2", swupdateHandler); + + // Create test workflow data. + ADUC_WorkflowHandle handle = nullptr; + ADUC_Result result = workflow_init(filecopy_workflow_2, false, &handle); + CHECK(result.ResultCode != 0); + + workflow_set_workfolder(handle, "/tmp/adu/testdata/swupdate_filecopy"); + + result = PrepareStepsWorkflowDataObject(handle); + CHECK(result.ResultCode != 0); + + ADUC_WorkflowHandle stepHandle = workflow_get_child(handle, 0); + CHECK(stepHandle != nullptr); + + // Dummy workflow to hold a childHandle. + ADUC_WorkflowData stepWorkflow = {}; + stepWorkflow.WorkflowHandle = stepHandle; + + std::string scriptFilePath; + std::vector args; + std::vector commandLineArgs; + std::string scriptOutput; + + result = SWUpdateHandler_PerformAction( + "--action-download", &stepWorkflow, true, scriptFilePath, args, commandLineArgs, scriptOutput); + CHECK(result.ResultCode != 0); + CHECK(result.ExtendedResultCode == 0); + + CHECK_THAT( + scriptOutput, + Equals( + R"( --update-type "microsoft/script" --update-action "execute" --target-data "/tmp/adu/testdata/swupdate_filecopy/example-du-swupdate-script.sh" --target-options --action-download --target-options --swu-file --target-options "/tmp/adu/testdata/swupdate_filecopy/du-agent-swupdate-filecopy-test-1_1.0.swu" --target-options --software-version-file --target-options "/tmp/adu/testdata/test-device/vacuum-1/data/mock-update-for-file-copy-test-1.txt" --target-options --workfolder --target-options "/tmp/adu/testdata/swupdate_filecopy" --target-options --result-file --target-options "/tmp/adu/testdata/swupdate_filecopy/aduc_result.json" --target-options --installed-criteria --target-options "This is swupdate filecopy test version 1.0")")); + + std::string output; + int exitCode = ADUC_LaunchChildProcess(commandLineArgs[0], commandLineArgs, output); + + CHECK(exitCode == 0); + + // Check result file. + bool fileOk = ReadResultFile("/tmp/adu/testdata/swupdate_filecopy/aduc_result.json", &result); + CHECK(fileOk); + CHECK(result.ResultCode == 500); + CHECK(result.ExtendedResultCode == 0); + + ExtensionManager::Uninit(); +} + +TEST_CASE("SWUpdate sample script --action-install", "[!hide][functional_test]") +{ + ContentHandler* swupdateHandler = CreateUpdateContentHandlerExtension(ADUC_LOG_DEBUG); + CHECK(swupdateHandler != nullptr); + ExtensionManager::SetUpdateContentHandlerExtension("microsoft/swupdate:2", swupdateHandler); + + // Create test workflow data. + ADUC_WorkflowHandle handle = nullptr; + ADUC_Result result = workflow_init(filecopy_workflow_2, false, &handle); + CHECK(result.ResultCode != 0); + + workflow_set_workfolder(handle, "/tmp/adu/testdata/swupdate_filecopy"); + + result = PrepareStepsWorkflowDataObject(handle); + CHECK(result.ResultCode != 0); + + ADUC_WorkflowHandle stepHandle = workflow_get_child(handle, 0); + CHECK(stepHandle != nullptr); + + // Dummy workflow to hold a childHandle. + ADUC_WorkflowData stepWorkflow = {}; + stepWorkflow.WorkflowHandle = stepHandle; + + std::string scriptFilePath; + std::vector args; + std::vector commandLineArgs; + std::string scriptOutput; + + result = SWUpdateHandler_PerformAction( + "--action-install", &stepWorkflow, true, scriptFilePath, args, commandLineArgs, scriptOutput); + CHECK(result.ResultCode != 0); + CHECK(result.ExtendedResultCode == 0); + + CHECK_THAT( + scriptOutput, + Equals( + R"( --update-type "microsoft/script" --update-action "execute" --target-data "/tmp/adu/testdata/swupdate_filecopy/example-du-swupdate-script.sh" --target-options --action-install --target-options --swu-file --target-options "/tmp/adu/testdata/swupdate_filecopy/du-agent-swupdate-filecopy-test-1_1.0.swu" --target-options --software-version-file --target-options "/tmp/adu/testdata/test-device/vacuum-1/data/mock-update-for-file-copy-test-1.txt" --target-options --workfolder --target-options "/tmp/adu/testdata/swupdate_filecopy" --target-options --result-file --target-options "/tmp/adu/testdata/swupdate_filecopy/aduc_result.json" --target-options --installed-criteria --target-options "This is swupdate filecopy test version 1.0")")); + + std::string output; + int exitCode = ADUC_LaunchChildProcess(commandLineArgs[0], commandLineArgs, output); + + CHECK(exitCode == 0); + + printf("Output:\n%s", output.c_str()); + + // Check result file. + bool fileOk = ReadResultFile("/tmp/adu/testdata/swupdate_filecopy/aduc_result.json", &result); + CHECK(fileOk); + CHECK(result.ResultCode == 600); + CHECK(result.ExtendedResultCode == 0); + + ExtensionManager::Uninit(); +} + +TEST_CASE("SWUpdate sample script --action-apply") +{ + ContentHandler* swupdateHandler = CreateUpdateContentHandlerExtension(ADUC_LOG_DEBUG); + CHECK(swupdateHandler != nullptr); + ExtensionManager::SetUpdateContentHandlerExtension("microsoft/swupdate:2", swupdateHandler); + + // Create test workflow data. + ADUC_WorkflowHandle handle = nullptr; + ADUC_Result result = workflow_init(filecopy_workflow_2, false, &handle); + CHECK(result.ResultCode != 0); + + workflow_set_workfolder(handle, "/tmp/adu/testdata/swupdate_filecopy"); + + result = PrepareStepsWorkflowDataObject(handle); + CHECK(result.ResultCode != 0); + + ADUC_WorkflowHandle stepHandle = workflow_get_child(handle, 0); + CHECK(stepHandle != nullptr); + + // Dummy workflow to hold a childHandle. + ADUC_WorkflowData stepWorkflow = {}; + stepWorkflow.WorkflowHandle = stepHandle; + + std::string scriptFilePath; + std::vector args; + std::vector commandLineArgs; + std::string scriptOutput; + + result = SWUpdateHandler_PerformAction( + "--action-apply", &stepWorkflow, true, scriptFilePath, args, commandLineArgs, scriptOutput); + CHECK(result.ResultCode != 0); + CHECK(result.ExtendedResultCode == 0); + + std::string output; + int exitCode = ADUC_LaunchChildProcess(commandLineArgs[0], commandLineArgs, output); + + CHECK(exitCode == 0); + + printf("Output:\n%s", output.c_str()); + + // Check result file. + bool fileOk = ReadResultFile("/tmp/adu/testdata/swupdate_filecopy/aduc_result.json", &result); + CHECK(fileOk); + CHECK(result.ResultCode == 700); + CHECK(result.ExtendedResultCode == 0); + + ExtensionManager::Uninit(); +} + +TEST_CASE("SWUpdate sample script --action-cancel") +{ + ContentHandler* swupdateHandler = CreateUpdateContentHandlerExtension(ADUC_LOG_DEBUG); + CHECK(swupdateHandler != nullptr); + ExtensionManager::SetUpdateContentHandlerExtension("microsoft/swupdate:2", swupdateHandler); + + // Create test workflow data. + ADUC_WorkflowHandle handle = nullptr; + ADUC_Result result = workflow_init(filecopy_workflow_2, false, &handle); + CHECK(result.ResultCode != 0); + + workflow_set_workfolder(handle, "/tmp/adu/testdata/swupdate_filecopy"); + + result = PrepareStepsWorkflowDataObject(handle); + CHECK(result.ResultCode != 0); + + ADUC_WorkflowHandle stepHandle = workflow_get_child(handle, 0); + CHECK(stepHandle != nullptr); + + // Dummy workflow to hold a childHandle. + ADUC_WorkflowData stepWorkflow = {}; + stepWorkflow.WorkflowHandle = stepHandle; + + std::string scriptFilePath; + std::vector args; + std::vector commandLineArgs; + std::string scriptOutput; + + result = SWUpdateHandler_PerformAction( + "--action-cancel", &stepWorkflow, true, scriptFilePath, args, commandLineArgs, scriptOutput); + CHECK(result.ResultCode != 0); + CHECK(result.ExtendedResultCode == 0); + + std::string output; + int exitCode = ADUC_LaunchChildProcess(commandLineArgs[0], commandLineArgs, output); + + CHECK(exitCode == 0); + + printf("Output:\n%s", output.c_str()); + + // Check result file. + bool fileOk = ReadResultFile("/tmp/adu/testdata/swupdate_filecopy/aduc_result.json", &result); + CHECK(fileOk); + CHECK(result.ResultCode == 801); + CHECK(result.ExtendedResultCode == 0); + + ExtensionManager::Uninit(); +} diff --git a/src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/README.md b/src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/README.md new file mode 100644 index 000000000..b6b2fccd8 --- /dev/null +++ b/src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/README.md @@ -0,0 +1,47 @@ +# A/B Update Strategy Example + +## Instruction + +- [A/B Update Strategy Example](#ab-update-strategy-example) + - [Instruction](#instruction) + - [Prepare Demo Folder and Payloads](#prepare-demo-folder-and-payloads) + - [Generate Device Update Import Manifest File](#generate-device-update-import-manifest-file) + - [Options #1](#options-1) + +### Prepare Demo Folder and Payloads + +This example requires an update file in the 'payloads' folder. + +Follow steps below to prepare the payloads: + +1. Create a work folder + + ```sh + work_folder=/tmp/ab-demo + mkdir -p $work_folder + cp -fr ./ $work_folder + pushd $work_folder + ``` + +2. Copy your update file (.swu) to `$work_folder/payloads` folder. For example, `/tmp/ab-demo/payload/my-update.swu` +3. **Optional:** copy [AduUpdate.psm1](../../../../../../../tools/AduCmdlets/AduUpdate.psm1) into the work folder + +### Generate Device Update Import Manifest File + +#### Options #1 + +Run provided PowerShell script: [create-yocto-ab-rootfs-update-import-manifest-1.0.ps](create-yocto-ab-rootfs-update-import-manifest-1.0.ps1) + +- Import ADU Update PowerShell Module + +```ps +Import-Module ./AduUpdate.psm1 +``` + +- Generate an import manifest + +```ps +./create-yocto-ab-rootfs-update-import-manifest-1.0.ps -SwuFileName 'my-update.swu' +``` + +The output import manifest and payloads will be copied into `$work_folder/EDS-ADUClient.yocto-update.1.0\` diff --git a/src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/create-yocto-ab-rootfs-update-import-manifest-1.0.ps1 b/src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/create-yocto-ab-rootfs-update-import-manifest-1.0.ps1 new file mode 100644 index 000000000..fb53d59d0 --- /dev/null +++ b/src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/create-yocto-ab-rootfs-update-import-manifest-1.0.ps1 @@ -0,0 +1,169 @@ +# +# Device Update for IoT Hub +# Sample PowerShell script for creating import manifests and payloads files for demo purposes. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# + +#Requires -Version 5.0 + +<# + .SYNOPSIS + Create an import manifest (v4) for A/B root file system update. + + .EXAMPLE + PS > create-yocto-ab-rootfs-update-import-manifest.ps1 ` + -UpdateProvider "Contoso" ` + -UpdateName "Virtual-Vacuum" ` + -Manufacturer "contoso" ` + -Model "virtual-vacuum-v1" ` + -UpdateVersion "1.0" ` + -SwuFileName "my-update.swu" ` + -InstalledCriteria "8.0.1.0001" ` + -OutputManifestPath . +#> +[CmdletBinding()] +Param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] $OutputManifestPath, + + # Update Provider + [ValidateNotNullOrEmpty()] + [string] $UpdateProvider = "EDS-ADUClient", + + # Update Name + [ValidateNotNullOrEmpty()] + [string] $UpdateName = "yocto-update", + + # Device Manufacturer + [ValidateNotNullOrEmpty()] + [string] $Manufacturer = "contoso", + + # Device Model + [ValidateNotNullOrEmpty()] + [string] $Model = "virtual-vacuum-v2", + + # Update Version + # NOTE: This is the version of the update. Not the version of the image in .swu file. + [ValidateNotNullOrEmpty()] + [string] $UpdateVersion = "1.0", + + # Update Filename + [ValidateNotNullOrEmpty()] + [string] $SwuFileName = "my-update.swu", + + # Installed Criteria + # NOTE: This is the version number in the '--software-version-file' + [ValidateNotNullOrEmpty()] + [string] $InstalledCriteria = "8.0.1.0001" +) + +# ------------------------------------------------------------ +# Create the parent update. +# ------------------------------------------------------------ + +$parentCompat = New-AduUpdateCompatibility -Manufacturer $Manufacturer -Model $Model +$parentUpdateId = New-AduUpdateId -Provider $UpdateProvider -Name $UpdateName -Version $UpdateVersion +$parentUpdateIdStr = "$($parentUpdateId.Provider).$($parentUpdateId.Name).$($parentUpdateId.Version)" +$parentSteps = @() + + +# Payloads for top-level inline steps + +# Source files +$PayloadsDir = "payloads" + +$parentFile0 = "$PSScriptRoot\$PayloadsDir\example-a-b-update.sh" +$parentFile1 = "$PSScriptRoot\$PayloadsDir\$SwuFileName" + +$payloadFiles = $parentFile0, $parentFile1 + +Write-Host "#" +Write-Host "# Generating an import manifest" +Write-Host "#" +Write-Host "# Udate version : $UpdateVersion" +Write-Host "#" +Write-Host "# Payloads :" +Write-Host "# $parentFile0" +Write-Host "# $parentFile1" + + # ----------- + # ADD TOP-LEVEL STEP(S) + + # ====== + # Step 1 + # ====== + # This step demonstrates software installation using 'swupdate' program. + # The step handler type is 'microsoft/swupdate:2' which corresponding to SWUpdate Handler v2. + # + # The SWUpdate Handler v2 requires following handlerProperties: + # + # swuFile - Name of the image (.swu) file to be installed on the target device. + # + # scriptFile - Name of the primary script file that implements download, install, apply, cancel and isInstalled logic. + # This update uses 'example-a-b-update.sh' script. + # The script supports following actions: + # --action-download : No-op. (No additional payloads) + # --action-install : Invokes swupdate to install the specified 'swuFile' + # --action-apply : Sets uboot environment variable to switch to the desired rootfs partition after rebooted. + # Returns a special (ADUC_Result) result code to instruct DU Agent to reboot the device. + # --action-cancel : Restore uboot environment variable to the value before '--action-apply' is performed. + # --action-is-installed : Returns (ADUC_Result) result code that indicates whether the current rootfs contains + # software (image) version that match specified 'installCriteria' string. + # + # installedCriteria - String used to evaluate whether the software is installed on the target. + # When performing '--action-apply', the primary script file compare this value against content of the + # specified '--software-version-file' in 'arguments' property (see below). + # + # arguments - List of options and arguments that will be passed to the primary script. + # + # Required argument: + # --software-version-file + # A full path to a file (on a target device) containing a single line of text that identifies the image version. + # Default path '/etc/adu-version + # + # Optional argument: + # --swupdate-log-file + # A full path to a file that swupdate will write an output into. + # Default path '/adu/logs/swupdate.log' + # + $parentSteps += New-AduInstallationStep ` + -Handler 'microsoft/swupdate:2' ` + -Files $payloadFiles ` + -HandlerProperties @{ ` + 'scriptFile'='example-a-b-update.sh'; ` + 'swuFile'="$SwuFileName" + 'installedCriteria'="$InstalledCriteria"; ` + 'arguments'='--software-version-file /etc/adu-version --swupdate-log-file /adu/logs/swupdate.log' + } ` + -Description 'Update rootfs using A/B update strategy' + +# ------------------------------ +# Create parent update manifest +Write-Host "#" +Write-Host "# Output directory: $parentUpdateIdStr" + +$parentManifest = New-AduImportManifest -UpdateId $parentUpdateId ` + -IsDeployable $true ` + -Compatibility $parentCompat ` + -InstallationSteps $parentSteps ` + -ErrorAction Stop + +# Create folder for manifest files and payload. +$outputPath = "$OutputManifestPath\$parentUpdateIdStr" +New-Item $outputPath -ItemType Directory -ErrorAction SilentlyContinue | Out-Null + +# Generate manifest file. +$outputManifestFile = "$outputPath\$parentUpdateIdStr.importmanifest.json" +Write-Host "#" +Write-Host "# Import manifest: $outputManifestFile" +$parentManifest | Out-File "$outputManifestFile" -Encoding utf8 + +# Copy all payloads (if used) +if ($payloadFiles) { ` + Write-Host "# Saved payload(s) to: $outputPath" + Copy-Item -Path $payloadFiles -Destination $outputPath -Force ` +} + +Write-Host "#" diff --git a/src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/payloads/adu-version b/src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/payloads/adu-version new file mode 100644 index 000000000..6fb461b80 --- /dev/null +++ b/src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/payloads/adu-version @@ -0,0 +1 @@ +8.0.1.0001 diff --git a/src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/payloads/example-a-b-update.sh b/src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/payloads/example-a-b-update.sh new file mode 100755 index 000000000..14fe0ce3e --- /dev/null +++ b/src/extensions/step_handlers/swupdate_handler_v2/tests/testdata/adu-yocto-ab-rootfs-update/payloads/example-a-b-update.sh @@ -0,0 +1,751 @@ +#!/bin/bash + +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Ensure that getopt starts from first option if ". " was used. +OPTIND=1 + +ret_val=0 + +# Ensure we dont end the user's terminal session if invoked from source ("."). +if [[ $0 != "${BASH_SOURCE[0]}" ]]; then + ret='return' +else + ret='exit' +fi + +# Output formatting. + +# Log level: 0=debug, 1=info, 2=warning, 3=error, 4=none +log_level=1 + +warn() { echo -e "\033[1;33mWarning:\033[0m $*" >&2; } + +error() { echo -e "\033[1;31mError:\033[0m $*" >&2; } + +header() { echo -e "\e[4m\e[1m\e[1;32m$*\e[0m"; } + +bullet() { echo -e "\e[1;34m*\e[0m $*"; } + +warn "*************************************************" +warn "* WARNING *" +warn "* *" +warn "* THIS FILE IS FOR DEMONSTRATION PURPOSES ONLY. *" +warn "* DO NOT USE THIS FOR YOUR REAL PRODUCT UPDATE! *" +warn "* *" +warn "*************************************************" + +# Log debug prefix - blue +log_debug_pref="\033[1;30m[D]\033[0m" + +# Log info prefix - blue +log_info_pref="\033[1;34m[I]\033[0m" + +# Log warning prefix - yellow +log_warn_pref="\033[1;33m[W]\033[0m" + +# Log error prefix - red +log_error_pref="\033[1;31m[E]\033[0m" + +# +# Files and Folders information +# +workfolder= +swu_file= +output_file=/adu/logs/swpupdate.output +log_file=/adu/logs/swupdate.log +result_file=/adu/logs/swupdate.result.json + +# +# Following files are based on the how the Yocto Refererence Image was built. +# This value can be replaced in this script, or specify in import manifest (handlerProperties) as needed. +# +software_version_file="/etc/adu-version" +public_key_file="/adukey/public.pem" + +# +# For install task, the --image-file option must be specified. +# +image_file="" + +# +# Device Update specific arguments +# +check_is_installed= +installed_criteria= +do_download_action= +do_install_action= +do_apply_action= +do_cancel_action= + +restart_to_apply= +restart_agent_to_apply= + +# +# Remaining aguments and parameters +# +PARAMS= + +# +# Output, Logs, and Result helper functions. +# +_timestamp= + +# SWUpdate doesn't support everything necessary for the dual-copy or A/B update strategy. +# Here we figure out the current OS partition and then set some environment variables +# that we use to tell swupdate which partition to target. +rootfs_dev=$(mount | grep "on / type" | cut -d':' -f 2 | cut -d' ' -f 1) +if [[ $rootfs_dev == '/dev/mmcblk0p2' ]]; then + selection="stable,copy2" + current_partition=2 + update_partition=3 +else + selection="stable,copy1" + current_partition=3 + update_partition=2 +fi + +update_timestamp() { + # See https://man7.org/linux/man-pages/man1/date.1.html + _timestamp="$(date +'%Y/%m/%d:%H%M%S')" +} + +log_debug() { + if [ $log_level -gt 0 ]; then + return + fi + log "$log_debug_pref" "$@" +} + +log_info() { + if [ $log_level -gt 1 ]; then + return + fi + log "$log_info_pref" "$@" +} + +log_warn() { + if [ $log_level -gt 2 ]; then + return + fi + log "$log_warn_pref" "$@" +} + +log_error() { + if [ $log_level -gt 3 ]; then + return + fi + log "$log_error_pref" "$@" +} + +log() { + update_timestamp + if [ -z $log_file ]; then + echo -e "[$_timestamp]" "$@" >&1 + else + echo "[$_timestamp]" "$@" >> $log_file + fi +} + +output() { + update_timestamp + if [ -z $output_file ]; then + echo "[$_timestamp]" "$@" >&1 + else + echo "[$_timestamp]" "$@" >> "$output_file" + fi +} + +result() { + # NOTE: don't insert timestamp in result file. + if [ -z $result_file ]; then + echo "$@" >&1 + else + echo "$@" > "$result_file" + fi +} + +# +# Helper function for creating extended result code that indicates +# errors from this script. +# Note: these error range (0x30101000 - 0x30101fff) a free to use. +# +# Usage: make_swupdate_handler_erc error_value result_variable +# +# e.g. +# error_code=20 +# RESULT=0 +# make_swupdate_handler_erc RESULT $error_code +# echo $RESULT +# +# (RESULT is 0x30101014) +# +make_swupdate_handler_erc() { + local base_erc=0x30101000 + local -n res=$2 # name reference + res=$((base_erc + $1)) +} + +# usage: make_aduc_result_json $resultCode $extendedResultCode $resultDetails +# shellcheck disable=SC2034 +make_aduc_result_json() { + local -n res=$4 # name reference + res="{\"resultCode\":$1, \"extendedResultCode\":$2,\"resultDetails\":\"$3\"}" +} + +# +# Usage +# +print_help() { + echo "" + echo "Usage: .sh [options...]" + echo "" + echo "" + echo "Device Update reserved argument" + echo "===============================" + echo "" + echo "--action-is-installed Perform 'is-installed' check." + echo " Check whether the selected component [or primary device] current states" + echo " satisfies specified 'installedCriteria' data." + echo "--installed-criteria Specify the Installed-Criteria string." + echo "" + echo "--action-download Perform 'download' aciton." + echo "--action-install Perform 'install' action." + echo "--action-apply Perform 'apply' action." + echo "--action-cancel Perform 'cancel' action." + echo "" + echo "--restart-to-apply Request the host device to restart when applying update to this component." + echo "--restart-agent-to-apply Request the DU Agent to restart when applying update to this component." + echo "" + echo "File and Folder information" + echo "===========================" + echo "" + echo "--workfolder A work-folder (or sandbox folder)." + echo "--swu-file A swu file to install." + echo "--output-file An output file." + echo "--log-file A log file." + echo "--result-file A file contain ADUC_Result data (in JSON format)." + echo "--software-version-file A file contain image version number." + echo "--public-key-file A public key file for signature validateion." + echo " See InstallUpdate() function for more details." + echo "" + echo "--log-level <0-4> A minimum log level. 0=debug, 1=info, 2=warning, 3=error, 4=none." + echo "-h, --help Show this help message." + echo "" + echo "Example:" + echo "" + echo "Scenario: is-installed check" + echo "========================================" + echo "