Version in base suite: 2.2.34-4 Base version: waagent_2.2.34-4 Target version: waagent_2.2.45-4~deb10u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/w/waagent/waagent_2.2.34-4.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/w/waagent/waagent_2.2.45-4~deb10u1.dsc /srv/release.debian.org/tmp/rfssO7vR7q/waagent-2.2.45/tests/data/ext/sample_ext-1.3.0.zip |binary /srv/release.debian.org/tmp/rfssO7vR7q/waagent-2.2.45/tests/data/ga/WALinuxAgent-2.2.33.zip |binary /srv/release.debian.org/tmp/rfssO7vR7q/waagent-2.2.45/tests/data/ga/WALinuxAgent-2.2.45.zip |binary waagent-2.2.45/.flake8 | 32 waagent-2.2.45/.github/ISSUE_TEMPLATE/bug_report.md | 23 waagent-2.2.45/.gitignore | 3 waagent-2.2.45/.travis.yml | 53 waagent-2.2.45/CODEOWNERS | 14 waagent-2.2.45/Changelog | 2 waagent-2.2.45/README.md | 40 waagent-2.2.45/__main__.py | 20 waagent-2.2.45/azurelinuxagent/agent.py | 25 waagent-2.2.45/azurelinuxagent/common/cgroup.py | 247 + waagent-2.2.45/azurelinuxagent/common/cgroupapi.py | 566 +++ waagent-2.2.45/azurelinuxagent/common/cgroupconfigurator.py | 210 + waagent-2.2.45/azurelinuxagent/common/cgroups.py | 812 ----- waagent-2.2.45/azurelinuxagent/common/cgroupstelemetry.py | 222 + waagent-2.2.45/azurelinuxagent/common/conf.py | 31 waagent-2.2.45/azurelinuxagent/common/datacontract.py | 83 waagent-2.2.45/azurelinuxagent/common/dhcp.py | 9 waagent-2.2.45/azurelinuxagent/common/event.py | 135 waagent-2.2.45/azurelinuxagent/common/exception.py | 97 waagent-2.2.45/azurelinuxagent/common/future.py | 29 waagent-2.2.45/azurelinuxagent/common/logger.py | 59 waagent-2.2.45/azurelinuxagent/common/osutil/alpine.py | 6 waagent-2.2.45/azurelinuxagent/common/osutil/arch.py | 10 waagent-2.2.45/azurelinuxagent/common/osutil/bigip.py | 11 waagent-2.2.45/azurelinuxagent/common/osutil/clearlinux.py | 7 waagent-2.2.45/azurelinuxagent/common/osutil/coreos.py | 10 waagent-2.2.45/azurelinuxagent/common/osutil/debian.py | 23 waagent-2.2.45/azurelinuxagent/common/osutil/default.py | 252 + waagent-2.2.45/azurelinuxagent/common/osutil/factory.py | 39 waagent-2.2.45/azurelinuxagent/common/osutil/freebsd.py | 313 ++ waagent-2.2.45/azurelinuxagent/common/osutil/nsbsd.py | 8 waagent-2.2.45/azurelinuxagent/common/osutil/openbsd.py | 15 waagent-2.2.45/azurelinuxagent/common/osutil/openwrt.py | 152 waagent-2.2.45/azurelinuxagent/common/osutil/redhat.py | 16 waagent-2.2.45/azurelinuxagent/common/osutil/suse.py | 24 waagent-2.2.45/azurelinuxagent/common/osutil/ubuntu.py | 25 waagent-2.2.45/azurelinuxagent/common/protocol/hostplugin.py | 6 waagent-2.2.45/azurelinuxagent/common/protocol/imds.py | 120 waagent-2.2.45/azurelinuxagent/common/protocol/metadata.py | 74 waagent-2.2.45/azurelinuxagent/common/protocol/restapi.py | 90 waagent-2.2.45/azurelinuxagent/common/protocol/util.py | 6 waagent-2.2.45/azurelinuxagent/common/protocol/wire.py | 438 +- waagent-2.2.45/azurelinuxagent/common/rdma.py | 168 - waagent-2.2.45/azurelinuxagent/common/telemetryevent.py | 45 waagent-2.2.45/azurelinuxagent/common/utils/cryptutil.py | 24 waagent-2.2.45/azurelinuxagent/common/utils/extensionprocessutil.py | 141 waagent-2.2.45/azurelinuxagent/common/utils/processutil.py | 207 - waagent-2.2.45/azurelinuxagent/common/utils/restutil.py | 136 waagent-2.2.45/azurelinuxagent/common/utils/shellutil.py | 77 waagent-2.2.45/azurelinuxagent/common/utils/textutil.py | 13 waagent-2.2.45/azurelinuxagent/common/version.py | 9 waagent-2.2.45/azurelinuxagent/daemon/main.py | 9 waagent-2.2.45/azurelinuxagent/daemon/resourcedisk/default.py | 76 waagent-2.2.45/azurelinuxagent/daemon/resourcedisk/factory.py | 4 waagent-2.2.45/azurelinuxagent/daemon/resourcedisk/freebsd.py | 74 waagent-2.2.45/azurelinuxagent/daemon/resourcedisk/openwrt.py | 135 waagent-2.2.45/azurelinuxagent/ga/env.py | 55 waagent-2.2.45/azurelinuxagent/ga/exthandlers.py | 579 ++- waagent-2.2.45/azurelinuxagent/ga/monitor.py | 233 - waagent-2.2.45/azurelinuxagent/ga/remoteaccess.py | 33 waagent-2.2.45/azurelinuxagent/ga/update.py | 134 waagent-2.2.45/azurelinuxagent/pa/deprovision/factory.py | 2 waagent-2.2.45/azurelinuxagent/pa/provision/cloudinit.py | 74 waagent-2.2.45/azurelinuxagent/pa/provision/factory.py | 15 waagent-2.2.45/azurelinuxagent/pa/rdma/centos.py | 4 waagent-2.2.45/azurelinuxagent/pa/rdma/factory.py | 6 waagent-2.2.45/azurelinuxagent/pa/rdma/suse.py | 22 waagent-2.2.45/azurelinuxagent/pa/rdma/ubuntu.py | 2 waagent-2.2.45/config/arch/waagent.conf | 3 waagent-2.2.45/config/bigip/waagent.conf | 3 waagent-2.2.45/config/clearlinux/waagent.conf | 3 waagent-2.2.45/config/coreos/waagent.conf | 3 waagent-2.2.45/config/debian/waagent.conf | 3 waagent-2.2.45/config/freebsd/waagent.conf | 3 waagent-2.2.45/config/gaia/waagent.conf | 3 waagent-2.2.45/config/iosxe/waagent.conf | 3 waagent-2.2.45/config/nsbsd/waagent.conf | 3 waagent-2.2.45/config/openbsd/waagent.conf | 3 waagent-2.2.45/config/suse/waagent.conf | 6 waagent-2.2.45/config/ubuntu/waagent.conf | 6 waagent-2.2.45/config/waagent.conf | 6 waagent-2.2.45/debian/.gitignore | 6 waagent-2.2.45/debian/README.source.md | 9 waagent-2.2.45/debian/changelog | 40 waagent-2.2.45/debian/control | 3 waagent-2.2.45/debian/patches/agent-command-provision.patch | 30 waagent-2.2.45/debian/patches/agent-command-resourcedisk.patch | 51 waagent-2.2.45/debian/patches/cve-2019-0804.patch | 149 waagent-2.2.45/debian/patches/debian-changes | 458 ++ waagent-2.2.45/debian/patches/disable-auto-update.patch | 21 waagent-2.2.45/debian/patches/disable-bytecode-exthandler.patch | 31 waagent-2.2.45/debian/patches/entry-points.patch | 102 waagent-2.2.45/debian/patches/ignore-tests.patch | 63 waagent-2.2.45/debian/patches/osutil-debian.patch | 25 waagent-2.2.45/debian/patches/resourcedisk-filesystem.patch | 155 - waagent-2.2.45/debian/patches/series | 11 waagent-2.2.45/debian/patches/user-shell.patch | 24 waagent-2.2.45/debian/rules | 20 waagent-2.2.45/init/openwrt/waagent | 60 waagent-2.2.45/requirements.txt | 2 waagent-2.2.45/setup.py | 34 waagent-2.2.45/test-requirements.txt | 5 waagent-2.2.45/tests/common/dhcp/test_dhcp.py | 7 waagent-2.2.45/tests/common/osutil/test_alpine.py | 34 waagent-2.2.45/tests/common/osutil/test_arch.py | 34 waagent-2.2.45/tests/common/osutil/test_bigip.py | 28 waagent-2.2.45/tests/common/osutil/test_clearlinux.py | 34 waagent-2.2.45/tests/common/osutil/test_coreos.py | 34 waagent-2.2.45/tests/common/osutil/test_default.py | 94 waagent-2.2.45/tests/common/osutil/test_default_osutil.py | 182 + waagent-2.2.45/tests/common/osutil/test_factory.py | 273 + waagent-2.2.45/tests/common/osutil/test_freebsd.py | 124 waagent-2.2.45/tests/common/osutil/test_nsbsd.py | 87 waagent-2.2.45/tests/common/osutil/test_openbsd.py | 34 waagent-2.2.45/tests/common/osutil/test_openwrt.py | 34 waagent-2.2.45/tests/common/osutil/test_redhat.py | 34 waagent-2.2.45/tests/common/osutil/test_suse.py | 34 waagent-2.2.45/tests/common/osutil/test_ubuntu.py | 45 waagent-2.2.45/tests/common/test_cgroupapi.py | 642 ++++ waagent-2.2.45/tests/common/test_cgroupconfigurator.py | 295 + waagent-2.2.45/tests/common/test_cgroups.py | 433 -- waagent-2.2.45/tests/common/test_cgroupstelemetry.py | 699 ++++ waagent-2.2.45/tests/common/test_conf.py | 7 waagent-2.2.45/tests/common/test_event.py | 255 + waagent-2.2.45/tests/common/test_logger.py | 144 waagent-2.2.45/tests/common/test_telemetryevent.py | 49 waagent-2.2.45/tests/common/test_version.py | 1 waagent-2.2.45/tests/daemon/test_daemon.py | 57 waagent-2.2.45/tests/data/cgroups/cpu_mount/cpuacct.stat | 2 waagent-2.2.45/tests/data/cgroups/dummy_proc_stat | 12 waagent-2.2.45/tests/data/cgroups/dummy_proc_stat_updated | 12 waagent-2.2.45/tests/data/cgroups/memory_mount/memory.max_usage_in_bytes | 1 waagent-2.2.45/tests/data/cgroups/memory_mount/memory.usage_in_bytes | 1 waagent-2.2.45/tests/data/events/collect_and_send_events_invalid_data/1560752429123264-1.tld | 32 waagent-2.2.45/tests/data/events/collect_and_send_events_invalid_data/1560752429123264.tld | 32 waagent-2.2.45/tests/data/events/collect_and_send_events_invalid_data/1560752429133818-1.tld | 32 waagent-2.2.45/tests/data/events/collect_and_send_events_invalid_data/1560752429133818.tld | 32 waagent-2.2.45/tests/data/events/collect_and_send_events_unreadable_data/IncorrectExtension.tmp | 32 waagent-2.2.45/tests/data/events/collect_and_send_events_unreadable_data/UnreadableFile.tld | 32 waagent-2.2.45/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_stderr_with_non_ascii_characters | 1 waagent-2.2.45/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_stdout_with_non_ascii_characters | 1 waagent-2.2.45/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_valid_stderr | 1 waagent-2.2.45/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_valid_stdout | 1 waagent-2.2.45/tests/data/ext/dsc_event.json | 38 waagent-2.2.45/tests/data/ext/event.json | 38 waagent-2.2.45/tests/data/ext/event.xml | 1 waagent-2.2.45/tests/data/ext/event_from_agent.json | 1 waagent-2.2.45/tests/data/ext/event_from_extension.xml | 21 waagent-2.2.45/tests/data/ext/sample_ext-1.3.0/exit.sh | 3 waagent-2.2.45/tests/data/test_waagent.conf | 7 waagent-2.2.45/tests/data/wire/certs_format_not_pfx.xml | 7 waagent-2.2.45/tests/data/wire/certs_no_format_specified.xml | 81 waagent-2.2.45/tests/data/wire/ext_conf_multiple_extensions.xml | 76 waagent-2.2.45/tests/data/wire/trans_pub | 9 waagent-2.2.45/tests/distro/test_resourceDisk.py | 47 waagent-2.2.45/tests/ga/test_env.py | 161 + waagent-2.2.45/tests/ga/test_extension.py | 1532 +++++++++- waagent-2.2.45/tests/ga/test_exthandlers.py | 476 +++ waagent-2.2.45/tests/ga/test_exthandlers_download_extension.py | 211 + waagent-2.2.45/tests/ga/test_exthandlers_exthandlerinstance.py | 128 waagent-2.2.45/tests/ga/test_monitor.py | 833 +++++ waagent-2.2.45/tests/ga/test_update.py | 14 waagent-2.2.45/tests/pa/test_provision.py | 70 waagent-2.2.45/tests/protocol/mockwiredata.py | 8 waagent-2.2.45/tests/protocol/test_datacontract.py | 49 waagent-2.2.45/tests/protocol/test_hostplugin.py | 32 waagent-2.2.45/tests/protocol/test_image_info_matcher.py | 26 waagent-2.2.45/tests/protocol/test_imds.py | 157 - waagent-2.2.45/tests/protocol/test_metadata.py | 11 waagent-2.2.45/tests/protocol/test_restapi.py | 50 waagent-2.2.45/tests/protocol/test_wire.py | 713 ++++ waagent-2.2.45/tests/test_agent.py | 12 waagent-2.2.45/tests/tools.py | 189 + waagent-2.2.45/tests/utils/cgroups_tools.py | 50 waagent-2.2.45/tests/utils/process_target.sh | 41 waagent-2.2.45/tests/utils/test_crypt_util.py | 34 waagent-2.2.45/tests/utils/test_extension_process_util.py | 260 + waagent-2.2.45/tests/utils/test_process_util.py | 260 - waagent-2.2.45/tests/utils/test_rest_util.py | 210 + waagent-2.2.45/tests/utils/test_shell_util.py | 179 + waagent-2.2.45/tests/utils/test_text_util.py | 12 184 files changed, 14015 insertions(+), 3931 deletions(-) diff -Nru waagent-2.2.34/.flake8 waagent-2.2.45/.flake8 --- waagent-2.2.34/.flake8 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/.flake8 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,32 @@ +# +# The project did not use flake8 since inception so there are a number +# of time-consuming flake8-identified improvements that are just a lot +# of busy work. Each of these should be disabled and code cleaned up. +# +# W503: Line break occurred before a binary operator +# W504: Line break occurred after a binary operator +# E126: Continuation line over-indented for hanging indent +# E127: Continuation line over-indented for visual indent +# E128: Continuation line under-indented for visual indent +# E201: Whitespace after '(' +# E202: Whitespace before ')' +# E203: Whitespace before ':' +# E221: Multiple spaces before operator +# E225: Missing whitespace around operator +# E226: Missing whitespace around arithmetic operator +# E231: Missing whitespace after ',', ';', or ':' +# E261: At least two spaces before inline comment +# E265: Block comment should start with '# ' +# E302: Expected 2 blank lines, found 0 +# E501: Line too long (xx > yy characters) +# E502: The backslash is redundant between brackets +# F401: Module imported but unused +# F403: 'from module import *' used; unable to detect undefined names +# F405: Name may be undefined, or defined from star imports: module +# + +[flake8] +ignore = W503,W504,E126,E127,E128,E201,E202,E203,E221,E225,E226,E231,E261,E265,E302,E501,E502,F401,F403,F405 +exclude = .git,__pycache__,docs/source/conf.py,old,build,dist,tests +max-complexity = 30 +max-line-length = 120 \ No newline at end of file diff -Nru waagent-2.2.34/.github/ISSUE_TEMPLATE/bug_report.md waagent-2.2.45/.github/ISSUE_TEMPLATE/bug_report.md --- waagent-2.2.34/.github/ISSUE_TEMPLATE/bug_report.md 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/.github/ISSUE_TEMPLATE/bug_report.md 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,23 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: triage +assignees: narrieta, pgombar, vrdmr, larohra + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +Note: Please add some context which would help us understand the problem better +1. Section of the log where the error occurs. +2. Serial console output +3. Steps to reproduce the behavior. + +**Distro and WALinuxAgent details (please complete the following information):** + - Distro and Version: [e.g. Ubuntu 16.04] + - WALinuxAgent version [e.g. 2.2.34, you can copy the output of `waagent --version`, more info [here](https://github.com/Azure/WALinuxAgent/wiki/FAQ#what-does-goal-state-agent-mean-in-waagent---version-output) ] + +**Additional context** +Add any other context about the problem here. diff -Nru waagent-2.2.34/.gitignore waagent-2.2.45/.gitignore --- waagent-2.2.34/.gitignore 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/.gitignore 2019-11-07 00:36:56.000000000 +0000 @@ -69,3 +69,6 @@ # rope project .ropeproject/ + +# mac osx specific files +.DS_Store diff -Nru waagent-2.2.34/.travis.yml waagent-2.2.45/.travis.yml --- waagent-2.2.34/.travis.yml 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/.travis.yml 2019-11-07 00:36:56.000000000 +0000 @@ -1,14 +1,43 @@ +--- +os: linux +dist: xenial language: python -python: - - "2.6" - - "2.7" - #- "3.2" - #- "3.3" - - "3.4" -# command to install dependencies +env: + - NOSEOPTS="--verbose" SETUPOPTS="" + # Add SETUPOPTS="check flake8" to enable flake8 checks + +matrix: + # exclude the default "python" build - we're being specific here... + exclude: + - python: + env: + - NOSEOPTS="" SETUPOPTS="check flake8" + + include: + - python: 2.6 + dist: trusty + env: + - NOSEOPTS="--verbose" SETUPOPTS="" + - python: 2.7 + - python: 3.4 + - python: 3.6 + - python: 3.7 + env: + - >- + NOSEOPTS="--verbose --with-coverage --cover-inclusive + --cover-min-percentage=60 --cover-branches + --cover-package=azurelinuxagent --cover-xml" + SETUPOPTS="" + install: - #- pip install . - #- pip install -r requirements.txt - - pip install pyasn1 -# command to run tests -script: nosetests tests + - pip install -r requirements.txt + - pip install -r test-requirements.txt + +script: + # future: - pylint setup.py makepkg.py azurelinuxagent/ + - nosetests $NOSEOPTS --attr '!requires_sudo' tests + - sudo env "PATH=$PATH" nosetests $NOSEOPTS --verbose --attr 'requires_sudo' tests + - if [ ! -z "$SETUPOPTS" ]; then /usr/bin/env python setup.py $SETUPOPTS; fi + +after_success: + - if [[ $TRAVIS_PYTHON_VERSION == 3.7 ]]; then codecov; fi \ No newline at end of file diff -Nru waagent-2.2.34/CODEOWNERS waagent-2.2.45/CODEOWNERS --- waagent-2.2.34/CODEOWNERS 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/CODEOWNERS 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,14 @@ +# See https://help.github.com/articles/about-codeowners/ +# for more info about CODEOWNERS file + +# It uses the same pattern rule for gitignore file +# https://git-scm.com/docs/gitignore#_pattern_format + +# Provisioning Agent +# The Azure Linux Provisioning team is interested in getting notifications +# when there are requests for changes in the provisioning agent. For any +# questions, please feel free to reach out to thstring@microsoft.com. +/azurelinuxagent/pa/ @trstringer @anhvoms + +# Guest Agent team +* @narrieta @vrdmr @pgombar @larohra diff -Nru waagent-2.2.34/Changelog waagent-2.2.45/Changelog --- waagent-2.2.34/Changelog 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/Changelog 2019-11-07 00:36:56.000000000 +0000 @@ -1,6 +1,8 @@ WALinuxAgent Changelog ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| +Refer to releases WALinuxAgent release page: https://github.com/Azure/WALinuxAgent/releases for detailed changelog after v2.2.0 + 12 August 2016, v2.1.6 . Improved RDMA support . Extension state migration diff -Nru waagent-2.2.34/README.md waagent-2.2.45/README.md --- waagent-2.2.34/README.md 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/README.md 2019-11-07 00:36:56.000000000 +0000 @@ -2,6 +2,9 @@ ## Master branch status +[![Travis CI](https://travis-ci.org/Azure/WALinuxAgent.svg?branch=develop)](https://travis-ci.org/Azure/WALinuxAgent/branches) +[![CodeCov](https://codecov.io/gh/Azure/WALinusAgent/branch/develop/graph/badge.svg)](https://codecov.io/gh/Azure/WALinuxAgent/branch/develop) + Each badge below represents our basic validation tests for an image, which are executed several times each day. These include provisioning, user account, disk, extension and networking scenarios. Image | Status | @@ -180,8 +183,7 @@ ```yml Extensions.Enabled=y -Provisioning.Enabled=y -Provisioning.UseCloudInit=n +Provisioning.Agent=auto Provisioning.DeleteRootPassword=n Provisioning.RegenerateSshHostKeyPair=y Provisioning.SshHostKeyPairType=rsa @@ -234,9 +236,18 @@ provisioning time, via whichever API is being used. We will provide more details on this on our wiki when it is generally available. -#### __Provisioning.Enabled__ +#### __Provisioning.Agent__ -_Type: Boolean_ +_Type: String_ +_Default: auto_ + +Choose which provisioning agent to use (or allow waagent to figure it out by +specifying "auto"). Possible options are "auto" (default), "waagent", "cloud-init", +or "disabled". + +#### __Provisioning.Enabled__ (*removed in VERSION*) + +_Type: Boolean_ _Default: y_ This allows the user to enable or disable the provisioning functionality in the @@ -244,9 +255,13 @@ user keys in the image are preserved and any configuration specified in the Azure provisioning API is ignored. -#### __Provisioning.UseCloudInit__ - -_Type: Boolean_ +_Note_: This configuration option has been removed and has no effect. waagent +now auto-detects cloud-init as a provisioning agent (with an option to override +with `Provisioning.Agent`). + +#### __Provisioning.UseCloudInit__ (*removed in VERSION*) + +_Type: Boolean_ _Default: n_ This options enables / disables support for provisioning by means of cloud-init. @@ -255,6 +270,10 @@ disabled ("n") for this option to have an effect. Setting _Provisioning.Enabled_ to true ("y") overrides this option and runs the built-in agent provisioning code. +_Note_: This configuration option has been removed and has no effect. waagent +now auto-detects cloud-init as a provisioning agent (with an option to override +with `Provisioning.Agent`). + #### __Provisioning.DeleteRootPassword__ _Type: Boolean_ @@ -398,11 +417,10 @@ _Type: Boolean_ _Default: n_ -If set to `y` and SSL support is not compiled into Python, the agent will fall-back to -use HTTP. Otherwise, if SSL support is not compiled into Python, the agent will fail -all HTTPS requests. +If SSL support is not compiled into Python, the agent will fail all HTTPS requests. +You can set this option to 'y' to make the agent fall-back to HTTP, instead of failing the requests. -Note: Allowing HTTP may unintentionally expose secure data. +NOTE: Allowing HTTP may unintentionally expose secure data. #### __OS.EnableRDMA__ diff -Nru waagent-2.2.34/__main__.py waagent-2.2.45/__main__.py --- waagent-2.2.34/__main__.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/__main__.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,20 @@ +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# + +import azurelinuxagent.agent as agent + +agent.main() diff -Nru waagent-2.2.34/azurelinuxagent/agent.py waagent-2.2.45/azurelinuxagent/agent.py --- waagent-2.2.34/azurelinuxagent/agent.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/agent.py 2019-11-07 00:36:56.000000000 +0000 @@ -27,6 +27,7 @@ import sys import re import subprocess +import threading import traceback import azurelinuxagent.common.logger as logger @@ -39,6 +40,7 @@ from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.utils import fileutil + class Agent(object): def __init__(self, verbose, conf_file_path=None): """ @@ -62,8 +64,9 @@ level = logger.LogLevel.VERBOSE if verbose else logger.LogLevel.INFO logger.add_logger_appender(logger.AppenderType.FILE, level, path="/var/log/waagent.log") - logger.add_logger_appender(logger.AppenderType.CONSOLE, level, - path="/dev/console") + if conf.get_logs_console(): + logger.add_logger_appender(logger.AppenderType.CONSOLE, level, + path="/dev/console") # See issue #1035 # logger.add_logger_appender(logger.AppenderType.TELEMETRY, # logger.LogLevel.WARNING, @@ -91,6 +94,7 @@ Run agent daemon """ logger.set_prefix("Daemon") + threading.current_thread().setName("Daemon") child_args = None \ if self.conf_file_path is None \ else "-configuration-path:{0}".format(self.conf_file_path) @@ -126,20 +130,22 @@ print("Start {0} service".format(AGENT_NAME)) self.osutil.start_agent_service() - def run_exthandlers(self): + def run_exthandlers(self, debug=False): """ Run the update and extension handler """ logger.set_prefix("ExtHandler") + threading.current_thread().setName("ExtHandler") from azurelinuxagent.ga.update import get_update_handler update_handler = get_update_handler() - update_handler.run() + update_handler.run(debug) def show_configuration(self): configuration = conf.get_configuration() for k in sorted(configuration.keys()): print("{0} = {1}".format(k, configuration[k])) + def main(args=[]): """ Parse command line arguments, exit with usage() on error. @@ -147,7 +153,7 @@ """ if len(args) <= 0: args = sys.argv[1:] - command, force, verbose, conf_file_path = parse_args(args) + command, force, verbose, debug, conf_file_path = parse_args(args) if command == "version": version() elif command == "help": @@ -168,7 +174,7 @@ elif command == "daemon": agent.daemon() elif command == "run-exthandlers": - agent.run_exthandlers() + agent.run_exthandlers(debug) elif command == "show-configuration": agent.show_configuration() except Exception: @@ -183,6 +189,7 @@ cmd = "help" force = False verbose = False + debug = False conf_file_path = None for a in sys_args: m = re.match("^(?:[-/]*)configuration-path:([\w/\.\-_]+)", a) @@ -210,6 +217,8 @@ cmd = "version" elif re.match("^([-/]*)verbose", a): verbose = True + elif re.match("^([-/]*)debug", a): + debug = True elif re.match("^([-/]*)force", a): force = True elif re.match("^([-/]*)show-configuration", a): @@ -219,7 +228,9 @@ else: cmd = "help" break - return cmd, force, verbose, conf_file_path + + return cmd, force, verbose, debug, conf_file_path + def version(): """ diff -Nru waagent-2.2.34/azurelinuxagent/common/cgroup.py waagent-2.2.45/azurelinuxagent/common/cgroup.py --- waagent-2.2.34/azurelinuxagent/common/cgroup.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/cgroup.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,247 @@ +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +import errno +import os +import re + +from azurelinuxagent.common import logger +from azurelinuxagent.common.exception import CGroupsException +from azurelinuxagent.common.future import ustr +from azurelinuxagent.common.osutil import get_osutil +from azurelinuxagent.common.utils import fileutil + +re_user_system_times = re.compile(r'user (\d+)\nsystem (\d+)\n') + + +class CGroup(object): + @staticmethod + def create(cgroup_path, controller, extension_name): + """ + Factory method to create the correct CGroup. + """ + if controller == "cpu": + return CpuCgroup(extension_name, cgroup_path) + if controller == "memory": + return MemoryCgroup(extension_name, cgroup_path) + raise CGroupsException('CGroup controller {0} is not supported'.format(controller)) + + def __init__(self, name, cgroup_path, controller_type): + """ + Initialize _data collection for the Memory controller + :param: name: Name of the CGroup + :param: cgroup_path: Path of the controller + :param: controller_type: + :return: + """ + self.name = name + self.path = cgroup_path + self.controller = controller_type + + def _get_cgroup_file(self, file_name): + return os.path.join(self.path, file_name) + + def _get_file_contents(self, file_name): + """ + Retrieve the contents to file. + + :param str file_name: Name of file within that metric controller + :return: Entire contents of the file + :rtype: str + """ + + parameter_file = self._get_cgroup_file(file_name) + + try: + return fileutil.read_file(parameter_file) + except Exception: + raise + + def _get_parameters(self, parameter_name, first_line_only=False): + """ + Retrieve the values of a parameter from a controller. + Returns a list of values in the file. + + :param first_line_only: return only the first line. + :param str parameter_name: Name of file within that metric controller + :return: The first line of the file, without line terminator + :rtype: [str] + """ + result = [] + try: + values = self._get_file_contents(parameter_name).splitlines() + result = values[0] if first_line_only else values + except IndexError: + parameter_filename = self._get_cgroup_file(parameter_name) + logger.error("File {0} is empty but should not be".format(parameter_filename)) + raise CGroupsException("File {0} is empty but should not be".format(parameter_filename)) + except Exception as e: + if isinstance(e, (IOError, OSError)) and e.errno == errno.ENOENT: + raise e + parameter_filename = self._get_cgroup_file(parameter_name) + raise CGroupsException("Exception while attempting to read {0}".format(parameter_filename), e) + return result + + def is_active(self): + try: + tasks = self._get_parameters("tasks") + if tasks: + return len(tasks) != 0 + except (IOError, OSError) as e: + if e.errno == errno.ENOENT: + # only suppressing file not found exceptions. + pass + else: + logger.periodic_warn(logger.EVERY_HALF_HOUR, + 'Could not get list of tasks from "tasks" file in the cgroup: {0}.' + ' Internal error: {1}'.format(self.path, ustr(e))) + except CGroupsException as e: + logger.periodic_warn(logger.EVERY_HALF_HOUR, + 'Could not get list of tasks from "tasks" file in the cgroup: {0}.' + ' Internal error: {1}'.format(self.path, ustr(e))) + return False + + return False + + +class CpuCgroup(CGroup): + def __init__(self, name, cgroup_path): + """ + Initialize _data collection for the Cpu controller. User must call update() before attempting to get + any useful metrics. + + :return: CpuCgroup + """ + super(CpuCgroup, self).__init__(name, cgroup_path, "cpu") + + self._osutil = get_osutil() + self._current_cpu_total = 0 + self._previous_cpu_total = 0 + self._current_system_cpu = self._osutil.get_total_cpu_ticks_since_boot() + self._previous_system_cpu = 0 + + def __str__(self): + return "cgroup: Name: {0}, cgroup_path: {1}; Controller: {2}".format( + self.name, self.path, self.controller + ) + + def _get_current_cpu_total(self): + """ + Compute the number of USER_HZ of CPU time (user and system) consumed by this cgroup since boot. + + :return: int + """ + cpu_total = 0 + try: + cpu_stat = self._get_file_contents('cpuacct.stat') + except Exception as e: + if isinstance(e, (IOError, OSError)) and e.errno == errno.ENOENT: + raise e + raise CGroupsException("Exception while attempting to read {0}".format("cpuacct.stat"), e) + + if cpu_stat: + m = re_user_system_times.match(cpu_stat) + if m: + cpu_total = int(m.groups()[0]) + int(m.groups()[1]) + return cpu_total + + def _update_cpu_data(self): + """ + Update all raw _data required to compute metrics of interest. The intent is to call update() once, then + call the various get_*() methods which use this _data, which we've collected exactly once. + """ + self._previous_cpu_total = self._current_cpu_total + self._previous_system_cpu = self._current_system_cpu + self._current_cpu_total = self._get_current_cpu_total() + self._current_system_cpu = self._osutil.get_total_cpu_ticks_since_boot() + + def _get_cpu_percent(self): + """ + Compute the percent CPU time used by this cgroup over the elapsed time since the last time this instance was + update()ed. If the cgroup fully consumed 2 cores on a 4 core system, return 200. + + :return: CPU usage in percent of a single core + :rtype: float + """ + cpu_delta = self._current_cpu_total - self._previous_cpu_total + system_delta = max(1, self._current_system_cpu - self._previous_system_cpu) + + return round(float(cpu_delta * self._osutil.get_processor_cores() * 100) / float(system_delta), 3) + + def get_cpu_usage(self): + """ + Collects and return the cpu usage. + + :rtype: float + """ + self._update_cpu_data() + return self._get_cpu_percent() + + +class MemoryCgroup(CGroup): + def __init__(self, name, cgroup_path): + """ + Initialize _data collection for the Memory controller + + :return: MemoryCgroup + """ + super(MemoryCgroup, self).__init__(name, cgroup_path, "memory") + + def __str__(self): + return "cgroup: Name: {0}, cgroup_path: {1}; Controller: {2}".format( + self.name, self.path, self.controller + ) + + def get_memory_usage(self): + """ + Collect memory.usage_in_bytes from the cgroup. + + :return: Memory usage in bytes + :rtype: int + """ + usage = None + + try: + usage = self._get_parameters('memory.usage_in_bytes', first_line_only=True) + except (IOError, OSError) as e: + if e.errno == errno.ENOENT: + # only suppressing file not found exceptions. + pass + else: + raise e + + if not usage: + usage = "0" + return int(usage) + + def get_max_memory_usage(self): + """ + Collect memory.usage_in_bytes from the cgroup. + + :return: Memory usage in bytes + :rtype: int + """ + usage = None + try: + usage = self._get_parameters('memory.max_usage_in_bytes', first_line_only=True) + except (IOError, OSError) as e: + if e.errno == errno.ENOENT: + # only suppressing file not found exceptions. + pass + else: + raise e + if not usage: + usage = "0" + return int(usage) diff -Nru waagent-2.2.34/azurelinuxagent/common/cgroupapi.py waagent-2.2.45/azurelinuxagent/common/cgroupapi.py --- waagent-2.2.34/azurelinuxagent/common/cgroupapi.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/cgroupapi.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,566 @@ +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ + +import errno +import os +import shutil +import subprocess +import uuid + +from azurelinuxagent.common import logger +from azurelinuxagent.common.cgroup import CGroup +from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry +from azurelinuxagent.common.conf import get_agent_pid_file_path +from azurelinuxagent.common.event import add_event, WALAEventOperation +from azurelinuxagent.common.exception import CGroupsException, ExtensionErrorCodes, ExtensionError, \ + ExtensionOperationError +from azurelinuxagent.common.future import ustr +from azurelinuxagent.common.utils import fileutil, shellutil +from azurelinuxagent.common.utils.extensionprocessutil import handle_process_completion, read_output +from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION + +CGROUPS_FILE_SYSTEM_ROOT = '/sys/fs/cgroup' +CGROUP_CONTROLLERS = ["cpu", "memory"] +VM_AGENT_CGROUP_NAME = "walinuxagent.service" +EXTENSIONS_ROOT_CGROUP_NAME = "walinuxagent.extensions" +UNIT_FILES_FILE_SYSTEM_PATH = "/etc/systemd/system" + + +class CGroupsApi(object): + """ + Interface for the cgroups API + """ + def create_agent_cgroups(self): + raise NotImplementedError() + + def create_extension_cgroups_root(self): + raise NotImplementedError() + + def create_extension_cgroups(self, extension_name): + raise NotImplementedError() + + def remove_extension_cgroups(self, extension_name): + raise NotImplementedError() + + def get_extension_cgroups(self, extension_name): + raise NotImplementedError() + + def start_extension_command(self, extension_name, command, timeout, shell, cwd, env, stdout, stderr, error_code): + raise NotImplementedError() + + def cleanup_legacy_cgroups(self): + raise NotImplementedError() + + @staticmethod + def track_cgroups(extension_cgroups): + try: + for cgroup in extension_cgroups: + CGroupsTelemetry.track_cgroup(cgroup) + except Exception as e: + logger.warn("Cannot add cgroup '{0}' to tracking list; resource usage will not be tracked. " + "Error: {1}".format(cgroup.path, ustr(e))) + + @staticmethod + def _get_extension_cgroup_name(extension_name): + # Since '-' is used as a separator in systemd unit names, we replace it with '_' to prevent side-effects. + return extension_name.replace('-', '_') + + @staticmethod + def create(): + """ + Factory method to create the correct API for the current platform + """ + return SystemdCgroupsApi() if CGroupsApi._is_systemd() else FileSystemCgroupsApi() + + @staticmethod + def _is_systemd(): + """ + Determine if systemd is managing system services; the implementation follows the same strategy as, for example, + sd_booted() in libsystemd, or /usr/sbin/service + """ + return os.path.exists('/run/systemd/system/') + + @staticmethod + def _foreach_controller(operation, message): + """ + Executes the given operation on all controllers that need to be tracked; outputs 'message' if the controller + is not mounted or if an error occurs in the operation + :return: Returns a list of error messages or an empty list if no errors occurred + """ + mounted_controllers = os.listdir(CGROUPS_FILE_SYSTEM_ROOT) + + for controller in CGROUP_CONTROLLERS: + try: + if controller not in mounted_controllers: + logger.warn('Cgroup controller "{0}" is not mounted. {1}', controller, message) + else: + operation(controller) + except Exception as e: + logger.warn('Error in cgroup controller "{0}": {1}. {2}', controller, ustr(e), message) + + @staticmethod + def _foreach_legacy_cgroup(operation): + """ + Previous versions of the daemon (2.2.31-2.2.40) wrote their PID to /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent; + starting from version 2.2.41 we track the agent service in walinuxagent.service instead of WALinuxAgent/WALinuxAgent. Also, + when running under systemd, the PIDs should not be explicitly moved to the cgroup filesystem. The older daemons would + incorrectly do that under certain conditions. + + This method checks for the existence of the legacy cgroups and, if the daemon's PID has been added to them, executes the + given operation on the cgroups. After this check, the method attempts to remove the legacy cgroups. + + :param operation: + The function to execute on each legacy cgroup. It must take 2 arguments: the controller and the daemon's PID + """ + legacy_cgroups = [] + for controller in ['cpu', 'memory']: + cgroup = os.path.join(CGROUPS_FILE_SYSTEM_ROOT, controller, "WALinuxAgent", "WALinuxAgent") + if os.path.exists(cgroup): + logger.info('Found legacy cgroup {0}', cgroup) + legacy_cgroups.append((controller, cgroup)) + + try: + for controller, cgroup in legacy_cgroups: + procs_file = os.path.join(cgroup, "cgroup.procs") + + if os.path.exists(procs_file): + procs_file_contents = fileutil.read_file(procs_file).strip() + daemon_pid = fileutil.read_file(get_agent_pid_file_path()).strip() + + if daemon_pid in procs_file_contents: + operation(controller, daemon_pid) + finally: + for _, cgroup in legacy_cgroups: + logger.info('Removing {0}', cgroup) + shutil.rmtree(cgroup, ignore_errors=True) + + +class FileSystemCgroupsApi(CGroupsApi): + """ + Cgroups interface using the cgroups file system directly + """ + @staticmethod + def _try_mkdir(path): + """ + Try to create a directory, recursively. If it already exists as such, do nothing. Raise the appropriate + exception should an error occur. + + :param path: str + """ + if not os.path.isdir(path): + try: + os.makedirs(path, 0o755) + except OSError as e: + if e.errno == errno.EEXIST: + if not os.path.isdir(path): + raise CGroupsException("Create directory for cgroup {0}: normal file already exists with that name".format(path)) + else: + pass # There was a race to create the directory, but it's there now, and that's fine + elif e.errno == errno.EACCES: + # This is unexpected, as the agent runs as root + raise CGroupsException("Create directory for cgroup {0}: permission denied".format(path)) + else: + raise + + @staticmethod + def _get_agent_cgroup_path(controller): + return os.path.join(CGROUPS_FILE_SYSTEM_ROOT, controller, VM_AGENT_CGROUP_NAME) + + @staticmethod + def _get_extension_cgroups_root_path(controller): + return os.path.join(CGROUPS_FILE_SYSTEM_ROOT, controller, EXTENSIONS_ROOT_CGROUP_NAME) + + def _get_extension_cgroup_path(self, controller, extension_name): + extensions_root = self._get_extension_cgroups_root_path(controller) + + if not os.path.exists(extensions_root): + logger.warn("Root directory {0} does not exist.".format(extensions_root)) + + cgroup_name = self._get_extension_cgroup_name(extension_name) + + return os.path.join(extensions_root, cgroup_name) + + def _create_extension_cgroup(self, controller, extension_name): + return CGroup.create(self._get_extension_cgroup_path(controller, extension_name), controller, extension_name) + + @staticmethod + def _add_process_to_cgroup(pid, cgroup_path): + tasks_file = os.path.join(cgroup_path, 'cgroup.procs') + fileutil.append_file(tasks_file, "{0}\n".format(pid)) + logger.info("Added PID {0} to cgroup {1}".format(pid, cgroup_path)) + + def cleanup_legacy_cgroups(self): + """ + Previous versions of the daemon (2.2.31-2.2.40) wrote their PID to /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent; + starting from version 2.2.41 we track the agent service in walinuxagent.service instead of WALinuxAgent/WALinuxAgent. This + method moves the daemon's PID from the legacy cgroups to the newer cgroups. + """ + def move_daemon_pid(controller, daemon_pid): + new_path = FileSystemCgroupsApi._get_agent_cgroup_path(controller) + logger.info("Writing daemon's PID ({0}) to {1}", daemon_pid, new_path) + fileutil.append_file(os.path.join(new_path, "cgroup.procs"), daemon_pid) + msg = "Moved daemon's PID from legacy cgroup to {0}".format(new_path) + add_event(AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.CGroupsCleanUp, is_success=True, message=msg) + + CGroupsApi._foreach_legacy_cgroup(move_daemon_pid) + + def create_agent_cgroups(self): + """ + Creates a cgroup for the VM Agent in each of the controllers we are tracking; returns the created cgroups. + """ + cgroups = [] + + pid = int(os.getpid()) + + def create_cgroup(controller): + path = FileSystemCgroupsApi._get_agent_cgroup_path(controller) + + if not os.path.isdir(path): + FileSystemCgroupsApi._try_mkdir(path) + logger.info("Created cgroup {0}".format(path)) + + self._add_process_to_cgroup(pid, path) + + cgroups.append(CGroup.create(path, controller, VM_AGENT_CGROUP_NAME)) + + self._foreach_controller(create_cgroup, 'Failed to create a cgroup for the VM Agent; resource usage will not be tracked') + + if len(cgroups) == 0: + raise CGroupsException("Failed to create any cgroup for the VM Agent") + + return cgroups + + def create_extension_cgroups_root(self): + """ + Creates the directory within the cgroups file system that will contain the cgroups for the extensions. + """ + def create_cgroup(controller): + path = self._get_extension_cgroups_root_path(controller) + + if not os.path.isdir(path): + FileSystemCgroupsApi._try_mkdir(path) + logger.info("Created {0}".format(path)) + + self._foreach_controller(create_cgroup, 'Failed to create a root cgroup for extensions') + + def create_extension_cgroups(self, extension_name): + """ + Creates a cgroup for the given extension in each of the controllers we are tracking; returns the created cgroups. + """ + cgroups = [] + + def create_cgroup(controller): + cgroup = self._create_extension_cgroup(controller, extension_name) + + if not os.path.isdir(cgroup.path): + FileSystemCgroupsApi._try_mkdir(cgroup.path) + logger.info("Created cgroup {0}".format(cgroup.path)) + + cgroups.append(cgroup) + + self._foreach_controller(create_cgroup, 'Failed to create a cgroup for extension {0}'.format(extension_name)) + + return cgroups + + def remove_extension_cgroups(self, extension_name): + """ + Deletes the cgroups for the given extension. + """ + def remove_cgroup(controller): + path = self._get_extension_cgroup_path(controller, extension_name) + + if os.path.exists(path): + try: + os.rmdir(path) + logger.info('Deleted cgroup "{0}".'.format(path)) + except OSError as exception: + if exception.errno == 16: # [Errno 16] Device or resource busy + logger.warn('CGroup "{0}" still has active tasks; will not remove it.'.format(path)) + + self._foreach_controller(remove_cgroup, 'Failed to delete cgroups for extension {0}'.format(extension_name)) + + def get_extension_cgroups(self, extension_name): + """ + Returns the cgroups for the given extension. + """ + + cgroups = [] + + def get_cgroup(controller): + cgroup = self._create_extension_cgroup(controller, extension_name) + cgroups.append(cgroup) + + self._foreach_controller(get_cgroup, 'Failed to retrieve cgroups for extension {0}'.format(extension_name)) + + return cgroups + + def start_extension_command(self, extension_name, command, timeout, shell, cwd, env, stdout, stderr, + error_code=ExtensionErrorCodes.PluginUnknownFailure): + """ + Starts a command (install/enable/etc) for an extension and adds the command's PID to the extension's cgroup + :param extension_name: The extension executing the command + :param command: The command to invoke + :param timeout: Number of seconds to wait for command completion + :param cwd: The working directory for the command + :param env: The environment to pass to the command's process + :param stdout: File object to redirect stdout to + :param stderr: File object to redirect stderr to + :param error_code: Extension error code to raise in case of error + """ + try: + extension_cgroups = self.create_extension_cgroups(extension_name) + except Exception as exception: + extension_cgroups = [] + logger.warn("Failed to create cgroups for extension '{0}'; resource usage will not be tracked. " + "Error: {1}".format(extension_name, ustr(exception))) + + def pre_exec_function(): + os.setsid() + + try: + pid = os.getpid() + + for cgroup in extension_cgroups: + try: + self._add_process_to_cgroup(pid, cgroup.path) + except Exception as exception: + logger.warn("Failed to add PID {0} to the cgroups for extension '{1}'. " + "Resource usage will not be tracked. Error: {2}".format(pid, + extension_name, + ustr(exception))) + except Exception as e: + logger.warn("Failed to add extension {0} to its cgroup. Resource usage will not be tracked. " + "Error: {1}".format(extension_name, ustr(e))) + + process = subprocess.Popen(command, + shell=shell, + cwd=cwd, + env=env, + stdout=stdout, + stderr=stderr, + preexec_fn=pre_exec_function) + + self.track_cgroups(extension_cgroups) + process_output = handle_process_completion(process=process, + command=command, + timeout=timeout, + stdout=stdout, + stderr=stderr, + error_code=error_code) + + return extension_cgroups, process_output + + +class SystemdCgroupsApi(CGroupsApi): + """ + Cgroups interface via systemd + """ + + @staticmethod + def create_and_start_unit(unit_filename, unit_contents): + try: + unit_path = os.path.join(UNIT_FILES_FILE_SYSTEM_PATH, unit_filename) + fileutil.write_file(unit_path, unit_contents) + shellutil.run_command(["systemctl", "daemon-reload"]) + shellutil.run_command(["systemctl", "start", unit_filename]) + except Exception as e: + raise CGroupsException("Failed to create and start {0}. Error: {1}".format(unit_filename, ustr(e))) + + @staticmethod + def _get_extensions_slice_root_name(): + return "system-{0}.slice".format(EXTENSIONS_ROOT_CGROUP_NAME) + + def _get_extension_slice_name(self, extension_name): + return "system-{0}-{1}.slice".format(EXTENSIONS_ROOT_CGROUP_NAME, self._get_extension_cgroup_name(extension_name)) + + def create_agent_cgroups(self): + try: + cgroup_unit = None + cgroup_paths = fileutil.read_file("/proc/self/cgroup") + for entry in cgroup_paths.splitlines(): + fields = entry.split(':') + if fields[1] == "name=systemd": + cgroup_unit = fields[2].lstrip(os.path.sep) + + cpu_cgroup_path = os.path.join(CGROUPS_FILE_SYSTEM_ROOT, 'cpu', cgroup_unit) + memory_cgroup_path = os.path.join(CGROUPS_FILE_SYSTEM_ROOT, 'memory', cgroup_unit) + + return [CGroup.create(cpu_cgroup_path, 'cpu', VM_AGENT_CGROUP_NAME), + CGroup.create(memory_cgroup_path, 'memory', VM_AGENT_CGROUP_NAME)] + except Exception as e: + raise CGroupsException("Failed to get paths of agent's cgroups. Error: {0}".format(ustr(e))) + + def create_extension_cgroups_root(self): + unit_contents = """ +[Unit] +Description=Slice for walinuxagent extensions +DefaultDependencies=no +Before=slices.target +Requires=system.slice +After=system.slice""" + unit_filename = self._get_extensions_slice_root_name() + self.create_and_start_unit(unit_filename, unit_contents) + logger.info("Created slice for walinuxagent extensions {0}".format(unit_filename)) + + def create_extension_cgroups(self, extension_name): + # TODO: The slice created by this function is not used currently. We need to create the extension scopes within + # this slice and use the slice to monitor the cgroups. Also see comment in get_extension_cgroups. + # the slice. + unit_contents = """ +[Unit] +Description=Slice for extension {0} +DefaultDependencies=no +Before=slices.target +Requires=system-{1}.slice +After=system-{1}.slice""".format(extension_name, EXTENSIONS_ROOT_CGROUP_NAME) + unit_filename = self._get_extension_slice_name(extension_name) + self.create_and_start_unit(unit_filename, unit_contents) + logger.info("Created slice for {0}".format(unit_filename)) + + return self.get_extension_cgroups(extension_name) + + def remove_extension_cgroups(self, extension_name): + # For transient units, cgroups are released automatically when the unit stops, so it is sufficient + # to call stop on them. Persistent cgroups are released when the unit is disabled and its configuration + # file is deleted. + # The assumption is that this method is called after the extension has been uninstalled. For now, since + # we're running extensions within transient scopes which clean up after they finish running, no removal + # of units is needed. In the future, when the extension is running under its own slice, + # the following clean up is needed. + unit_filename = self._get_extension_slice_name(extension_name) + try: + unit_path = os.path.join(UNIT_FILES_FILE_SYSTEM_PATH, unit_filename) + shellutil.run_command(["systemctl", "stop", unit_filename]) + fileutil.rm_files(unit_path) + shellutil.run_command(["systemctl", "daemon-reload"]) + except Exception as e: + raise CGroupsException("Failed to remove {0}. Error: {1}".format(unit_filename, ustr(e))) + + def get_extension_cgroups(self, extension_name): + # TODO: The slice returned by this function is not used currently. We need to create the extension scopes within + # this slice and use the slice to monitor the cgroups. Also see comment in create_extension_cgroups. + slice_name = self._get_extension_cgroup_name(extension_name) + + cgroups = [] + + def create_cgroup(controller): + cpu_cgroup_path = os.path.join(CGROUPS_FILE_SYSTEM_ROOT, controller, 'system.slice', slice_name) + cgroups.append(CGroup.create(cpu_cgroup_path, controller, extension_name)) + + self._foreach_controller(create_cgroup, 'Cannot retrieve cgroup for extension {0}; resource usage will not be tracked.'.format(extension_name)) + + return cgroups + + @staticmethod + def _is_systemd_failure(scope_name, process_output): + unit_not_found = "Unit {0} not found.".format(scope_name) + return unit_not_found in process_output or scope_name not in process_output + + def start_extension_command(self, extension_name, command, timeout, shell, cwd, env, stdout, stderr, + error_code=ExtensionErrorCodes.PluginUnknownFailure): + scope_name = "{0}_{1}".format(self._get_extension_cgroup_name(extension_name), uuid.uuid4()) + + process = subprocess.Popen( + "systemd-run --unit={0} --scope {1}".format(scope_name, command), + shell=shell, + cwd=cwd, + stdout=stdout, + stderr=stderr, + env=env, + preexec_fn=os.setsid) + + logger.info("Started extension using scope '{0}'", scope_name) + extension_cgroups = [] + + def create_cgroup(controller): + cgroup_path = os.path.join(CGROUPS_FILE_SYSTEM_ROOT, controller, 'system.slice', scope_name + ".scope") + extension_cgroups.append(CGroup.create(cgroup_path, controller, extension_name)) + + self._foreach_controller(create_cgroup, 'Cannot create cgroup for extension {0}; ' + 'resource usage will not be tracked.'.format(extension_name)) + self.track_cgroups(extension_cgroups) + + # Wait for process completion or timeout + try: + process_output = handle_process_completion(process=process, + command=command, + timeout=timeout, + stdout=stdout, + stderr=stderr, + error_code=error_code) + except ExtensionError as e: + # The extension didn't terminate successfully. Determine whether it was due to systemd errors or + # extension errors. + process_output = read_output(stdout, stderr) + systemd_failure = self._is_systemd_failure(scope_name, process_output) + + if not systemd_failure: + # There was an extension error; it either timed out or returned a non-zero exit code. Re-raise the error + raise + else: + # There was an issue with systemd-run. We need to log it and retry the extension without systemd. + err_msg = 'Systemd process exited with code %s and output %s' % (e.exit_code, process_output) \ + if isinstance(e, ExtensionOperationError) else "Systemd timed-out, output: %s" % process_output + event_msg = 'Failed to run systemd-run for unit {0}.scope. ' \ + 'Will retry invoking the extension without systemd. ' \ + 'Systemd-run error: {1}'.format(scope_name, err_msg) + add_event(AGENT_NAME, + version=CURRENT_VERSION, + op=WALAEventOperation.InvokeCommandUsingSystemd, + is_success=False, + log_event=False, + message=event_msg) + logger.warn(event_msg) + + # Reset the stdout and stderr + stdout.truncate(0) + stderr.truncate(0) + + # Try invoking the process again, this time without systemd-run + logger.info('Extension invocation using systemd failed, falling back to regular invocation ' + 'without cgroups tracking.') + process = subprocess.Popen(command, + shell=shell, + cwd=cwd, + env=env, + stdout=stdout, + stderr=stderr, + preexec_fn=os.setsid) + + process_output = handle_process_completion(process=process, + command=command, + timeout=timeout, + stdout=stdout, + stderr=stderr, + error_code=error_code) + + return [], process_output + + # The process terminated in time and successfully + return extension_cgroups, process_output + + def cleanup_legacy_cgroups(self): + """ + Previous versions of the daemon (2.2.31-2.2.40) wrote their PID to /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent; + starting from version 2.2.41 we track the agent service in walinuxagent.service instead of WALinuxAgent/WALinuxAgent. If + we find that any of the legacy groups include the PID of the daemon then we disable data collection for this instance + (under systemd, moving PIDs across the cgroup file system can produce unpredictable results) + """ + def report_error(_, daemon_pid): + raise CGroupsException( + "The daemon's PID ({0}) was already added to the legacy cgroup; this invalidates resource usage data.".format(daemon_pid)) + + CGroupsApi._foreach_legacy_cgroup(report_error) diff -Nru waagent-2.2.34/azurelinuxagent/common/cgroupconfigurator.py waagent-2.2.45/azurelinuxagent/common/cgroupconfigurator.py --- waagent-2.2.34/azurelinuxagent/common/cgroupconfigurator.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/cgroupconfigurator.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,210 @@ +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ + +import os +import subprocess + +from azurelinuxagent.common import logger +from azurelinuxagent.common.cgroupapi import CGroupsApi +from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry +from azurelinuxagent.common.exception import CGroupsException, ExtensionErrorCodes +from azurelinuxagent.common.future import ustr +from azurelinuxagent.common.osutil import get_osutil +from azurelinuxagent.common.utils.extensionprocessutil import handle_process_completion +from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION +from azurelinuxagent.common.event import add_event, WALAEventOperation + + +class CGroupConfigurator(object): + """ + This class implements the high-level operations on CGroups (e.g. initialization, creation, etc) + + NOTE: with the exception of start_extension_command, none of the methods in this class raise exceptions (cgroup operations should not block extensions) + """ + class __impl(object): + def __init__(self): + """ + Ensures the cgroups file system is mounted and selects the correct API to interact with it + """ + osutil = get_osutil() + + self._cgroups_supported = osutil.is_cgroups_supported() + + if self._cgroups_supported: + self._enabled = True + try: + osutil.mount_cgroups() + self._cgroups_api = CGroupsApi.create() + status = "The cgroup filesystem is ready to use" + except Exception as e: + status = ustr(e) + self._enabled = False + else: + self._enabled = False + self._cgroups_api = None + status = "Cgroups are not supported by the platform" + + logger.info("CGroups Status: {0}".format(status)) + + add_event( + AGENT_NAME, + version=CURRENT_VERSION, + op=WALAEventOperation.InitializeCGroups, + is_success=self._enabled, + message=status, + log_event=False) + + def enabled(self): + return self._enabled + + def enable(self): + if not self._cgroups_supported: + raise CGroupsException("cgroups are not supported on the current platform") + + self._enabled = True + + def disable(self): + self._enabled = False + + def _invoke_cgroup_operation(self, operation, error_message, on_error=None): + """ + Ensures the given operation is invoked only if cgroups are enabled and traps any errors on the operation. + """ + if not self.enabled(): + return + + try: + return operation() + except Exception as e: + logger.warn("{0} Error: {1}".format(error_message, ustr(e))) + if on_error is not None: + try: + on_error(e) + except Exception as ex: + logger.warn("CGroupConfigurator._invoke_cgroup_operation: {0}".format(ustr(e))) + + def create_agent_cgroups(self, track_cgroups): + """ + Creates and returns the cgroups needed to track the VM Agent + """ + def __impl(): + cgroups = self._cgroups_api.create_agent_cgroups() + + if track_cgroups: + for cgroup in cgroups: + CGroupsTelemetry.track_cgroup(cgroup) + + return cgroups + + self._invoke_cgroup_operation(__impl, "Failed to create a cgroup for the VM Agent; resource usage for the Agent will not be tracked.") + + def cleanup_legacy_cgroups(self): + def __impl(): + self._cgroups_api.cleanup_legacy_cgroups() + + message = 'Failed to process legacy cgroups. Collection of resource usage data will be disabled.' + + def disable_cgroups(exception): + self.disable() + CGroupsTelemetry.reset() + add_event( + AGENT_NAME, + version=CURRENT_VERSION, + op=WALAEventOperation.CGroupsCleanUp, + is_success=False, + log_event=False, + message='{0} {1}'.format(message, ustr(exception))) + + self._invoke_cgroup_operation(__impl, message, on_error=disable_cgroups) + + def create_extension_cgroups_root(self): + """ + Creates the container (directory/cgroup) that includes the cgroups for all extensions (/sys/fs/cgroup/*/walinuxagent.extensions) + """ + def __impl(): + self._cgroups_api.create_extension_cgroups_root() + + self._invoke_cgroup_operation(__impl, "Failed to create a root cgroup for extensions; resource usage for extensions will not be tracked.") + + def create_extension_cgroups(self, name): + """ + Creates and returns the cgroups for the given extension + """ + def __impl(): + return self._cgroups_api.create_extension_cgroups(name) + + return self._invoke_cgroup_operation(__impl, "Failed to create a cgroup for extension '{0}'; resource usage will not be tracked.".format(name)) + + def remove_extension_cgroups(self, name): + """ + Deletes the cgroup for the given extension + """ + def __impl(): + cgroups = self._cgroups_api.remove_extension_cgroups(name) + return cgroups + + self._invoke_cgroup_operation(__impl, "Failed to delete cgroups for extension '{0}'.".format(name)) + + def start_extension_command(self, extension_name, command, timeout, shell, cwd, env, stdout, stderr, + error_code=ExtensionErrorCodes.PluginUnknownFailure): + """ + Starts a command (install/enable/etc) for an extension and adds the command's PID to the extension's cgroup + :param extension_name: The extension executing the command + :param command: The command to invoke + :param timeout: Number of seconds to wait for command completion + :param cwd: The working directory for the command + :param env: The environment to pass to the command's process + :param stdout: File object to redirect stdout to + :param stderr: File object to redirect stderr to + :param stderr: File object to redirect stderr to + :param error_code: Extension error code to raise in case of error + """ + if not self.enabled(): + process = subprocess.Popen(command, + shell=shell, + cwd=cwd, + env=env, + stdout=stdout, + stderr=stderr, + preexec_fn=os.setsid) + + process_output = handle_process_completion(process=process, + command=command, + timeout=timeout, + stdout=stdout, + stderr=stderr, + error_code=error_code) + else: + extension_cgroups, process_output = self._cgroups_api.start_extension_command(extension_name, + command, + timeout, + shell=shell, + cwd=cwd, + env=env, + stdout=stdout, + stderr=stderr, + error_code=error_code) + + return process_output + + # unique instance for the singleton (TODO: find a better pattern for a singleton) + _instance = None + + @staticmethod + def get_instance(): + if CGroupConfigurator._instance is None: + CGroupConfigurator._instance = CGroupConfigurator.__impl() + return CGroupConfigurator._instance diff -Nru waagent-2.2.34/azurelinuxagent/common/cgroups.py waagent-2.2.45/azurelinuxagent/common/cgroups.py --- waagent-2.2.34/azurelinuxagent/common/cgroups.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/cgroups.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,812 +0,0 @@ -# Copyright 2018 Microsoft Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Requires Python 2.6+ and Openssl 1.0+ - -import errno -import os -import re - -import time - -from azurelinuxagent.common import logger, conf -from azurelinuxagent.common.future import ustr -from azurelinuxagent.common.osutil import get_osutil -from azurelinuxagent.common.osutil.default import BASE_CGROUPS -from azurelinuxagent.common.utils import fileutil -from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION - - -WRAPPER_CGROUP_NAME = "Agent+Extensions" -METRIC_HIERARCHIES = ['cpu', 'memory'] -MEMORY_DEFAULT = -1 - -# percentage of a single core -DEFAULT_CPU_LIMIT_AGENT = 10 -DEFAULT_CPU_LIMIT_EXT = 40 - -DEFAULT_MEM_LIMIT_MIN_MB = 256 # mb, applies to agent and extensions -DEFAULT_MEM_LIMIT_MAX_MB = 512 # mb, applies to agent only -DEFAULT_MEM_LIMIT_PCT = 15 # percent, applies to extensions - -re_user_system_times = re.compile('user (\d+)\nsystem (\d+)\n') - -related_services = { - "Microsoft.OSTCExtensions.LinuxDiagnostic": ["omid", "omsagent-LAD", "mdsd-lde"], - "Microsoft.Azure.Diagnostics.LinuxDiagnostic": ["omid", "omsagent-LAD", "mdsd-lde"], -} - - -class CGroupsException(Exception): - - def __init__(self, msg): - self.msg = msg - - def __str__(self): - return repr(self.msg) - - -# The metric classes (Cpu, Memory, etc) can all assume that CGroups is enabled, as the CGroupTelemetry -# class is very careful not to call them if CGroups isn't enabled. Any tests should be disabled if the osutil -# is_cgroups_support() method returns false. - - -class Cpu(object): - def __init__(self, cgt): - """ - Initialize data collection for the Cpu hierarchy. User must call update() before attempting to get - any useful metrics. - - :param cgt: CGroupsTelemetry - :return: - """ - self.cgt = cgt - self.osutil = get_osutil() - self.current_cpu_total = self.get_current_cpu_total() - self.previous_cpu_total = 0 - self.current_system_cpu = self.osutil.get_total_cpu_ticks_since_boot() - self.previous_system_cpu = 0 - - def __str__(self): - return "Cgroup: Current {0}, previous {1}; System: Current {2}, previous {3}".format( - self.current_cpu_total, self.previous_cpu_total, self.current_system_cpu, self.previous_system_cpu - ) - - def get_current_cpu_total(self): - """ - Compute the number of USER_HZ of CPU time (user and system) consumed by this cgroup since boot. - - :return: int - """ - cpu_total = 0 - try: - cpu_stat = self.cgt.cgroup.\ - get_file_contents('cpu', 'cpuacct.stat') - if cpu_stat is not None: - m = re_user_system_times.match(cpu_stat) - if m: - cpu_total = int(m.groups()[0]) + int(m.groups()[1]) - except CGroupsException: - # There are valid reasons for file contents to be unavailable; for example, if an extension - # has not yet started (or has stopped) an associated service on a VM using systemd, the cgroup for - # the service will not exist ('cause systemd will tear it down). This might be a transient or a - # long-lived state, so there's no point in logging it, much less emitting telemetry. - pass - return cpu_total - - def update(self): - """ - Update all raw data required to compute metrics of interest. The intent is to call update() once, then - call the various get_*() methods which use this data, which we've collected exactly once. - """ - self.previous_cpu_total = self.current_cpu_total - self.previous_system_cpu = self.current_system_cpu - self.current_cpu_total = self.get_current_cpu_total() - self.current_system_cpu = self.osutil.get_total_cpu_ticks_since_boot() - - def get_cpu_percent(self): - """ - Compute the percent CPU time used by this cgroup over the elapsed time since the last time this instance was - update()ed. If the cgroup fully consumed 2 cores on a 4 core system, return 200. - - :return: CPU usage in percent of a single core - :rtype: float - """ - cpu_delta = self.current_cpu_total - self.previous_cpu_total - system_delta = max(1, self.current_system_cpu - self.previous_system_cpu) - - return round(float(cpu_delta * self.cgt.cpu_count * 100) / float(system_delta), 3) - - def collect(self): - """ - Collect and return a list of all cpu metrics. If no metrics are collected, return an empty list. - - :rtype: [(str, str, float)] - """ - self.update() - usage = self.get_cpu_percent() - return [("Process", "% Processor Time", usage)] - - -class Memory(object): - def __init__(self, cgt): - """ - Initialize data collection for the Memory hierarchy - - :param CGroupsTelemetry cgt: The telemetry object for which memory metrics should be collected - :return: - """ - self.cgt = cgt - - def get_memory_usage(self): - """ - Collect memory.usage_in_bytes from the cgroup. - - :return: Memory usage in bytes - :rtype: int - """ - usage = self.cgt.cgroup.get_parameter('memory', 'memory.usage_in_bytes') - if not usage: - usage = "0" - return int(usage) - - def collect(self): - """ - Collect and return a list of all memory metrics - - :rtype: [(str, str, float)] - """ - usage = self.get_memory_usage() - return [("Memory", "Total Memory Usage", usage)] - - -class CGroupsTelemetry(object): - """ - Encapsulate the cgroup-based telemetry for the agent or one of its extensions, or for the aggregation across - the agent and all of its extensions. These objects should have lifetimes that span the time window over which - measurements are desired; in general, they're not terribly effective at providing instantaneous measurements. - """ - _tracked = {} - _metrics = { - "cpu": Cpu, - "memory": Memory - } - _hierarchies = list(_metrics.keys()) - - tracked_names = set() - - @staticmethod - def metrics_hierarchies(): - return CGroupsTelemetry._hierarchies - - @staticmethod - def track_cgroup(cgroup): - """ - Create a CGroupsTelemetry object to track a particular CGroups instance. Typical usage: - 1) Create a CGroups object - 2) Ask CGroupsTelemetry to track it - 3) Tell the CGroups object to add one or more processes (or let systemd handle that, for its cgroups) - - :param CGroups cgroup: The cgroup to track - """ - name = cgroup.name - if CGroups.enabled() and not CGroupsTelemetry.is_tracked(name): - tracker = CGroupsTelemetry(name, cgroup=cgroup) - CGroupsTelemetry._tracked[name] = tracker - - @staticmethod - def track_systemd_service(name): - """ - If not already tracking it, create the CGroups object for a systemd service and track it. - - :param str name: Service name (without .service suffix) to be tracked. - """ - service_name = "{0}.service".format(name).lower() - if CGroups.enabled() and not CGroupsTelemetry.is_tracked(service_name): - cgroup = CGroups.for_systemd_service(service_name) - tracker = CGroupsTelemetry(service_name, cgroup=cgroup) - CGroupsTelemetry._tracked[service_name] = tracker - - @staticmethod - def track_extension(name, cgroup=None): - """ - Create all required CGroups to track all metrics for an extension and its associated services. - - :param str name: Full name of the extension to be tracked - :param CGroups cgroup: CGroup for the extension itself. This method will create it if none is supplied. - """ - if not CGroups.enabled(): - return - - if not CGroupsTelemetry.is_tracked(name): - cgroup = CGroups.for_extension(name) if cgroup is None else cgroup - logger.info("Now tracking cgroup {0}".format(name)) - cgroup.set_limits() - CGroupsTelemetry.track_cgroup(cgroup) - if CGroups.is_systemd_manager(): - if name in related_services: - for service_name in related_services[name]: - CGroupsTelemetry.track_systemd_service(service_name) - - @staticmethod - def track_agent(): - """ - Create and track the correct cgroup for the agent itself. The actual cgroup depends on whether systemd - is in use, but the caller doesn't need to know that. - """ - if not CGroups.enabled(): - return - if CGroups.is_systemd_manager(): - CGroupsTelemetry.track_systemd_service(AGENT_NAME) - else: - CGroupsTelemetry.track_cgroup(CGroups.for_extension(AGENT_NAME)) - - @staticmethod - def is_tracked(name): - return name in CGroupsTelemetry._tracked - - @staticmethod - def stop_tracking(name): - """ - Stop tracking telemetry for the CGroups associated with an extension. If any system services are being - tracked, those will continue to be tracked; multiple extensions might rely upon the same service. - - :param str name: Extension to be dropped from tracking - """ - if CGroupsTelemetry.is_tracked(name): - del (CGroupsTelemetry._tracked[name]) - - @staticmethod - def collect_all_tracked(): - """ - Return a dictionary mapping from the name of a tracked cgroup to the list of collected metrics for that cgroup. - Collecting metrics is not guaranteed to be a fast operation; it's possible some other thread might add or remove - tracking for a cgroup while we're doing it. To avoid "dictionary changed size during iteration" exceptions, - work from a shallow copy of the _tracked dictionary. - - :returns: Dictionary of list collected metrics (metric class, metric name, value), by cgroup - :rtype: dict(str: [(str, str, float)]) - """ - results = {} - limits = {} - - for cgroup_name, collector in CGroupsTelemetry._tracked.copy().items(): - cgroup_name = cgroup_name if cgroup_name else WRAPPER_CGROUP_NAME - results[cgroup_name] = collector.collect() - limits[cgroup_name] = collector.cgroup.threshold - - return results, limits - - @staticmethod - def update_tracked(ext_handlers): - """ - Track CGroups for all enabled extensions. - Track CGroups for services created by enabled extensions. - Stop tracking CGroups for not-enabled extensions. - - :param List(ExtHandler) ext_handlers: - """ - if not CGroups.enabled(): - return - - not_enabled_extensions = set() - for extension in ext_handlers: - if extension.properties.state == u"enabled": - CGroupsTelemetry.track_extension(extension.name) - else: - not_enabled_extensions.add(extension.name) - - names_now_tracked = set(CGroupsTelemetry._tracked.keys()) - if CGroupsTelemetry.tracked_names != names_now_tracked: - now_tracking = " ".join("[{0}]".format(name) for name in sorted(names_now_tracked)) - if len(now_tracking): - logger.info("After updating cgroup telemetry, tracking {0}".format(now_tracking)) - else: - logger.warn("After updating cgroup telemetry, tracking no cgroups.") - CGroupsTelemetry.tracked_names = names_now_tracked - - def __init__(self, name, cgroup=None): - """ - Create the necessary state to collect metrics for the agent, one of its extensions, or the aggregation across - the agent and all of its extensions. To access aggregated metrics, instantiate this object with an empty string - or None. - - :param name: str - """ - if name is None: - name = "" - self.name = name - if cgroup is None: - cgroup = CGroups.for_extension(name) - self.cgroup = cgroup - self.cpu_count = CGroups.get_num_cores() - self.current_wall_time = time.time() - self.previous_wall_time = 0 - - self.data = {} - if CGroups.enabled(): - for hierarchy in CGroupsTelemetry.metrics_hierarchies(): - self.data[hierarchy] = CGroupsTelemetry._metrics[hierarchy](self) - - def collect(self): - """ - Return a list of collected metrics. Each element is a tuple of - (metric group name, metric name, metric value) - :return: [(str, str, float)] - """ - results = [] - for collector in self.data.values(): - results.extend(collector.collect()) - return results - - -class CGroups(object): - """ - This class represents the cgroup folders for the agent or an extension. This is a pretty lightweight object - without much state worth preserving; it's not unreasonable to create one just when you need it. - """ - # whether cgroup support is enabled - _enabled = True - _hierarchies = CGroupsTelemetry.metrics_hierarchies() - _use_systemd = None # Tri-state: None (i.e. "unknown"), True, False - _osutil = get_osutil() - - @staticmethod - def _construct_custom_path_for_hierarchy(hierarchy, cgroup_name): - return os.path.join(BASE_CGROUPS, hierarchy, AGENT_NAME, cgroup_name).rstrip(os.path.sep) - - @staticmethod - def _construct_systemd_path_for_hierarchy(hierarchy, cgroup_name): - return os.path.join(BASE_CGROUPS, hierarchy, 'system.slice', cgroup_name).rstrip(os.path.sep) - - @staticmethod - def for_extension(name, limits=None): - return CGroups(name, CGroups._construct_custom_path_for_hierarchy, limits) - - @staticmethod - def for_systemd_service(name, limits=None): - return CGroups(name.lower(), CGroups._construct_systemd_path_for_hierarchy, limits) - - @staticmethod - def enabled(): - return CGroups._osutil.is_cgroups_supported() and CGroups._enabled - - @staticmethod - def disable(): - CGroups._enabled = False - - @staticmethod - def enable(): - CGroups._enabled = True - - def __init__(self, name, path_maker, limits=None): - """ - Construct CGroups object. Create appropriately-named directory for each hierarchy of interest. - - :param str name: Name for the cgroup (usually the full name of the extension) - :param path_maker: Function which constructs the root path for a given hierarchy where this cgroup lives - """ - if not name or name == "": - self.name = "Agents+Extensions" - self.is_wrapper_cgroup = True - else: - self.name = name - self.is_wrapper_cgroup = False - - self.cgroups = {} - - self.threshold = CGroupsLimits(self.name, limits) - - if not self.enabled(): - return - - system_hierarchies = os.listdir(BASE_CGROUPS) - for hierarchy in CGroups._hierarchies: - if hierarchy not in system_hierarchies: - self.disable() - raise CGroupsException("Hierarchy {0} is not mounted".format(hierarchy)) - - cgroup_name = "" if self.is_wrapper_cgroup else self.name - cgroup_path = path_maker(hierarchy, cgroup_name) - if not os.path.isdir(cgroup_path): - logger.info("Creating cgroup directory {0}".format(cgroup_path)) - CGroups._try_mkdir(cgroup_path) - self.cgroups[hierarchy] = cgroup_path - - @staticmethod - def is_systemd_manager(): - """ - Determine if systemd is managing system services. Many extensions are structured as a set of services, - including the agent itself; systemd expects those services to remain in the cgroups in which it placed them. - If this process (presumed to be the agent) is in a cgroup that looks like one created by systemd, we can - assume systemd is in use. - - :return: True if systemd is managing system services - :rtype: Bool - """ - if not CGroups.enabled(): - return False - if CGroups._use_systemd is None: - hierarchy = METRIC_HIERARCHIES[0] - path = CGroups.get_my_cgroup_folder(hierarchy) - CGroups._use_systemd = path.startswith(CGroups._construct_systemd_path_for_hierarchy(hierarchy, "")) - return CGroups._use_systemd - - @staticmethod - def _try_mkdir(path): - """ - Try to create a directory, recursively. If it already exists as such, do nothing. Raise the appropriate - exception should an error occur. - - :param path: str - """ - if not os.path.isdir(path): - try: - os.makedirs(path, 0o755) - except OSError as e: - if e.errno == errno.EEXIST: - if not os.path.isdir(path): - raise CGroupsException("Create directory for cgroup {0}: " - "normal file already exists with that name".format(path)) - else: - pass # There was a race to create the directory, but it's there now, and that's fine - elif e.errno == errno.EACCES: - # This is unexpected, as the agent runs as root - raise CGroupsException("Create directory for cgroup {0}: permission denied".format(path)) - else: - raise - - def add(self, pid): - """ - Add a process to the cgroups for this agent/extension. - """ - if not self.enabled(): - return - - if self.is_wrapper_cgroup: - raise CGroupsException("Cannot add a process to the Agents+Extensions wrapper cgroup") - - if not self._osutil.check_pid_alive(pid): - raise CGroupsException('PID {0} does not exist'.format(pid)) - for hierarchy, cgroup in self.cgroups.items(): - tasks_file = self._get_cgroup_file(hierarchy, 'cgroup.procs') - fileutil.append_file(tasks_file, "{0}\n".format(pid)) - - def set_limits(self): - """ - Set per-hierarchy limits based on the cgroup name (agent or particular extension) - """ - - if not conf.get_cgroups_enforce_limits(): - return - - if self.name is None: - return - - for ext in conf.get_cgroups_excluded(): - if ext in self.name.lower(): - logger.info('No cgroups limits for {0}'.format(self.name)) - return - - cpu_limit = self.threshold.cpu_limit - mem_limit = self.threshold.memory_limit - - msg = '{0}: {1}% {2}mb'.format(self.name, cpu_limit, mem_limit) - logger.info("Setting cgroups limits for {0}".format(msg)) - success = False - - try: - self.set_cpu_limit(cpu_limit) - self.set_memory_limit(mem_limit) - success = True - except Exception as ge: - msg = '[{0}] {1}'.format(msg, ustr(ge)) - raise - finally: - from azurelinuxagent.common.event import add_event, WALAEventOperation - add_event( - AGENT_NAME, - version=CURRENT_VERSION, - op=WALAEventOperation.SetCGroupsLimits, - is_success=success, - message=msg, - log_event=False) - - @staticmethod - def _apply_wrapper_limits(path, hierarchy): - """ - Find wrapping limits for the hierarchy and apply them to the cgroup denoted by the path - - :param path: str - :param hierarchy: str - """ - pass - - @staticmethod - def _setup_wrapper_groups(): - """ - For each hierarchy, construct the wrapper cgroup and apply the appropriate limits - """ - for hierarchy in METRIC_HIERARCHIES: - root_dir = CGroups._construct_custom_path_for_hierarchy(hierarchy, "") - CGroups._try_mkdir(root_dir) - CGroups._apply_wrapper_limits(root_dir, hierarchy) - - @staticmethod - def setup(suppress_process_add=False): - """ - Only needs to be called once, and should be called from the -daemon instance of the agent. - Mount the cgroup fs if necessary - Create wrapper cgroups for agent-plus-extensions and set limits on them; - Add this process to the "agent" cgroup, if required - Actual collection of metrics from cgroups happens in the -run-exthandlers instance - """ - if CGroups.enabled(): - try: - CGroups._osutil.mount_cgroups() - if not suppress_process_add: - CGroups._setup_wrapper_groups() - pid = int(os.getpid()) - if not CGroups.is_systemd_manager(): - cg = CGroups.for_extension(AGENT_NAME) - logger.info("Add daemon process pid {0} to {1} cgroup".format(pid, cg.name)) - cg.add(pid) - cg.set_limits() - else: - cg = CGroups.for_systemd_service(AGENT_NAME) - logger.info("Add daemon process pid {0} to {1} systemd cgroup".format(pid, cg.name)) - # systemd sets limits; any limits we write would be overwritten - status = "ok" - except CGroupsException as cge: - status = cge.msg - CGroups.disable() - except Exception as ge: - status = ustr(ge) - CGroups.disable() - else: - status = "not supported by platform" - CGroups.disable() - - logger.info("CGroups: {0}".format(status)) - - from azurelinuxagent.common.event import add_event, WALAEventOperation - add_event( - AGENT_NAME, - version=CURRENT_VERSION, - op=WALAEventOperation.InitializeCGroups, - is_success=CGroups.enabled(), - message=status, - log_event=False) - - @staticmethod - def add_to_extension_cgroup(name, pid=int(os.getpid())): - """ - Create cgroup directories for this extension in each of the hierarchies and add this process to the new cgroup. - Should only be called when creating sub-processes and invoked inside the fork/exec window. As a result, - there's no point in returning the CGroups object itself; the goal is to move the child process into the - cgroup before the new code even starts running. - - :param str name: Short name of extension, suitable for naming directories in the filesystem - :param int pid: Process id of extension to be added to the cgroup - """ - if not CGroups.enabled(): - return - if name == AGENT_NAME: - logger.warn('Extension cgroup name cannot match agent cgroup name ({0})'.format(AGENT_NAME)) - return - - try: - logger.info("Move process {0} into cgroups for extension {1}".format(pid, name)) - CGroups.for_extension(name).add(pid) - except Exception as ex: - logger.warn("Unable to move process {0} into cgroups for extension {1}: {2}".format(pid, name, ex)) - - @staticmethod - def get_my_cgroup_path(hierarchy_id): - """ - Get the cgroup path "suffix" for this process for the given hierarchy ID. The leading "/" is always stripped, - so the suffix is suitable for passing to os.path.join(). (If the process is in the root cgroup, an empty - string is returned, and os.path.join() will still do the right thing.) - - :param hierarchy_id: str - :return: str - """ - cgroup_paths = fileutil.read_file("/proc/self/cgroup") - for entry in cgroup_paths.splitlines(): - fields = entry.split(':') - if fields[0] == hierarchy_id: - return fields[2].lstrip(os.path.sep) - raise CGroupsException("This process belongs to no cgroup for hierarchy ID {0}".format(hierarchy_id)) - - @staticmethod - def get_hierarchy_id(hierarchy): - """ - Get the cgroups hierarchy ID for a given hierarchy name - - :param hierarchy: - :return: str - """ - cgroup_states = fileutil.read_file("/proc/cgroups") - for entry in cgroup_states.splitlines(): - fields = entry.split('\t') - if fields[0] == hierarchy: - return fields[1] - raise CGroupsException("Cgroup hierarchy {0} not found in /proc/cgroups".format(hierarchy)) - - @staticmethod - def get_my_cgroup_folder(hierarchy): - """ - Find the path of the cgroup in which this process currently lives for the given hierarchy. - - :param hierarchy: str - :return: str - """ - hierarchy_id = CGroups.get_hierarchy_id(hierarchy) - return os.path.join(BASE_CGROUPS, hierarchy, CGroups.get_my_cgroup_path(hierarchy_id)) - - def _get_cgroup_file(self, hierarchy, file_name): - return os.path.join(self.cgroups[hierarchy], file_name) - - @staticmethod - def _convert_cpu_limit_to_fraction(value): - """ - Convert a CPU limit from percent (e.g. 50 meaning 50%) to a decimal fraction (0.50). - :return: Fraction of one CPU to be made available (e.g. 0.5 means half a core) - :rtype: float - """ - try: - limit = float(value) - except ValueError: - raise CGroupsException('CPU Limit must be convertible to a float') - - if limit <= float(0) or limit > float(CGroups.get_num_cores() * 100): - raise CGroupsException('CPU Limit must be between 0 and 100 * numCores') - - return limit / 100.0 - - def get_file_contents(self, hierarchy, file_name): - """ - Retrieve the value of a parameter from a hierarchy. - - :param str hierarchy: Name of cgroup metric hierarchy - :param str file_name: Name of file within that metric hierarchy - :return: Entire contents of the file - :rtype: str - """ - if hierarchy in self.cgroups: - parameter_file = self._get_cgroup_file(hierarchy, file_name) - - try: - return fileutil.read_file(parameter_file) - except Exception: - raise CGroupsException("Could not retrieve cgroup file {0}/{1}".format(hierarchy, file_name)) - else: - raise CGroupsException("{0} subsystem not available in cgroup {1}. cgroup paths: {2}".format( - hierarchy, self.name, self.cgroups)) - - def get_parameter(self, hierarchy, parameter_name): - """ - Retrieve the value of a parameter from a hierarchy. - Assumes the parameter is the sole line of the file. - - :param str hierarchy: Name of cgroup metric hierarchy - :param str parameter_name: Name of file within that metric hierarchy - :return: The first line of the file, without line terminator - :rtype: str - """ - result = "" - try: - values = self.get_file_contents(hierarchy, parameter_name).splitlines() - result = values[0] - except IndexError: - parameter_filename = self._get_cgroup_file(hierarchy, parameter_name) - logger.error("File {0} is empty but should not be".format(parameter_filename)) - except CGroupsException as e: - # ignore if the file does not exist yet - pass - except Exception as e: - parameter_filename = self._get_cgroup_file(hierarchy, parameter_name) - logger.error("Exception while attempting to read {0}: {1}".format(parameter_filename, ustr(e))) - return result - - def set_cpu_limit(self, limit=None): - """ - Limit this cgroup to a percentage of a single core. limit=10 means 10% of one core; 150 means 150%, which - is useful only in multi-core systems. - To limit a cgroup to utilize 10% of a single CPU, use the following commands: - # echo 10000 > /cgroup/cpu/red/cpu.cfs_quota_us - # echo 100000 > /cgroup/cpu/red/cpu.cfs_period_us - - :param limit: - """ - if not CGroups.enabled(): - return - - if limit is None: - return - - if 'cpu' in self.cgroups: - total_units = float(self.get_parameter('cpu', 'cpu.cfs_period_us')) - limit_units = int(self._convert_cpu_limit_to_fraction(limit) * total_units) - cpu_shares_file = self._get_cgroup_file('cpu', 'cpu.cfs_quota_us') - logger.verbose("writing {0} to {1}".format(limit_units, cpu_shares_file)) - fileutil.write_file(cpu_shares_file, '{0}\n'.format(limit_units)) - else: - raise CGroupsException("CPU hierarchy not available in this cgroup") - - @staticmethod - def get_num_cores(): - """ - Return the number of CPU cores exposed to this system. - - :return: int - """ - return CGroups._osutil.get_processor_cores() - - @staticmethod - def _format_memory_value(unit, limit=None): - units = {'bytes': 1, 'kilobytes': 1024, 'megabytes': 1024*1024, 'gigabytes': 1024*1024*1024} - if unit not in units: - raise CGroupsException("Unit must be one of {0}".format(units.keys())) - if limit is None: - value = MEMORY_DEFAULT - else: - try: - limit = float(limit) - except ValueError: - raise CGroupsException('Limit must be convertible to a float') - else: - value = int(limit * units[unit]) - return value - - def set_memory_limit(self, limit=None, unit='megabytes'): - if 'memory' in self.cgroups: - value = self._format_memory_value(unit, limit) - memory_limit_file = self._get_cgroup_file('memory', 'memory.limit_in_bytes') - logger.verbose("writing {0} to {1}".format(value, memory_limit_file)) - fileutil.write_file(memory_limit_file, '{0}\n'.format(value)) - else: - raise CGroupsException("Memory hierarchy not available in this cgroup") - - -class CGroupsLimits(object): - @staticmethod - def _get_value_or_default(name, threshold, limit, compute_default): - return threshold[limit] if threshold and limit in threshold else compute_default(name) - - def __init__(self, cgroup_name, threshold=None): - if not cgroup_name or cgroup_name == "": - cgroup_name = "Agents+Extensions" - - self.cpu_limit = self._get_value_or_default(cgroup_name, threshold, "cpu", CGroupsLimits.get_default_cpu_limits) - self.memory_limit = self._get_value_or_default(cgroup_name, threshold, "memory", - CGroupsLimits.get_default_memory_limits) - - @staticmethod - def get_default_cpu_limits(cgroup_name): - # default values - cpu_limit = DEFAULT_CPU_LIMIT_AGENT if AGENT_NAME.lower() in cgroup_name.lower() else DEFAULT_CPU_LIMIT_EXT - return cpu_limit - - @staticmethod - def get_default_memory_limits(cgroup_name): - os_util = get_osutil() - - # default values - mem_limit = max(DEFAULT_MEM_LIMIT_MIN_MB, round(os_util.get_total_mem() * DEFAULT_MEM_LIMIT_PCT / 100, 0)) - - # agent values - if AGENT_NAME.lower() in cgroup_name.lower(): - mem_limit = min(DEFAULT_MEM_LIMIT_MAX_MB, mem_limit) - return mem_limit diff -Nru waagent-2.2.34/azurelinuxagent/common/cgroupstelemetry.py waagent-2.2.45/azurelinuxagent/common/cgroupstelemetry.py --- waagent-2.2.34/azurelinuxagent/common/cgroupstelemetry.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/cgroupstelemetry.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,222 @@ +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +import errno +import threading +from datetime import datetime as dt + +from azurelinuxagent.common import logger +from azurelinuxagent.common.future import ustr +from azurelinuxagent.common.exception import CGroupsException + + +class CGroupsTelemetry(object): + """ + """ + _tracked = [] + _cgroup_metrics = {} + _rlock = threading.RLock() + + @staticmethod + def _get_metrics_list(metric): + return [metric.average(), metric.min(), metric.max(), metric.median(), metric.count(), + metric.first_poll_time(), metric.last_poll_time()] + + @staticmethod + def _process_cgroup_metric(cgroup_metrics): + memory_usage = cgroup_metrics.get_memory_usage() + max_memory_usage = cgroup_metrics.get_max_memory_usage() + cpu_usage = cgroup_metrics.get_cpu_usage() + + processed_extension = {} + + if cpu_usage.count() > 0: + processed_extension["cpu"] = {"cur_cpu": CGroupsTelemetry._get_metrics_list(cpu_usage)} + + if memory_usage.count() > 0: + if "memory" in processed_extension: + processed_extension["memory"]["cur_mem"] = CGroupsTelemetry._get_metrics_list(memory_usage) + else: + processed_extension["memory"] = {"cur_mem": CGroupsTelemetry._get_metrics_list(memory_usage)} + + if max_memory_usage.count() > 0: + if "memory" in processed_extension: + processed_extension["memory"]["max_mem"] = CGroupsTelemetry._get_metrics_list(max_memory_usage) + else: + processed_extension["memory"] = {"max_mem": CGroupsTelemetry._get_metrics_list(max_memory_usage)} + + return processed_extension + + @staticmethod + def track_cgroup(cgroup): + """ + Adds the given item to the dictionary of tracked cgroups + """ + with CGroupsTelemetry._rlock: + if not CGroupsTelemetry.is_tracked(cgroup.path): + CGroupsTelemetry._tracked.append(cgroup) + logger.info("Started tracking new cgroup: {0}, path: {1}".format(cgroup.name, cgroup.path)) + + @staticmethod + def is_tracked(path): + """ + Returns true if the given item is in the list of tracked items + O(n) operation. But limited to few cgroup objects we have. + """ + with CGroupsTelemetry._rlock: + for cgroup in CGroupsTelemetry._tracked: + if path == cgroup.path: + return True + + return False + + @staticmethod + def stop_tracking(cgroup): + """ + Stop tracking the cgroups for the given name + """ + with CGroupsTelemetry._rlock: + CGroupsTelemetry._tracked.remove(cgroup) + logger.info("Stopped tracking cgroup: {0}, path: {1}".format(cgroup.name, cgroup.path)) + + @staticmethod + def report_all_tracked(): + collected_metrics = {} + + for name, cgroup_metrics in CGroupsTelemetry._cgroup_metrics.items(): + perf_metric = CGroupsTelemetry._process_cgroup_metric(cgroup_metrics) + + if perf_metric: + collected_metrics[name] = perf_metric + + cgroup_metrics.clear() + + # Doing cleanup after the metrics have already been collected. + for key in [key for key in CGroupsTelemetry._cgroup_metrics if + CGroupsTelemetry._cgroup_metrics[key].marked_for_delete]: + del CGroupsTelemetry._cgroup_metrics[key] + + return collected_metrics + + @staticmethod + def poll_all_tracked(): + with CGroupsTelemetry._rlock: + for cgroup in CGroupsTelemetry._tracked[:]: + + if cgroup.name not in CGroupsTelemetry._cgroup_metrics: + CGroupsTelemetry._cgroup_metrics[cgroup.name] = CgroupMetrics() + + CGroupsTelemetry._cgroup_metrics[cgroup.name].collect_data(cgroup) + + if not cgroup.is_active(): + CGroupsTelemetry.stop_tracking(cgroup) + CGroupsTelemetry._cgroup_metrics[cgroup.name].marked_for_delete = True + + @staticmethod + def prune_all_tracked(): + with CGroupsTelemetry._rlock: + for cgroup in CGroupsTelemetry._tracked[:]: + if not cgroup.is_active(): + CGroupsTelemetry.stop_tracking(cgroup) + + @staticmethod + def reset(): + with CGroupsTelemetry._rlock: + CGroupsTelemetry._tracked *= 0 # emptying the list + CGroupsTelemetry._cgroup_metrics = {} + + +class CgroupMetrics(object): + def __init__(self): + self._memory_usage = Metric() + self._max_memory_usage = Metric() + self._cpu_usage = Metric() + self.marked_for_delete = False + + def collect_data(self, cgroup): + # noinspection PyBroadException + try: + if cgroup.controller == "cpu": + self._cpu_usage.append(cgroup.get_cpu_usage()) + elif cgroup.controller == "memory": + self._memory_usage.append(cgroup.get_memory_usage()) + self._max_memory_usage.append(cgroup.get_max_memory_usage()) + else: + raise CGroupsException('CGroup controller {0} is not supported'.format(controller)) + except Exception as e: + if not isinstance(e, (IOError, OSError)) or e.errno != errno.ENOENT: + logger.periodic_warn(logger.EVERY_HALF_HOUR, 'Could not collect metrics for cgroup {0}. Error : {1}'.format(cgroup.path, ustr(e))) + + def get_memory_usage(self): + return self._memory_usage + + def get_max_memory_usage(self): + return self._max_memory_usage + + def get_cpu_usage(self): + return self._cpu_usage + + def clear(self): + self._memory_usage.clear() + self._max_memory_usage.clear() + self._cpu_usage.clear() + + +class Metric(object): + def __init__(self): + self._data = [] + self._first_poll_time = None + self._last_poll_time = None + + def append(self, data): + if not self._first_poll_time: + # We only want to do it first time. + self._first_poll_time = dt.utcnow() + + self._data.append(data) + self._last_poll_time = dt.utcnow() + + def clear(self): + self._first_poll_time = None + self._last_poll_time = None + self._data *= 0 + + def average(self): + return float(sum(self._data)) / float(len(self._data)) if self._data else None + + def max(self): + return max(self._data) if self._data else None + + def min(self): + return min(self._data) if self._data else None + + def median(self): + data = sorted(self._data) + l_len = len(data) + if l_len < 1: + return None + if l_len % 2 == 0: + return (data[int((l_len - 1) / 2)] + data[int((l_len + 1) / 2)]) / 2.0 + else: + return data[int((l_len - 1) / 2)] + + def count(self): + return len(self._data) + + def first_poll_time(self): + return str(self._first_poll_time) + + def last_poll_time(self): + return str(self._last_poll_time) diff -Nru waagent-2.2.34/azurelinuxagent/common/conf.py waagent-2.2.45/azurelinuxagent/common/conf.py --- waagent-2.2.34/azurelinuxagent/common/conf.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/conf.py 2019-11-07 00:36:56.000000000 +0000 @@ -96,9 +96,8 @@ "OS.UpdateRdmaDriver": False, "OS.CheckRdmaDriver": False, "Logs.Verbose": False, + "Logs.Console": True, "Extensions.Enabled": True, - "Provisioning.Enabled": True, - "Provisioning.UseCloudInit": False, "Provisioning.AllowResetSysUser": False, "Provisioning.RegenerateSshHostKeyPair": False, "Provisioning.DeleteRootPassword": False, @@ -126,6 +125,7 @@ "OS.PasswordPath": "/etc/shadow", "OS.SudoersDir": "/etc/sudoers.d", "OS.RootDeviceScsiTimeout": None, + "Provisioning.Agent": "auto", "Provisioning.SshHostKeyPairType": "rsa", "Provisioning.PasswordCryptId": "6", "HttpProxy.Host": None, @@ -173,10 +173,14 @@ def enable_rdma_update(conf=__conf__): return conf.get_switch("OS.UpdateRdmaDriver", False) +def enable_check_rdma_driver(conf=__conf__): + return conf.get_switch("OS.CheckRdmaDriver", True) def get_logs_verbose(conf=__conf__): return conf.get_switch("Logs.Verbose", False) +def get_logs_console(conf=__conf__): + return conf.get_switch("Logs.Console", True) def get_lib_dir(conf=__conf__): return conf.get("Lib.Dir", "/var/lib/waagent") @@ -263,18 +267,10 @@ return conf.get("Provisioning.SshHostKeyPairType", "rsa") -def get_provision_enabled(conf=__conf__): - return conf.get_switch("Provisioning.Enabled", True) - - def get_extensions_enabled(conf=__conf__): return conf.get_switch("Extensions.Enabled", True) -def get_provision_cloudinit(conf=__conf__): - return conf.get_switch("Provisioning.UseCloudInit", False) - - def get_allow_reset_sys_user(conf=__conf__): return conf.get_switch("Provisioning.AllowResetSysUser", False) @@ -299,6 +295,21 @@ return conf.get("Provisioning.PasswordCryptId", "6") +def get_provisioning_agent(conf=__conf__): + return conf.get("Provisioning.Agent", "auto") + + +def get_provision_enabled(conf=__conf__): + """ + Provisioning (as far as waagent is concerned) is enabled if either the + agent is set to 'auto' or 'waagent'. This wraps logic that was introduced + for flexible provisioning agent configuration and detection. The replaces + the older bool setting to turn provisioning on or off. + """ + + return get_provisioning_agent(conf) in ("auto", "waagent") + + def get_password_crypt_salt_len(conf=__conf__): return conf.get_int("Provisioning.PasswordCryptSaltLength", 10) diff -Nru waagent-2.2.34/azurelinuxagent/common/datacontract.py waagent-2.2.45/azurelinuxagent/common/datacontract.py --- waagent-2.2.34/azurelinuxagent/common/datacontract.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/datacontract.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,83 @@ +# Microsoft Azure Linux Agent +# +# Copyright 2019 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# + +from azurelinuxagent.common.exception import ProtocolError +import azurelinuxagent.common.logger as logger + +""" +Base class for data contracts between guest and host and utilities to manipulate the properties in those contracts +""" + + +class DataContract(object): + pass + + +class DataContractList(list): + def __init__(self, item_cls): + self.item_cls = item_cls + + +def validate_param(name, val, expected_type): + if val is None: + raise ProtocolError("{0} is None".format(name)) + if not isinstance(val, expected_type): + raise ProtocolError(("{0} type should be {1} not {2}" + "").format(name, expected_type, type(val))) + + +def set_properties(name, obj, data): + if isinstance(obj, DataContract): + validate_param("Property '{0}'".format(name), data, dict) + for prob_name, prob_val in data.items(): + prob_full_name = "{0}.{1}".format(name, prob_name) + try: + prob = getattr(obj, prob_name) + except AttributeError: + logger.warn("Unknown property: {0}", prob_full_name) + continue + prob = set_properties(prob_full_name, prob, prob_val) + setattr(obj, prob_name, prob) + return obj + elif isinstance(obj, DataContractList): + validate_param("List '{0}'".format(name), data, list) + for item_data in data: + item = obj.item_cls() + item = set_properties(name, item, item_data) + obj.append(item) + return obj + else: + return data + + +def get_properties(obj): + if isinstance(obj, DataContract): + data = {} + props = vars(obj) + for prob_name, prob in list(props.items()): + data[prob_name] = get_properties(prob) + return data + elif isinstance(obj, DataContractList): + data = [] + for item in obj: + item_data = get_properties(item) + data.append(item_data) + return data + else: + return obj diff -Nru waagent-2.2.34/azurelinuxagent/common/dhcp.py waagent-2.2.45/azurelinuxagent/common/dhcp.py --- waagent-2.2.34/azurelinuxagent/common/dhcp.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/dhcp.py 2019-11-07 00:36:56.000000000 +0000 @@ -86,9 +86,8 @@ route_exists = False logger.info("Test for route to {0}".format(KNOWN_WIRESERVER_IP)) try: - route_file = '/proc/net/route' - if os.path.exists(route_file) and \ - KNOWN_WIRESERVER_IP_ENTRY in open(route_file).read(): + route_table = self.osutil.read_route_table() + if any([(KNOWN_WIRESERVER_IP_ENTRY in route) for route in route_table]): # reset self.gateway and self.routes # we do not need to alter the routing table self.endpoint = KNOWN_WIRESERVER_IP @@ -102,7 +101,7 @@ logger.error( "Could not determine whether route exists to {0}: {1}".format( KNOWN_WIRESERVER_IP, e)) - + return route_exists @property @@ -385,7 +384,7 @@ unpack_big_endian(request, 4, 4))) if request_broadcast: - # set broadcast flag to true to request the dhcp sever + # set broadcast flag to true to request the dhcp server # to respond to a boradcast address, # this is useful when user dhclient fails. request[0x0A] = 0x80; diff -Nru waagent-2.2.34/azurelinuxagent/common/event.py waagent-2.2.45/azurelinuxagent/common/event.py --- waagent-2.2.34/azurelinuxagent/common/event.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/event.py 2019-11-07 00:36:56.000000000 +0000 @@ -16,27 +16,32 @@ # import atexit -import datetime import json import os import sys +import threading import time import traceback - from datetime import datetime import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger - from azurelinuxagent.common.exception import EventError from azurelinuxagent.common.future import ustr -from azurelinuxagent.common.protocol.restapi import TelemetryEventParam, \ - TelemetryEvent, \ - get_properties +from azurelinuxagent.common.datacontract import get_properties +from azurelinuxagent.common.telemetryevent import TelemetryEventParam, TelemetryEvent from azurelinuxagent.common.utils import fileutil, textutil -from azurelinuxagent.common.version import CURRENT_VERSION +from azurelinuxagent.common.version import CURRENT_VERSION, CURRENT_AGENT _EVENT_MSG = "Event: name={0}, op={1}, message={2}, duration={3}" +TELEMETRY_EVENT_PROVIDER_ID = "69B669B9-4AF8-4C50-BDC4-6006FA76E975" + +# Store the last retrieved container id as an environment variable to be shared between threads for telemetry purposes +CONTAINER_ID_ENV_VARIABLE = "AZURE_GUEST_AGENT_CONTAINER_ID" + + +def get_container_id_from_env(): + return os.environ.get(CONTAINER_ID_ENV_VARIABLE, "UNINITIALIZED") class WALAEventOperation: @@ -46,7 +51,9 @@ ArtifactsProfileBlob = "ArtifactsProfileBlob" AutoUpdate = "AutoUpdate" CustomData = "CustomData" + CGroupsCleanUp = "CGroupsCleanUp" CGroupsLimitsCrossed = "CGroupsLimitsCrossed" + ExtensionMetricsData = "ExtensionMetricsData" Deploy = "Deploy" Disable = "Disable" Downgrade = "Downgrade" @@ -66,7 +73,9 @@ Install = "Install" InitializeCGroups = "InitializeCGroups" InitializeHostPlugin = "InitializeHostPlugin" + InvokeCommandUsingSystemd = "InvokeCommandUsingSystemd" Log = "Log" + OSInfo = "OSInfo" Partition = "Partition" ProcessGoalState = "ProcessGoalState" Provision = "Provision" @@ -93,6 +102,7 @@ WALAEventOperation.UnInstall, ] + class EventStatus(object): EVENT_STATUS_FILE = "event_status.json" @@ -205,7 +215,11 @@ logger.warn("Cannot save event -- Event reporter is not initialized.") return - fileutil.mkdir(self.event_dir, mode=0o700) + try: + fileutil.mkdir(self.event_dir, mode=0o700) + except (IOError, OSError) as e: + msg = "Failed to create events folder {0}. Error: {1}".format(self.event_dir, ustr(e)) + raise EventError(msg) existing_events = os.listdir(self.event_dir) if len(existing_events) >= 1000: @@ -225,7 +239,8 @@ hfile.write(data.encode("utf-8")) os.rename(filename + ".tmp", filename + ".tld") except IOError as e: - raise EventError("Failed to write events to file:{0}", e) + msg = "Failed to write events to file: {0}".format(e) + raise EventError(msg) def reset_periodic(self): self.periodic_events = {} @@ -234,54 +249,42 @@ return h not in self.periodic_events or \ (self.periodic_events[h] + delta) <= datetime.now() - def add_periodic(self, - delta, name, op=WALAEventOperation.Unknown, is_success=True, duration=0, - version=CURRENT_VERSION, message="", evt_type="", - is_internal=False, log_event=True, force=False): + def add_periodic(self, delta, name, op=WALAEventOperation.Unknown, is_success=True, duration=0, + version=str(CURRENT_VERSION), message="", evt_type="", is_internal=False, log_event=True, + force=False): + h = hash(name + op + ustr(is_success) + message) - h = hash(name+op+ustr(is_success)+message) - if force or self.is_period_elapsed(delta, h): - self.add_event(name, - op=op, is_success=is_success, duration=duration, - version=version, message=message, evt_type=evt_type, - is_internal=is_internal, log_event=log_event) + self.add_event(name, op=op, is_success=is_success, duration=duration, + version=version, message=message, evt_type=evt_type, + is_internal=is_internal, log_event=log_event) self.periodic_events[h] = datetime.now() - def add_event(self, - name, - op=WALAEventOperation.Unknown, - is_success=True, - duration=0, - version=CURRENT_VERSION, - message="", - evt_type="", - is_internal=False, - log_event=True): + def add_event(self, name, op=WALAEventOperation.Unknown, is_success=True, duration=0, version=str(CURRENT_VERSION), + message="", evt_type="", is_internal=False, log_event=True): if (not is_success) and log_event: _log_event(name, op, message, duration, is_success=is_success) - self._add_event(duration, evt_type, is_internal, is_success, message, name, op, version, eventId=1) - self._add_event(duration, evt_type, is_internal, is_success, message, name, op, version, eventId=6) + self._add_event(duration, evt_type, is_internal, is_success, message, name, op, version, event_id=1) - def _add_event(self, duration, evt_type, is_internal, is_success, message, name, op, version, eventId): - event = TelemetryEvent(eventId, "69B669B9-4AF8-4C50-BDC4-6006FA76E975") + def _add_event(self, duration, evt_type, is_internal, is_success, message, name, op, version, event_id): + event = TelemetryEvent(event_id, TELEMETRY_EVENT_PROVIDER_ID) event.parameters.append(TelemetryEventParam('Name', name)) event.parameters.append(TelemetryEventParam('Version', str(version))) event.parameters.append(TelemetryEventParam('IsInternal', is_internal)) event.parameters.append(TelemetryEventParam('Operation', op)) - event.parameters.append(TelemetryEventParam('OperationSuccess', - is_success)) + event.parameters.append(TelemetryEventParam('OperationSuccess', is_success)) event.parameters.append(TelemetryEventParam('Message', message)) event.parameters.append(TelemetryEventParam('Duration', duration)) event.parameters.append(TelemetryEventParam('ExtensionType', evt_type)) + self.add_default_parameters_to_event(event) data = get_properties(event) try: self.save_event(json.dumps(data)) except EventError as e: - logger.error("{0}", e) + logger.periodic_error(logger.EVERY_FIFTEEN_MINUTES, "[PERIODIC] {0}".format(ustr(e))) def add_log_event(self, level, message): # By the time the message has gotten to this point it is formatted as @@ -303,6 +306,7 @@ event.parameters.append(TelemetryEventParam('Context2', '')) event.parameters.append(TelemetryEventParam('Context3', '')) + self.add_default_parameters_to_event(event) data = get_properties(event) try: self.save_event(json.dumps(data)) @@ -330,12 +334,35 @@ event.parameters.append(TelemetryEventParam('Instance', instance)) event.parameters.append(TelemetryEventParam('Value', value)) + self.add_default_parameters_to_event(event) data = get_properties(event) try: self.save_event(json.dumps(data)) except EventError as e: logger.error("{0}", e) + @staticmethod + def add_default_parameters_to_event(event, set_default_values=False): + # We write the GAVersion here rather than add it in azurelinuxagent.ga.monitor.MonitorHandler.add_sysinfo + # as there could be a possibility of events being sent with newer version of the agent, rather than the agent + # version generating the event. + # Old behavior example: V1 writes the event on the disk and finds an update immediately, and updates. Now the + # new monitor thread would pick up the events from the disk and send it with the CURRENT_AGENT, which would have + # newer version of the agent. This causes confusion. + # + # ContainerId can change due to live migration and we want to preserve the container Id of the container writing + # the event, rather than sending the event. + # OpcodeName: This is used as the actual time of event generation. + + default_parameters = [("GAVersion", CURRENT_AGENT), ('ContainerId', get_container_id_from_env()), + ('OpcodeName', datetime.utcnow().__str__()), + ('EventTid', threading.current_thread().ident), + ('EventPid', os.getpid()), ("TaskName", threading.current_thread().getName()), + ("KeywordName", '')] + + for param in default_parameters: + event.parameters.append(TelemetryEventParam(param[0], param[1])) + __event_logger__ = EventLogger() @@ -353,7 +380,7 @@ def report_event(op, is_success=True, message='', log_event=True): from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION add_event(AGENT_NAME, - version=CURRENT_VERSION, + version=str(CURRENT_VERSION), is_success=is_success, message=message, op=op, @@ -363,10 +390,10 @@ def report_periodic(delta, op, is_success=True, message=''): from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION add_periodic(delta, AGENT_NAME, - version=CURRENT_VERSION, - is_success=is_success, - message=message, - op=op) + version=str(CURRENT_VERSION), + is_success=is_success, + message=message, + op=op) def report_metric(category, counter, instance, value, log_event=False, reporter=__event_logger__): @@ -388,10 +415,8 @@ reporter.add_metric(category, counter, instance, value, log_event) -def add_event(name, op=WALAEventOperation.Unknown, is_success=True, duration=0, - version=CURRENT_VERSION, - message="", evt_type="", is_internal=False, log_event=True, - reporter=__event_logger__): +def add_event(name, op=WALAEventOperation.Unknown, is_success=True, duration=0, version=str(CURRENT_VERSION), message="", + evt_type="", is_internal=False, log_event=True, reporter=__event_logger__): if reporter.event_dir is None: logger.warn("Cannot add event -- Event reporter is not initialized.") _log_event(name, op, message, duration, is_success=is_success) @@ -399,10 +424,8 @@ if should_emit_event(name, version, op, is_success): mark_event_status(name, version, op, is_success) - reporter.add_event( - name, op=op, is_success=is_success, duration=duration, - version=str(version), message=message, evt_type=evt_type, - is_internal=is_internal, log_event=log_event) + reporter.add_event(name, op=op, is_success=is_success, duration=duration, version=str(version), message=message, + evt_type=evt_type, is_internal=is_internal, log_event=log_event) def add_log_event(level, message, reporter=__event_logger__): @@ -412,20 +435,16 @@ reporter.add_log_event(level, message) -def add_periodic( - delta, name, op=WALAEventOperation.Unknown, is_success=True, duration=0, - version=CURRENT_VERSION, - message="", evt_type="", is_internal=False, log_event=True, force=False, - reporter=__event_logger__): +def add_periodic(delta, name, op=WALAEventOperation.Unknown, is_success=True, duration=0, + version=str(CURRENT_VERSION), message="", evt_type="", is_internal=False, log_event=True, force=False, + reporter=__event_logger__): if reporter.event_dir is None: logger.warn("Cannot add periodic event -- Event reporter is not initialized.") _log_event(name, op, message, duration, is_success=is_success) return - reporter.add_periodic( - delta, name, op=op, is_success=is_success, duration=duration, - version=str(version), message=message, evt_type=evt_type, - is_internal=is_internal, log_event=log_event, force=force) + reporter.add_periodic(delta, name, op=op, is_success=is_success, duration=duration, version=str(version), + message=message, evt_type=evt_type, is_internal=is_internal, log_event=log_event, force=force) def mark_event_status(name, version, op, status): diff -Nru waagent-2.2.34/azurelinuxagent/common/exception.py waagent-2.2.45/azurelinuxagent/common/exception.py --- waagent-2.2.34/azurelinuxagent/common/exception.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/exception.py 2019-11-07 00:36:56.000000000 +0000 @@ -51,6 +51,16 @@ super(AgentNetworkError, self).__init__(msg, inner) +class CGroupsException(AgentError): + + def __init__(self, msg, inner=None): + super(AgentError, self).__init__(msg, inner) + # TODO: AgentError should set the message - investigate whether doing it there would break anything + self.message = msg + + def __str__(self): + return self.message + class ExtensionError(AgentError): """ When failed to execute an extension @@ -61,23 +71,33 @@ self.code = code -class ExtensionDownloadError(ExtensionError): +class ExtensionOperationError(ExtensionError): """ - When failed to download and setup an extension + When the command times out or returns with a non-zero exit_code """ - def __init__(self, msg=None, inner=None): - super(ExtensionDownloadError, self).__init__(msg, inner) + def __init__(self, msg=None, inner=None, code=-1, exit_code=-1): + super(ExtensionOperationError, self).__init__(msg, inner) + self.code = code + self.exit_code = exit_code -class ExtensionOperationError(ExtensionError): +class ExtensionUpdateError(ExtensionError): """ - When failed to execute an extension + When failed to update an extension """ def __init__(self, msg=None, inner=None, code=-1): - super(ExtensionOperationError, self).__init__(msg, inner) - self.code = code + super(ExtensionUpdateError, self).__init__(msg, inner, code) + + +class ExtensionDownloadError(ExtensionError): + """ + When failed to download and setup an extension + """ + + def __init__(self, msg=None, inner=None, code=-1): + super(ExtensionDownloadError, self).__init__(msg, inner, code) class ProvisionError(AgentError): @@ -143,6 +163,15 @@ super(HttpError, self).__init__(msg, inner) +class InvalidContainerError(HttpError): + """ + Container id sent in the header is invalid + """ + + def __init__(self, msg=None, inner=None): + super(InvalidContainerError, self).__init__(msg, inner) + + class EventError(AgentError): """ Event reporting error @@ -188,3 +217,55 @@ def __init__(self, msg=None, inner=None): super(RemoteAccessError, self).__init__(msg, inner) + + +class ExtensionErrorCodes(object): + """ + Common Error codes used across by Compute RP for better understanding + the cause and clarify common occurring errors + """ + + # Unknown Failures + PluginUnknownFailure = -1 + + # Success + PluginSuccess = 0 + + # Catch all error code. + PluginProcessingError = 1000 + + # Plugin failed to download + PluginManifestDownloadError = 1001 + + # Cannot find or load successfully the HandlerManifest.json + PluginHandlerManifestNotFound = 1002 + + # Cannot successfully serialize the HandlerManifest.json + PluginHandlerManifestDeserializationError = 1003 + + # Cannot download the plugin package + PluginPackageDownloadFailed = 1004 + + # Cannot extract the plugin form package + PluginPackageExtractionFailed = 1005 + + # Install failed + PluginInstallProcessingFailed = 1007 + + # Update failed + PluginUpdateProcessingFailed = 1008 + + # Enable failed + PluginEnableProcessingFailed = 1009 + + # Disable failed + PluginDisableProcessingFailed = 1010 + + # Extension script timed out + PluginHandlerScriptTimedout = 1011 + + # Invalid status file of the extension. + PluginSettingsStatusInvalid = 1012 + + def __init__(self): + pass diff -Nru waagent-2.2.34/azurelinuxagent/common/future.py waagent-2.2.45/azurelinuxagent/common/future.py --- waagent-2.2.34/azurelinuxagent/common/future.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/future.py 2019-11-07 00:36:56.000000000 +0000 @@ -1,5 +1,7 @@ import platform import sys +import os +import re # Note broken dependency handling to avoid potential backward # compatibility issues on different distributions @@ -45,6 +47,12 @@ supported_dists=supported ) ) + + # The platform.linux_distribution() lib has issue with detecting OpenWRT linux distribution. + # Merge the following patch provided by OpenWRT as a temporary fix. + if os.path.exists("/etc/openwrt_release"): + osinfo = get_openwrt_platform() + if not osinfo or osinfo == ['', '', '']: return get_linux_distribution_from_distro(get_full_name) full_name = platform.linux_distribution()[0].strip() @@ -68,3 +76,24 @@ full_name = distro.linux_distribution()[0].strip() osinfo.append(full_name) return osinfo + +def get_openwrt_platform(): + """ + Add this workaround for detecting OpenWRT products because + the version and product information is contained in the /etc/openwrt_release file. + """ + result = [None, None, None] + openwrt_version = re.compile(r"^DISTRIB_RELEASE=['\"](\d+\.\d+.\d+)['\"]") + openwrt_product = re.compile(r"^DISTRIB_ID=['\"]([\w-]+)['\"]") + + with open('/etc/openwrt_release', 'r') as fh: + content = fh.readlines() + for line in content: + version_matches = openwrt_version.match(line) + product_matches = openwrt_product.match(line) + if version_matches: + result[1] = version_matches.group(1) + elif product_matches: + if product_matches.group(1) == "OpenWrt": + result[0] = "openwrt" + return result \ No newline at end of file diff -Nru waagent-2.2.34/azurelinuxagent/common/logger.py waagent-2.2.45/azurelinuxagent/common/logger.py --- waagent-2.2.34/azurelinuxagent/common/logger.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/logger.py 2019-11-07 00:36:56.000000000 +0000 @@ -24,6 +24,7 @@ EVERY_DAY = timedelta(days=1) EVERY_HALF_DAY = timedelta(hours=12) +EVERY_SIX_HOURS = timedelta(hours=6) EVERY_HOUR = timedelta(hours=1) EVERY_HALF_HOUR = timedelta(minutes=30) EVERY_FIFTEEN_MINUTES = timedelta(minutes=15) @@ -45,16 +46,28 @@ def set_prefix(self, prefix): self.prefix = prefix - def is_period_elapsed(self, delta, h): + def _is_period_elapsed(self, delta, h): return h not in self.logger.periodic_messages or \ (self.logger.periodic_messages[h] + delta) <= datetime.now() - def periodic(self, delta, msg_format, *args): + def _periodic(self, delta, log_level_op, msg_format, *args): h = hash(msg_format) - if self.is_period_elapsed(delta, h): - self.info(msg_format, *args) + if self._is_period_elapsed(delta, h): + log_level_op(msg_format, *args) self.logger.periodic_messages[h] = datetime.now() + def periodic_info(self, delta, msg_format, *args): + self._periodic(delta, self.info, msg_format, *args) + + def periodic_verbose(self, delta, msg_format, *args): + self._periodic(delta, self.verbose, msg_format, *args) + + def periodic_warn(self, delta, msg_format, *args): + self._periodic(delta, self.warn, msg_format, *args) + + def periodic_error(self, delta, msg_format, *args): + self._periodic(delta, self.error, msg_format, *args) + def verbose(self, msg_format, *args): self.log(LogLevel.VERBOSE, msg_format, *args) @@ -68,7 +81,7 @@ self.log(LogLevel.ERROR, msg_format, *args) def log(self, level, msg_format, *args): - #if msg_format is not unicode convert it to unicode + # if msg_format is not unicode convert it to unicode if type(msg_format) is not ustr: msg_format = ustr(msg_format, errors="backslashreplace") if len(args) > 0: @@ -150,7 +163,7 @@ pass -#Initialize logger instance +# Initialize logger instance DEFAULT_LOGGER = Logger() @@ -181,11 +194,41 @@ def reset_periodic(): DEFAULT_LOGGER.reset_periodic() + def set_prefix(prefix): DEFAULT_LOGGER.set_prefix(prefix) -def periodic(delta, msg_format, *args): - DEFAULT_LOGGER.periodic(delta, msg_format, *args) + +def periodic_info(delta, msg_format, *args): + """ + The hash-map maintaining the state of the logs gets reset here - + azurelinuxagent.ga.monitor.MonitorHandler.reset_loggers. The current time period is defined by RESET_LOGGERS_PERIOD. + """ + DEFAULT_LOGGER.periodic_info(delta, msg_format, *args) + + +def periodic_verbose(delta, msg_format, *args): + """ + The hash-map maintaining the state of the logs gets reset here - + azurelinuxagent.ga.monitor.MonitorHandler.reset_loggers. The current time period is defined by RESET_LOGGERS_PERIOD. + """ + DEFAULT_LOGGER.periodic_verbose(delta, msg_format, *args) + + +def periodic_error(delta, msg_format, *args): + """ + The hash-map maintaining the state of the logs gets reset here - + azurelinuxagent.ga.monitor.MonitorHandler.reset_loggers. The current time period is defined by RESET_LOGGERS_PERIOD. + """ + DEFAULT_LOGGER.periodic_error(delta, msg_format, *args) + + +def periodic_warn(delta, msg_format, *args): + """ + The hash-map maintaining the state of the logs gets reset here - + azurelinuxagent.ga.monitor.MonitorHandler.reset_loggers. The current time period is defined by RESET_LOGGERS_PERIOD. + """ + DEFAULT_LOGGER.periodic_warn(delta, msg_format, *args) def verbose(msg_format, *args): diff -Nru waagent-2.2.34/azurelinuxagent/common/osutil/alpine.py waagent-2.2.45/azurelinuxagent/common/osutil/alpine.py --- waagent-2.2.34/azurelinuxagent/common/osutil/alpine.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/osutil/alpine.py 2019-11-07 00:36:56.000000000 +0000 @@ -32,11 +32,7 @@ return True def get_dhcp_pid(self): - ret = shellutil.run_get_output('pidof dhcpcd', chk_err=False) - if ret[0] == 0: - logger.info('dhcpcd is pid {}'.format(ret[1])) - return ret[1].strip() - return None + return self._get_dhcp_pid(["pidof", "dhcpcd"]) def restart_if(self, ifname): logger.info('restarting {} (sort of, actually SIGHUPing dhcpcd)'.format(ifname)) diff -Nru waagent-2.2.34/azurelinuxagent/common/osutil/arch.py waagent-2.2.45/azurelinuxagent/common/osutil/arch.py --- waagent-2.2.34/azurelinuxagent/common/osutil/arch.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/osutil/arch.py 2019-11-07 00:36:56.000000000 +0000 @@ -20,6 +20,7 @@ import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.osutil.default import DefaultOSUtil + class ArchUtil(DefaultOSUtil): def __init__(self): super(ArchUtil, self).__init__() @@ -45,15 +46,14 @@ return shellutil.run("systemctl start systemd-networkd", chk_err=False) def start_agent_service(self): - return shellutil.run("systemctl start waagent", chk_err=False) + return shellutil.run("systemctl start {0}".format(self.service_name), chk_err=False) def stop_agent_service(self): - return shellutil.run("systemctl stop waagent", chk_err=False) + return shellutil.run("systemctl stop {0}".format(self.service_name), chk_err=False) def get_dhcp_pid(self): - ret= shellutil.run_get_output("pidof systemd-networkd") - return ret[1] if ret[0] == 0 else None + return self._get_dhcp_pid(["pidof", "systemd-networkd"]) def conf_sshd(self, disable_password): # Don't whack the system default sshd conf - pass \ No newline at end of file + pass diff -Nru waagent-2.2.34/azurelinuxagent/common/osutil/bigip.py waagent-2.2.45/azurelinuxagent/common/osutil/bigip.py --- waagent-2.2.34/azurelinuxagent/common/osutil/bigip.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/osutil/bigip.py 2019-11-07 00:36:56.000000000 +0000 @@ -85,20 +85,19 @@ return shellutil.run("/usr/bin/bigstart restart sshd", chk_err=False) def stop_agent_service(self): - return shellutil.run("/sbin/service waagent stop", chk_err=False) + return shellutil.run("/sbin/service {0} stop".format(self.service_name), chk_err=False) def start_agent_service(self): - return shellutil.run("/sbin/service waagent start", chk_err=False) + return shellutil.run("/sbin/service {0} start".format(self.service_name), chk_err=False) def register_agent_service(self): - return shellutil.run("/sbin/chkconfig --add waagent", chk_err=False) + return shellutil.run("/sbin/chkconfig --add {0}".format(self.service_name), chk_err=False) def unregister_agent_service(self): - return shellutil.run("/sbin/chkconfig --del waagent", chk_err=False) + return shellutil.run("/sbin/chkconfig --del {0}".format(self.service_name), chk_err=False) def get_dhcp_pid(self): - ret = shellutil.run_get_output("/sbin/pidof dhclient") - return ret[1] if ret[0] == 0 else None + return self._get_dhcp_pid(["/sbin/pidof", "dhclient"]) def set_hostname(self, hostname): """Set the static hostname of the device diff -Nru waagent-2.2.34/azurelinuxagent/common/osutil/clearlinux.py waagent-2.2.45/azurelinuxagent/common/osutil/clearlinux.py --- waagent-2.2.34/azurelinuxagent/common/osutil/clearlinux.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/osutil/clearlinux.py 2019-11-07 00:36:56.000000000 +0000 @@ -60,14 +60,13 @@ return shellutil.run("systemctl start systemd-networkd", chk_err=False) def start_agent_service(self): - return shellutil.run("systemctl start waagent", chk_err=False) + return shellutil.run("systemctl start {0}".format(self.service_name), chk_err=False) def stop_agent_service(self): - return shellutil.run("systemctl stop waagent", chk_err=False) + return shellutil.run("systemctl stop {0}".format(self.service_name), chk_err=False) def get_dhcp_pid(self): - ret= shellutil.run_get_output("pidof systemd-networkd") - return ret[1] if ret[0] == 0 else None + return self._get_dhcp_pid(["pidof", "systemd-networkd"]) def conf_sshd(self, disable_password): # Don't whack the system default sshd conf diff -Nru waagent-2.2.34/azurelinuxagent/common/osutil/coreos.py waagent-2.2.45/azurelinuxagent/common/osutil/coreos.py --- waagent-2.2.34/azurelinuxagent/common/osutil/coreos.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/osutil/coreos.py 2019-11-07 00:36:56.000000000 +0000 @@ -20,6 +20,7 @@ import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.osutil.default import DefaultOSUtil + class CoreOSUtil(DefaultOSUtil): def __init__(self): @@ -67,16 +68,13 @@ return shellutil.run("systemctl start systemd-networkd", chk_err=False) def start_agent_service(self): - return shellutil.run("systemctl start waagent", chk_err=False) + return shellutil.run("systemctl start {0}".format(self.service_name), chk_err=False) def stop_agent_service(self): - return shellutil.run("systemctl stop waagent", chk_err=False) + return shellutil.run("systemctl stop {0}".format(self.service_name), chk_err=False) def get_dhcp_pid(self): - ret = shellutil.run_get_output("systemctl show -p MainPID " - "systemd-networkd", chk_err=False) - pid = ret[1].split('=', 1)[-1].strip() if ret[0] == 0 else None - return pid if pid != '0' else None + return self._get_dhcp_pid(["systemctl", "show", "-p", "MainPID", "systemd-networkd"]) def conf_sshd(self, disable_password): # In CoreOS, /etc/sshd_config is mount readonly. Skip the setting. diff -Nru waagent-2.2.34/azurelinuxagent/common/osutil/debian.py waagent-2.2.45/azurelinuxagent/common/osutil/debian.py --- waagent-2.2.34/azurelinuxagent/common/osutil/debian.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/osutil/debian.py 2019-11-07 00:36:56.000000000 +0000 @@ -32,10 +32,11 @@ import azurelinuxagent.common.utils.textutil as textutil from azurelinuxagent.common.osutil.default import DefaultOSUtil -class DebianOSUtil(DefaultOSUtil): + +class DebianOSBaseUtil(DefaultOSUtil): def __init__(self): - super(DebianOSUtil, self).__init__() + super(DebianOSBaseUtil, self).__init__() self.jit_enabled = True def restart_ssh_service(self): @@ -58,3 +59,21 @@ def get_dhcp_lease_endpoint(self): return self.get_endpoint_from_leases_path('/var/lib/dhcp/dhclient.*.leases') + + +class DebianOSModernUtil(DebianOSBaseUtil): + + def __init__(self): + super(DebianOSModernUtil, self).__init__() + self.jit_enabled = True + self.service_name = self.get_service_name() + + @staticmethod + def get_service_name(): + return "walinuxagent" + + def stop_agent_service(self): + return shellutil.run("systemctl stop {0}".format(self.service_name), chk_err=False) + + def start_agent_service(self): + return shellutil.run("systemctl start {0}".format(self.service_name), chk_err=False) diff -Nru waagent-2.2.34/azurelinuxagent/common/osutil/default.py waagent-2.2.45/azurelinuxagent/common/osutil/default.py --- waagent-2.2.34/azurelinuxagent/common/osutil/default.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/osutil/default.py 2019-11-07 00:36:56.000000000 +0000 @@ -16,7 +16,6 @@ # Requires Python 2.6+ and Openssl 1.0+ # -import array import base64 import datetime import errno @@ -32,20 +31,21 @@ import struct import sys import time +from pwd import getpwall + +import array -import azurelinuxagent.common.logger as logger import azurelinuxagent.common.conf as conf +import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil import azurelinuxagent.common.utils.textutil as textutil - from azurelinuxagent.common.exception import OSUtilError from azurelinuxagent.common.future import ustr from azurelinuxagent.common.utils.cryptutil import CryptUtil from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.utils.networkutil import RouteEntry, NetworkInterfaceCard - -from pwd import getpwall +from azurelinuxagent.common.utils.shellutil import CommandError __RULES_FILES__ = [ "/lib/udev/rules.d/75-persistent-net-generator.rules", "/etc/udev/rules.d/70-persistent-net.rules" ] @@ -97,6 +97,8 @@ BASE_CGROUPS = '/sys/fs/cgroup' +STORAGE_DEVICE_PATH = '/sys/bus/vmbus/devices/' +GEN2_DEVICE_ID = 'f8b3781a-1e82-4818-a1c3-63d806ec15bb' class DefaultOSUtil(object): def __init__(self): @@ -104,6 +106,11 @@ self.selinux = None self.disable_route_warning = False self.jit_enabled = False + self.service_name = self.get_service_name() + + @staticmethod + def get_service_name(): + return "waagent" def get_firewall_dropped_packets(self, dst_ip=None): # If a previous attempt failed, do not retry @@ -114,7 +121,7 @@ try: wait = self.get_firewall_will_wait() - rc, output = shellutil.run_get_output(FIREWALL_PACKETS.format(wait), log_cmd=False) + rc, output = shellutil.run_get_output(FIREWALL_PACKETS.format(wait), log_cmd=False, expected_errors=[3]) if rc == 3: # Transient error that we ignore. This code fires every loop # of the daemon (60m), so we will get the value eventually. @@ -257,7 +264,8 @@ "{0}".format(ustr(e))) return False - def _correct_instance_id(self, id): + @staticmethod + def _correct_instance_id(id): ''' Azure stores the instance ID with an incorrect byte ordering for the first parts. For example, the ID returned by the metadata service: @@ -290,18 +298,17 @@ may have been persisted using the incorrect byte ordering. ''' id_this = self.get_instance_id() - return id_that == id_this or \ - id_that == self._correct_instance_id(id_this) + logger.verbose("current instance id: {0}".format(id_this)) + logger.verbose(" former instance id: {0}".format(id_that)) + return id_this.lower() == id_that.lower() or \ + id_this.lower() == self._correct_instance_id(id_that).lower() @staticmethod def is_cgroups_supported(): """ - Enabled by default; disabled in WSL/Travis + Enabled by default; disabled if the base path of cgroups doesn't exist. """ - is_wsl = '-Microsoft-' in platform.platform() - is_travis = 'TRAVIS' in os.environ and os.environ['TRAVIS'] == 'true' - base_fs_exists = os.path.exists(BASE_CGROUPS) - return not is_wsl and not is_travis and base_fs_exists + return os.path.exists(BASE_CGROUPS) @staticmethod def _cgroup_path(tail=""): @@ -317,22 +324,34 @@ option="-t tmpfs", chk_err=False) elif not os.path.isdir(self._cgroup_path()): - logger.error("Could not mount cgroups: ordinary file at {0}".format(path)) + logger.error("Could not mount cgroups: ordinary file at {0}", path) return - for metric_hierarchy in ['cpu,cpuacct', 'memory']: - target_path = self._cgroup_path(metric_hierarchy) - if not os.path.exists(target_path): - fileutil.mkdir(target_path) - self.mount(device=metric_hierarchy, - mount_point=target_path, - option="-t cgroup -o {0}".format(metric_hierarchy), - chk_err=False) - - for metric_hierarchy in ['cpu', 'cpuacct']: - target_path = self._cgroup_path(metric_hierarchy) - if not os.path.exists(target_path): - os.symlink(self._cgroup_path('cpu,cpuacct'), target_path) + controllers_to_mount = ['cpu,cpuacct', 'memory'] + errors = 0 + cpu_mounted = False + for controller in controllers_to_mount: + try: + target_path = self._cgroup_path(controller) + if not os.path.exists(target_path): + fileutil.mkdir(target_path) + self.mount(device=controller, + mount_point=target_path, + option="-t cgroup -o {0}".format(controller), + chk_err=False) + if controller == 'cpu,cpuacct': + cpu_mounted = True + except Exception as exception: + errors += 1 + if errors == len(controllers_to_mount): + raise + logger.warn("Could not mount cgroup controller {0}: {1}", controller, ustr(exception)) + + if cpu_mounted: + for controller in ['cpu', 'cpuacct']: + target_path = self._cgroup_path(controller) + if not os.path.exists(target_path): + os.symlink(self._cgroup_path('cpu,cpuacct'), target_path) except OSError as oe: # log a warning for read-only file systems @@ -362,7 +381,8 @@ return self._correct_instance_id(s.strip()) - def get_userentry(self, username): + @staticmethod + def get_userentry(username): try: return pwd.getpwnam(username) except KeyError: @@ -473,7 +493,8 @@ except IOError as e: raise OSUtilError("Failed to delete root password:{0}".format(e)) - def _norm_path(self, filepath): + @staticmethod + def _norm_path(filepath): home = conf.get_home_dir() # Expand HOME variable if present in path path = os.path.normpath(filepath.replace("$HOME", home)) @@ -844,7 +865,8 @@ route_list.append(route_obj) return route_list - def read_route_table(self): + @staticmethod + def read_route_table(): """ Return a list of strings comprising the route table, including column headers. Each line is stripped of leading or trailing whitespace but is otherwise unmolested. @@ -860,7 +882,8 @@ return [] - def get_list_of_routes(self, route_table): + @staticmethod + def get_list_of_routes(route_table): """ Construct a list of all network routes known to this system. @@ -890,43 +913,26 @@ RTF_GATEWAY = 0x02 DEFAULT_DEST = "00000000" - hdr_iface = "Iface" - hdr_dest = "Destination" - hdr_flags = "Flags" - hdr_metric = "Metric" - - idx_iface = -1 - idx_dest = -1 - idx_flags = -1 - idx_metric = -1 - primary = None - primary_metric = None + primary_interface = None if not self.disable_route_warning: logger.info("Examine /proc/net/route for primary interface") - with open('/proc/net/route') as routing_table: - idx = 0 - for header in filter(lambda h: len(h) > 0, routing_table.readline().strip(" \n").split("\t")): - if header == hdr_iface: - idx_iface = idx - elif header == hdr_dest: - idx_dest = idx - elif header == hdr_flags: - idx_flags = idx - elif header == hdr_metric: - idx_metric = idx - idx = idx + 1 - for entry in routing_table.readlines(): - route = entry.strip(" \n").split("\t") - if route[idx_dest] == DEFAULT_DEST and int(route[idx_flags]) & RTF_GATEWAY == RTF_GATEWAY: - metric = int(route[idx_metric]) - iface = route[idx_iface] - if primary is None or metric < primary_metric: - primary = iface - primary_metric = metric - if primary is None: - primary = '' + route_table = DefaultOSUtil.read_route_table() + + def is_default(route): + return route.destination == DEFAULT_DEST and int(route.flags) & RTF_GATEWAY == RTF_GATEWAY + + candidates = list(filter(is_default, DefaultOSUtil.get_list_of_routes(route_table))) + + if len(candidates) > 0: + def get_metric(route): + return int(route.metric) + primary_route = min(candidates, key=get_metric) + primary_interface = primary_route.interface + + if primary_interface is None: + primary_interface = '' if not self.disable_route_warning: with open('/proc/net/route') as routing_table_fh: routing_table_text = routing_table_fh.read() @@ -936,9 +942,9 @@ logger.warn('Primary interface examination will retry silently') self.disable_route_warning = True else: - logger.info('Primary interface is [{0}]'.format(primary)) + logger.info('Primary interface is [{0}]'.format(primary_interface)) self.disable_route_warning = False - return primary + return primary_interface def is_primary_interface(self, ifname): """ @@ -1102,9 +1108,19 @@ cmd = "ip route add {0} via {1}".format(net, gateway) return shellutil.run(cmd, chk_err=False) + @staticmethod + def _text_to_pid_list(text): + return [int(n) for n in text.split()] + + @staticmethod + def _get_dhcp_pid(command): + try: + return DefaultOSUtil._text_to_pid_list(shellutil.run_command(command)) + except CommandError as exception: + return [] + def get_dhcp_pid(self): - ret = shellutil.run_get_output("pidof dhclient", chk_err=False) - return ret[1] if ret[0] == 0 else None + return self._get_dhcp_pid(["pidof", "dhclient"]) def set_hostname(self, hostname): fileutil.write_file('/etc/hostname', hostname) @@ -1126,7 +1142,7 @@ def restart_if(self, ifname, retries=3, wait=5): retry_limit=retries+1 for attempt in range(1, retry_limit): - return_code=shellutil.run("ifdown {0} && ifup {0}".format(ifname)) + return_code=shellutil.run("ifdown {0} && ifup {0}".format(ifname), expected_errors=[1] if attempt < retries else []) if return_code == 0: return logger.warn("failed to restart {0}: return code {1}".format(ifname, return_code)) @@ -1177,6 +1193,67 @@ return tokens[2] if len(tokens) > 2 else None return None + @staticmethod + def _enumerate_device_id(): + """ + Enumerate all storage device IDs. + + Args: + None + + Returns: + Iterator[Tuple[str, str]]: VmBus and storage devices. + """ + + if os.path.exists(STORAGE_DEVICE_PATH): + for vmbus in os.listdir(STORAGE_DEVICE_PATH): + deviceid = fileutil.read_file(os.path.join(STORAGE_DEVICE_PATH, vmbus, "device_id")) + guid = deviceid.strip('{}\n') + yield vmbus, guid + + @staticmethod + def search_for_resource_disk(gen1_device_prefix, gen2_device_id): + """ + Search the filesystem for a device by ID or prefix. + + Args: + gen1_device_prefix (str): Gen1 resource disk prefix. + gen2_device_id (str): Gen2 resource device ID. + + Returns: + str: The found device. + """ + + device = None + # We have to try device IDs for both Gen1 and Gen2 VMs. + logger.info('Searching gen1 prefix {0} or gen2 {1}'.format(gen1_device_prefix, gen2_device_id)) + try: + for vmbus, guid in DefaultOSUtil._enumerate_device_id(): + if guid.startswith(gen1_device_prefix) or guid == gen2_device_id: + for root, dirs, files in os.walk(STORAGE_DEVICE_PATH + vmbus): + root_path_parts = root.split('/') + # For Gen1 VMs we only have to check for the block dir in the + # current device. But for Gen2 VMs all of the disks (sda, sdb, + # sr0) are presented in this device on the same SCSI controller. + # Because of that we need to also read the LUN. It will be: + # 0 - OS disk + # 1 - Resource disk + # 2 - CDROM + if root_path_parts[-1] == 'block' and ( + guid != gen2_device_id or + root_path_parts[-2].split(':')[-1] == '1'): + device = dirs[0] + return device + else: + # older distros + for d in dirs: + if ':' in d and "block" == d.split(':')[0]: + device = d.split(':')[1] + return device + except (OSError, IOError) as exc: + logger.warn('Error getting device for {0} or {1}: {2}', gen1_device_prefix, gen2_device_id, ustr(exc)) + return None + def device_for_ide_port(self, port_id): """ Return device name attached to ide port 'n'. @@ -1187,27 +1264,13 @@ if port_id > 1: g0 = "00000001" port_id = port_id - 2 - device = None - path = "/sys/bus/vmbus/devices/" - if os.path.exists(path): - try: - for vmbus in os.listdir(path): - deviceid = fileutil.read_file(os.path.join(path, vmbus, "device_id")) - guid = deviceid.lstrip('{').split('-') - if guid[0] == g0 and guid[1] == "000" + ustr(port_id): - for root, dirs, files in os.walk(path + vmbus): - if root.endswith("/block"): - device = dirs[0] - break - else: - # older distros - for d in dirs: - if ':' in d and "block" == d.split(':')[0]: - device = d.split(':')[1] - break - break - except OSError as oe: - logger.warn('Could not obtain device for IDE port {0}: {1}', port_id, ustr(oe)) + + gen1_device_prefix = '{0}-000{1}'.format(g0, port_id) + device = DefaultOSUtil.search_for_resource_disk( + gen1_device_prefix=gen1_device_prefix, + gen2_device_id=GEN2_DEVICE_ID + ) + logger.info('Found device: {0}'.format(device)) return device def set_hostname_record(self, hostname): @@ -1275,6 +1338,7 @@ results = fileutil.read_file('/proc/stat') except (OSError, IOError) as ex: logger.warn("Couldn't read /proc/stat: {0}".format(ex.strerror)) + raise return results @@ -1303,7 +1367,7 @@ """ state = {} - status, output = shellutil.run_get_output("ip -a -d -o link", chk_err=False, log_cmd=False) + status, output = shellutil.run_get_output("ip -a -o link", chk_err=False, log_cmd=False) """ 1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0 addrgenmode eui64 2: eth0: mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000\ link/ether 00:0d:3a:30:c3:5a brd ff:ff:ff:ff:ff:ff promiscuity 0 addrgenmode eui64 @@ -1320,14 +1384,14 @@ name = result.group(1) state[name] = NetworkInterfaceCard(name, result.group(2)) - self._update_nic_state(state, "ip -4 -a -d -o address", NetworkInterfaceCard.add_ipv4, "an IPv4 address") + self._update_nic_state(state, "ip -4 -a -o address", NetworkInterfaceCard.add_ipv4, "an IPv4 address") """ 1: lo inet 127.0.0.1/8 scope host lo\ valid_lft forever preferred_lft forever 2: eth0 inet 10.145.187.220/26 brd 10.145.187.255 scope global eth0\ valid_lft forever preferred_lft forever 3: docker0 inet 192.168.43.1/24 brd 192.168.43.255 scope global docker0\ valid_lft forever preferred_lft forever """ - self._update_nic_state(state, "ip -6 -a -d -o address", NetworkInterfaceCard.add_ipv6, "an IPv6 address") + self._update_nic_state(state, "ip -6 -a -o address", NetworkInterfaceCard.add_ipv6, "an IPv6 address") """ 1: lo inet6 ::1/128 scope host \ valid_lft forever preferred_lft forever 2: eth0 inet6 fe80::20d:3aff:fe30:c35a/64 scope link \ valid_lft forever preferred_lft forever diff -Nru waagent-2.2.34/azurelinuxagent/common/osutil/factory.py waagent-2.2.45/azurelinuxagent/common/osutil/factory.py --- waagent-2.2.34/azurelinuxagent/common/osutil/factory.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/osutil/factory.py 2019-11-07 00:36:56.000000000 +0000 @@ -22,7 +22,7 @@ from .arch import ArchUtil from .clearlinux import ClearLinuxUtil from .coreos import CoreOSUtil -from .debian import DebianOSUtil +from .debian import DebianOSBaseUtil, DebianOSModernUtil from .freebsd import FreeBSDOSUtil from .openbsd import OpenBSDOSUtil from .redhat import RedhatOSUtil, Redhat6xOSUtil @@ -34,6 +34,7 @@ from .gaia import GaiaOSUtil from .iosxe import IosxeOSUtil from .nsbsd import NSBSDOSUtil +from .openwrt import OpenWRTOSUtil from distutils.version import LooseVersion as Version @@ -43,6 +44,14 @@ distro_version=DISTRO_VERSION, distro_full_name=DISTRO_FULL_NAME): + # We are adding another layer of abstraction here since we want to be able to mock the final result of the + # function call. Since the get_osutil function is imported in various places in our tests, we can't mock + # it globally. Instead, we add _get_osutil function and mock it in the test base class, AgentTestCase. + return _get_osutil(distro_name, distro_code_name, distro_version, distro_full_name) + + +def _get_osutil(distro_name, distro_code_name, distro_version, distro_full_name): + if distro_name == "arch": return ArchUtil() @@ -67,7 +76,7 @@ return AlpineOSUtil() if distro_name == "kali": - return DebianOSUtil() + return DebianOSBaseUtil() if distro_name == "coreos" or distro_code_name == "coreos": return CoreOSUtil() @@ -80,10 +89,13 @@ else: return SUSEOSUtil() - elif distro_name == "debian": - return DebianOSUtil() + if distro_name == "debian": + if "sid" in distro_version or Version(distro_version) > Version("7"): + return DebianOSModernUtil() + else: + return DebianOSBaseUtil() - elif distro_name == "redhat" \ + if distro_name == "redhat" \ or distro_name == "centos" \ or distro_name == "oracle": if Version(distro_version) < Version("7"): @@ -91,27 +103,30 @@ else: return RedhatOSUtil() - elif distro_name == "euleros": + if distro_name == "euleros": return RedhatOSUtil() - elif distro_name == "freebsd": + if distro_name == "freebsd": return FreeBSDOSUtil() - elif distro_name == "openbsd": + if distro_name == "openbsd": return OpenBSDOSUtil() - elif distro_name == "bigip": + if distro_name == "bigip": return BigIpOSUtil() - elif distro_name == "gaia": + if distro_name == "gaia": return GaiaOSUtil() - elif distro_name == "iosxe": + if distro_name == "iosxe": return IosxeOSUtil() - elif distro_name == "nsbsd": + if distro_name == "nsbsd": return NSBSDOSUtil() + if distro_name == "openwrt": + return OpenWRTOSUtil() + else: logger.warn("Unable to load distro implementation for {0}. Using " "default distro implementation instead.", diff -Nru waagent-2.2.34/azurelinuxagent/common/osutil/freebsd.py waagent-2.2.45/azurelinuxagent/common/osutil/freebsd.py --- waagent-2.2.34/azurelinuxagent/common/osutil/freebsd.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/osutil/freebsd.py 2019-11-07 00:36:56.000000000 +0000 @@ -16,9 +16,13 @@ # # Requires Python 2.6+ and Openssl 1.0+ +import socket +import struct +import binascii import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil import azurelinuxagent.common.utils.textutil as textutil +from azurelinuxagent.common.utils.networkutil import RouteEntry import azurelinuxagent.common.logger as logger from azurelinuxagent.common.exception import OSUtilError from azurelinuxagent.common.osutil.default import DefaultOSUtil @@ -93,6 +97,304 @@ def get_first_if(self): return self._get_net_info()[:2] + @staticmethod + def read_route_table(): + """ + Return a list of strings comprising the route table as in the Linux /proc/net/route format. The input taken is from FreeBSDs + `netstat -rn -f inet` command. Here is what the function does in detail: + + 1. Runs `netstat -rn -f inet` which outputs a column formatted list of ipv4 routes in priority order like so: + + > Routing tables + > + > Internet: + > Destination Gateway Flags Refs Use Netif Expire + > default 61.221.xx.yy UGS 0 247 em1 + > 10 10.10.110.5 UGS 0 50 em0 + > 10.10.110/26 link#1 UC 0 0 em0 + > 10.10.110.5 00:1b:0d:e6:58:40 UHLW 2 0 em0 1145 + > 61.221.xx.yy/29 link#2 UC 0 0 em1 + > 61.221.xx.yy 00:1b:0d:e6:57:c0 UHLW 2 0 em1 1055 + > 61.221.xx/24 link#2 UC 0 0 em1 + > 127.0.0.1 127.0.0.1 UH 0 0 lo0 + + 2. Convert it to an array of lines that resemble an equivalent /proc/net/route content on a Linux system like so: + + > Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT + > gre828 00000000 00000000 0001 0 0 0 000000F8 0 0 0 + > ens160 00000000 FE04700A 0003 0 0 100 00000000 0 0 0 + > gre828 00000008 00000000 0001 0 0 0 000000FE 0 0 0 + > ens160 0004700A 00000000 0001 0 0 100 00FFFFFF 0 0 0 + > gre828 2504700A 00000000 0005 0 0 0 FFFFFFFF 0 0 0 + > gre828 3704700A 00000000 0005 0 0 0 FFFFFFFF 0 0 0 + > gre828 4104700A 00000000 0005 0 0 0 FFFFFFFF 0 0 0 + + :return: Entries in the ipv4 route priority list from `netstat -rn -f inet` in the linux `/proc/net/route` style + :rtype: list(str) + """ + + def _get_netstat_rn_ipv4_routes(): + """ + Runs `netstat -rn -f inet` and parses its output and returns a list of routes where the key is the column name + and the value is the value in the column, stripped of leading and trailing whitespace. + + :return: List of dictionaries representing routes in the ipv4 route priority list from `netstat -rn -f inet` + :rtype: list(dict) + """ + cmd = [ "netstat", "-rn", "-f", "inet" ] + output = shellutil.run_command(cmd, log_error=True) + output_lines = output.split("\n") + if len(output_lines) < 3: + raise OSUtilError("`netstat -rn -f inet` output seems to be empty") + output_lines = [ line.strip() for line in output_lines if line ] + if "Internet:" not in output_lines: + raise OSUtilError("`netstat -rn -f inet` output seems to contain no ipv4 routes") + route_header_line = output_lines.index("Internet:") + 1 + # Parse the file structure and left justify the routes + route_start_line = route_header_line + 1 + route_line_length = max([len(line) for line in output_lines[route_header_line:]]) + netstat_route_list = [line.ljust(route_line_length) for line in output_lines[route_start_line:]] + # Parse the headers + _route_headers = output_lines[route_header_line].split() + n_route_headers = len(_route_headers) + route_columns = {} + for i in range(0, n_route_headers - 1): + route_columns[_route_headers[i]] = ( + output_lines[route_header_line].index(_route_headers[i]), + (output_lines[route_header_line].index(_route_headers[i+1]) - 1) + ) + route_columns[_route_headers[n_route_headers - 1]] = ( + output_lines[route_header_line].index(_route_headers[n_route_headers - 1]), + None + ) + # Parse the routes + netstat_routes = [] + n_netstat_routes = len(netstat_route_list) + for i in range(0, n_netstat_routes): + netstat_route = {} + for column in route_columns: + netstat_route[column] = netstat_route_list[i][route_columns[column][0]:route_columns[column][1]].strip() + netstat_route["Metric"] = n_netstat_routes - i + netstat_routes.append(netstat_route) + # Return the Sections + return netstat_routes + + def _ipv4_ascii_address_to_hex(ipv4_ascii_address): + """ + Converts an IPv4 32bit address from its ASCII notation (ie. 127.0.0.1) to an 8 digit padded hex notation + (ie. "0100007F") string. + + :return: 8 character long hex string representation of the IP + :rtype: string + """ + # Raises socket.error if the IP is not a valid IPv4 + return "%08X" % int(binascii.hexlify(struct.pack("!I", struct.unpack("=I", socket.inet_pton(socket.AF_INET, ipv4_ascii_address))[0])), 16) + + def _ipv4_cidr_mask_to_hex(ipv4_cidr_mask): + """ + Converts an subnet mask from its CIDR integer notation (ie. 32) to an 8 digit padded hex notation + (ie. "FFFFFFFF") string representing its bitmask form. + + :return: 8 character long hex string representation of the IP + :rtype: string + """ + return "{0:08x}".format(struct.unpack("=I", struct.pack("!I", (0xffffffff << (32 - ipv4_cidr_mask)) & 0xffffffff))[0]).upper() + + def _ipv4_cidr_destination_to_hex(destination): + """ + Converts an destination address from its CIDR notation (ie. 127.0.0.1/32 or default or localhost) to an 8 + digit padded hex notation (ie. "0100007F" or "00000000" or "0100007F") string and its subnet bitmask + also in hex (FFFFFFFF). + + :return: tuple of 8 character long hex string representation of the IP and 8 character long hex string representation of the subnet mask + :rtype: tuple(string, int) + """ + destination_ip = "0.0.0.0" + destination_subnetmask = 32 + if destination != "default": + if destination == "localhost": + destination_ip = "127.0.0.1" + else: + destination_ip = destination.split("/") + if len(destination_ip) > 1: + destination_subnetmask = int(destination_ip[1]) + destination_ip = destination_ip[0] + hex_destination_ip = _ipv4_ascii_address_to_hex(destination_ip) + hex_destination_subnetmask = _ipv4_cidr_mask_to_hex(destination_subnetmask) + return hex_destination_ip, hex_destination_subnetmask + + def _try_ipv4_gateway_to_hex(gateway): + """ + If the gateway is an IPv4 address, return its IP in hex, else, return "00000000" + + :return: 8 character long hex string representation of the IP of the gateway + :rtype: string + """ + try: + return _ipv4_ascii_address_to_hex(gateway) + except socket.error: + return "00000000" + + def _ascii_route_flags_to_bitmask(ascii_route_flags): + """ + Converts route flags to a bitmask of their equivalent linux/route.h values. + + :return: integer representation of a 16 bit mask + :rtype: int + """ + bitmask_flags = 0 + RTF_UP = 0x0001 + RTF_GATEWAY = 0x0002 + RTF_HOST = 0x0004 + RTF_DYNAMIC = 0x0010 + if "U" in ascii_route_flags: + bitmask_flags |= RTF_UP + if "G" in ascii_route_flags: + bitmask_flags |= RTF_GATEWAY + if "H" in ascii_route_flags: + bitmask_flags |= RTF_HOST + if "S" not in ascii_route_flags: + bitmask_flags |= RTF_DYNAMIC + return bitmask_flags + + def _freebsd_netstat_rn_route_to_linux_proc_net_route(netstat_route): + """ + Converts a single FreeBSD `netstat -rn -f inet` route to its equivalent /proc/net/route line. ie: + > default 0.0.0.0 UGS 0 247 em1 + to + > em1 00000000 00000000 0003 0 0 0 FFFFFFFF 0 0 0 + + :return: string representation of the equivalent /proc/net/route line + :rtype: string + """ + network_interface = netstat_route["Netif"] + hex_destination_ip, hex_destination_subnetmask = _ipv4_cidr_destination_to_hex(netstat_route["Destination"]) + hex_gateway = _try_ipv4_gateway_to_hex(netstat_route["Gateway"]) + bitmask_flags = _ascii_route_flags_to_bitmask(netstat_route["Flags"]) + dummy_refcount = 0 + dummy_use = 0 + route_metric = netstat_route["Metric"] + dummy_mtu = 0 + dummy_window = 0 + dummy_irtt = 0 + return "{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\t{7}\t{8}\t{9}\t{10}".format( + network_interface, + hex_destination_ip, + hex_gateway, + bitmask_flags, + dummy_refcount, + dummy_use, + route_metric, + hex_destination_subnetmask, + dummy_mtu, + dummy_window, + dummy_irtt + ) + + linux_style_route_file = [ "Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT" ] + + try: + netstat_routes = _get_netstat_rn_ipv4_routes() + # Make sure the `netstat -rn -f inet` contains columns for Netif, Destination, Gateway and Flags which are needed to convert + # to the Linux Format + if len(netstat_routes) > 0: + missing_headers = [] + if "Netif" not in netstat_routes[0]: + missing_headers.append("Netif") + if "Destination" not in netstat_routes[0]: + missing_headers.append("Destination") + if "Gateway" not in netstat_routes[0]: + missing_headers.append("Gateway") + if "Flags" not in netstat_routes[0]: + missing_headers.append("Flags") + if missing_headers: + raise KeyError("`netstat -rn -f inet` output is missing columns required to convert to the Linux /proc/net/route format; columns are [{0}]".format(missing_headers)) + # Parse the Netstat IPv4 Routes + for netstat_route in netstat_routes: + try: + linux_style_route = _freebsd_netstat_rn_route_to_linux_proc_net_route(netstat_route) + linux_style_route_file.append(linux_style_route) + except Exception: + # Skip the route + continue + except Exception as e: + logger.error("Cannot read route table [{0}]", ustr(e)) + return linux_style_route_file + + @staticmethod + def get_list_of_routes(route_table): + """ + Construct a list of all network routes known to this system. + + :param list(str) route_table: List of text entries from route table, including headers + :return: a list of network routes + :rtype: list(RouteEntry) + """ + route_list = [] + count = len(route_table) + + if count < 1: + logger.error("netstat -rn -f inet is missing headers") + elif count == 1: + logger.error("netstat -rn -f inet contains no routes") + else: + route_list = DefaultOSUtil._build_route_list(route_table) + return route_list + + def get_primary_interface(self): + """ + Get the name of the primary interface, which is the one with the + default route attached to it; if there are multiple default routes, + the primary has the lowest Metric. + :return: the interface which has the default route + """ + RTF_GATEWAY = 0x0002 + DEFAULT_DEST = "00000000" + + primary_interface = None + + if not self.disable_route_warning: + logger.info("Examine `netstat -rn -f inet` for primary interface") + + route_table = self.read_route_table() + + def is_default(route): + return (route.destination == DEFAULT_DEST) and (RTF_GATEWAY & route.flags) + + candidates = list(filter(is_default, self.get_list_of_routes(route_table))) + + if len(candidates) > 0: + def get_metric(route): + return int(route.metric) + primary_route = min(candidates, key=get_metric) + primary_interface = primary_route.interface + + if primary_interface is None: + primary_interface = '' + if not self.disable_route_warning: + logger.warn('Could not determine primary interface, ' + 'please ensure routes are correct') + logger.warn('Primary interface examination will retry silently') + self.disable_route_warning = True + else: + logger.info('Primary interface is [{0}]'.format(primary_interface)) + self.disable_route_warning = False + return primary_interface + + def is_primary_interface(self, ifname): + """ + Indicate whether the specified interface is the primary. + :param ifname: the name of the interface - eth0, lo, etc. + :return: True if this interface binds the default route + """ + return self.get_primary_interface() == ifname + + def is_loopback(self, ifname): + """ + Determine if a named interface is loopback. + """ + return ifname.startswith("lo") + def route_add(self, net, mask, gateway): cmd = 'route add {0} {1} {2}'.format(net, gateway, mask) return shellutil.run(cmd, chk_err=False) @@ -103,6 +405,14 @@ specify the route manually to get it work in a VNET environment. SEE ALSO: man ip(4) IP_ONESBCAST, """ + RTF_GATEWAY = 0x0002 + DEFAULT_DEST = "00000000" + + route_table = self.read_route_table() + routes = self.get_list_of_routes(route_table) + for route in routes: + if (route.destination == DEFAULT_DEST) and (RTF_GATEWAY & route.flags): + return False return True def is_dhcp_enabled(self): @@ -121,8 +431,7 @@ shellutil.run("route delete 255.255.255.255 -iface {0}".format(ifname), chk_err=False) def get_dhcp_pid(self): - ret = shellutil.run_get_output("pgrep -n dhclient", chk_err=False) - return ret[1] if ret[0] == 0 else None + return self._get_dhcp_pid(["pgrep", "-n", "dhclient"]) def eject_dvd(self, chk_err=True): dvd = self.get_dvd_device() diff -Nru waagent-2.2.34/azurelinuxagent/common/osutil/nsbsd.py waagent-2.2.45/azurelinuxagent/common/osutil/nsbsd.py --- waagent-2.2.34/azurelinuxagent/common/osutil/nsbsd.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/osutil/nsbsd.py 2019-11-07 00:36:56.000000000 +0000 @@ -16,14 +16,10 @@ import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil -import azurelinuxagent.common.utils.textutil as textutil import azurelinuxagent.common.logger as logger from azurelinuxagent.common.exception import OSUtilError from azurelinuxagent.common.osutil.freebsd import FreeBSDOSUtil -from azurelinuxagent.common.future import ustr -import azurelinuxagent.common.conf as conf import os -import time class NSBSDOSUtil(FreeBSDOSUtil): @@ -120,12 +116,12 @@ shellutil.run("/usr/Firewall/sbin/nstop dhclient", chk_err=False) def get_dhcp_pid(self): - ret = None + ret = "" pidfile = "/var/run/dhclient.pid" if os.path.isfile(pidfile): ret = fileutil.read_file(pidfile, encoding='ascii') - return ret + return self._text_to_pid_list(ret) def eject_dvd(self, chk_err=True): pass diff -Nru waagent-2.2.34/azurelinuxagent/common/osutil/openbsd.py waagent-2.2.45/azurelinuxagent/common/osutil/openbsd.py --- waagent-2.2.34/azurelinuxagent/common/osutil/openbsd.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/osutil/openbsd.py 2019-11-07 00:36:56.000000000 +0000 @@ -35,6 +35,7 @@ r'^\s*[A-F0-9]{8}(?:\-[A-F0-9]{4}){3}\-[A-F0-9]{12}\s*$', re.IGNORECASE) + class OpenBSDOSUtil(DefaultOSUtil): def __init__(self): @@ -56,17 +57,17 @@ return shellutil.run('rcctl restart sshd', chk_err=False) def start_agent_service(self): - return shellutil.run('rcctl start waagent', chk_err=False) + return shellutil.run('rcctl start {0}'.format(self.service_name), chk_err=False) def stop_agent_service(self): - return shellutil.run('rcctl stop waagent', chk_err=False) + return shellutil.run('rcctl stop {0}'.format(self.service_name), chk_err=False) def register_agent_service(self): - shellutil.run('chmod 0555 /etc/rc.d/waagent', chk_err=False) - return shellutil.run('rcctl enable waagent', chk_err=False) + shellutil.run('chmod 0555 /etc/rc.d/{0}'.format(self.service_name), chk_err=False) + return shellutil.run('rcctl enable {0}'.format(self.service_name), chk_err=False) def unregister_agent_service(self): - return shellutil.run('rcctl disable waagent', chk_err=False) + return shellutil.run('rcctl disable {0}'.format(self.service_name), chk_err=False) def del_account(self, username): if self.is_sys_user(username): @@ -225,9 +226,7 @@ "{0}".format(ifname), chk_err=False) def get_dhcp_pid(self): - ret, output = shellutil.run_get_output("pgrep -n dhclient", - chk_err=False) - return output if ret == 0 else None + return self._get_dhcp_pid(["pgrep", "-n", "dhclient"]) def get_dvd_device(self, dev_dir='/dev'): pattern = r'cd[0-9]c' diff -Nru waagent-2.2.34/azurelinuxagent/common/osutil/openwrt.py waagent-2.2.45/azurelinuxagent/common/osutil/openwrt.py --- waagent-2.2.34/azurelinuxagent/common/osutil/openwrt.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/osutil/openwrt.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,152 @@ +# Microsoft Azure Linux Agent +# +# Copyright 2018 Microsoft Corporation +# Copyright 2018 Sonus Networks, Inc. (d.b.a. Ribbon Communications Operating Company) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +import os +import re +import azurelinuxagent.common.logger as logger +import azurelinuxagent.common.utils.shellutil as shellutil +import azurelinuxagent.common.utils.fileutil as fileutil +from azurelinuxagent.common.osutil.default import DefaultOSUtil +from azurelinuxagent.common.utils.networkutil import NetworkInterfaceCard + +class OpenWRTOSUtil(DefaultOSUtil): + + def __init__(self): + super(OpenWRTOSUtil, self).__init__() + self.agent_conf_file_path = '/etc/waagent.conf' + self.dhclient_name = 'udhcpc' + self.ip_command_output = re.compile('^\d+:\s+(\w+):\s+(.*)$') + self.jit_enabled = True + + def eject_dvd(self, chk_err=True): + logger.warn('eject is not supported on OpenWRT') + + def useradd(self, username, expiration=None, comment=None): + """ + Create user account with 'username' + """ + userentry = self.get_userentry(username) + if userentry is not None: + logger.info("User {0} already exists, skip useradd", username) + return + + if expiration is not None: + cmd = "useradd -m {0} -s /bin/ash -e {1}".format(username, expiration) + else: + cmd = "useradd -m {0} -s /bin/ash".format(username) + + if not os.path.exists("/home"): + os.mkdir("/home") + + if comment is not None: + cmd += " -c {0}".format(comment) + retcode, out = shellutil.run_get_output(cmd) + if retcode != 0: + raise OSUtilError(("Failed to create user account:{0}, " + "retcode:{1}, " + "output:{2}").format(username, retcode, out)) + + def get_dhcp_pid(self): + return self._get_dhcp_pid(["pidof", self.dhclient_name]) + + def get_nic_state(self): + """ + Capture NIC state (IPv4 and IPv6 addresses plus link state). + + :return: Dictionary of NIC state objects, with the NIC name as key + :rtype: dict(str,NetworkInformationCard) + """ + state = {} + status, output = shellutil.run_get_output("ip -o link", chk_err=False, log_cmd=False) + + if status != 0: + logger.verbose("Could not fetch NIC link info; status {0}, {1}".format(status, output)) + return {} + + for entry in output.splitlines(): + result = self.ip_command_output.match(entry) + if result: + name = result.group(1) + state[name] = NetworkInterfaceCard(name, result.group(2)) + + + self._update_nic_state(state, "ip -o -f inet address", NetworkInterfaceCard.add_ipv4, "an IPv4 address") + self._update_nic_state(state, "ip -o -f inet6 address", NetworkInterfaceCard.add_ipv6, "an IPv6 address") + + return state + + def _update_nic_state(self, state, ip_command, handler, description): + """ + Update the state of NICs based on the output of a specified ip subcommand. + + :param dict(str, NetworkInterfaceCard) state: Dictionary of NIC state objects + :param str ip_command: The ip command to run + :param handler: A method on the NetworkInterfaceCard class + :param str description: Description of the particular information being added to the state + """ + status, output = shellutil.run_get_output(ip_command, chk_err=True) + if status != 0: + return + + for entry in output.splitlines(): + result = self.ip_command_output.match(entry) + if result: + interface_name = result.group(1) + if interface_name in state: + handler(state[interface_name], result.group(2)) + else: + logger.error("Interface {0} has {1} but no link state".format(interface_name, description)) + + def is_dhcp_enabled(self): + pass + + def start_dhcp_service(self): + pass + + def stop_dhcp_service(self): + pass + + def start_network(self) : + return shellutil.run("/etc/init.d/network start", chk_err=True) + + def restart_ssh_service(self): + # Since Dropbear is the default ssh server on OpenWRt, lets do a sanity check + if os.path.exists("/etc/init.d/sshd"): + return shellutil.run("/etc/init.d/sshd restart", chk_err=True) + else: + logger.warn("sshd service does not exists", username) + + def stop_agent_service(self): + return shellutil.run("/etc/init.d/{0} stop".format(self.service_name), chk_err=True) + + def start_agent_service(self): + return shellutil.run("/etc/init.d/{0} start".format(self.service_name), chk_err=True) + + def register_agent_service(self): + return shellutil.run("/etc/init.d/{0} enable".format(self.service_name), chk_err=True) + + def unregister_agent_service(self): + return shellutil.run("/etc/init.d/{0} disable".format(self.service_name), chk_err=True) + + def set_hostname(self, hostname): + fileutil.write_file('/etc/hostname', hostname) + shellutil.run("uci set system.@system[0].hostname='{0}' && uci commit system && /etc/init.d/system reload".format(hostname), chk_err=False) + + def remove_rules_files(self, rules_files=""): + pass diff -Nru waagent-2.2.34/azurelinuxagent/common/osutil/redhat.py waagent-2.2.45/azurelinuxagent/common/osutil/redhat.py --- waagent-2.2.34/azurelinuxagent/common/osutil/redhat.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/osutil/redhat.py 2019-11-07 00:36:56.000000000 +0000 @@ -50,16 +50,16 @@ return shellutil.run("/sbin/service sshd condrestart", chk_err=False) def stop_agent_service(self): - return shellutil.run("/sbin/service waagent stop", chk_err=False) + return shellutil.run("/sbin/service {0} stop".format(self.service_name), chk_err=False) def start_agent_service(self): - return shellutil.run("/sbin/service waagent start", chk_err=False) + return shellutil.run("/sbin/service {0} start".format(self.service_name), chk_err=False) def register_agent_service(self): - return shellutil.run("chkconfig --add waagent", chk_err=False) + return shellutil.run("chkconfig --add {0}".format(self.service_name), chk_err=False) def unregister_agent_service(self): - return shellutil.run("chkconfig --del waagent", chk_err=False) + return shellutil.run("chkconfig --del {0}".format(self.service_name), chk_err=False) def openssl_to_openssh(self, input_file, output_file): pubkey = fileutil.read_file(input_file) @@ -72,8 +72,7 @@ # Override def get_dhcp_pid(self): - ret = shellutil.run_get_output("pidof dhclient", chk_err=False) - return ret[1] if ret[0] == 0 else None + return self._get_dhcp_pid(["pidof", "dhclient"]) def set_hostname(self, hostname): """ @@ -98,6 +97,7 @@ class RedhatOSUtil(Redhat6xOSUtil): def __init__(self): super(RedhatOSUtil, self).__init__() + self.service_name = self.get_service_name() def set_hostname(self, hostname): """ @@ -118,10 +118,10 @@ super(RedhatOSUtil, self).publish_hostname(hostname) def register_agent_service(self): - return shellutil.run("systemctl enable waagent", chk_err=False) + return shellutil.run("systemctl enable {0}".format(self.service_name), chk_err=False) def unregister_agent_service(self): - return shellutil.run("systemctl disable waagent", chk_err=False) + return shellutil.run("systemctl disable {0}".format(self.service_name), chk_err=False) def openssl_to_openssh(self, input_file, output_file): DefaultOSUtil.openssl_to_openssh(self, input_file, output_file) diff -Nru waagent-2.2.34/azurelinuxagent/common/osutil/suse.py waagent-2.2.45/azurelinuxagent/common/osutil/suse.py --- waagent-2.2.34/azurelinuxagent/common/osutil/suse.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/osutil/suse.py 2019-11-07 00:36:56.000000000 +0000 @@ -32,6 +32,7 @@ from azurelinuxagent.common.version import DISTRO_NAME, DISTRO_VERSION, DISTRO_FULL_NAME from azurelinuxagent.common.osutil.default import DefaultOSUtil + class SUSE11OSUtil(DefaultOSUtil): def __init__(self): @@ -44,9 +45,7 @@ shellutil.run("hostname {0}".format(hostname), chk_err=False) def get_dhcp_pid(self): - ret = shellutil.run_get_output("pidof {0}".format(self.dhclient_name), - chk_err=False) - return ret[1] if ret[0] == 0 else None + return self._get_dhcp_pid(["pidof", self.dhclient_name]) def is_dhcp_enabled(self): return True @@ -66,16 +65,17 @@ return shellutil.run("/sbin/service sshd restart", chk_err=False) def stop_agent_service(self): - return shellutil.run("/sbin/service waagent stop", chk_err=False) + return shellutil.run("/sbin/service {0} stop".format(self.service_name), chk_err=False) def start_agent_service(self): - return shellutil.run("/sbin/service waagent start", chk_err=False) + return shellutil.run("/sbin/service {0} start".format(self.service_name), chk_err=False) def register_agent_service(self): - return shellutil.run("/sbin/insserv waagent", chk_err=False) + return shellutil.run("/sbin/insserv {0}".format(self.service_name), chk_err=False) def unregister_agent_service(self): - return shellutil.run("/sbin/insserv -r waagent", chk_err=False) + return shellutil.run("/sbin/insserv -r {0}".format(self.service_name), chk_err=False) + class SUSEOSUtil(SUSE11OSUtil): def __init__(self): @@ -97,15 +97,13 @@ return shellutil.run("systemctl restart sshd", chk_err=False) def stop_agent_service(self): - return shellutil.run("systemctl stop waagent", chk_err=False) + return shellutil.run("systemctl stop {0}".format(self.service_name), chk_err=False) def start_agent_service(self): - return shellutil.run("systemctl start waagent", chk_err=False) + return shellutil.run("systemctl start {0}".format(self.service_name), chk_err=False) def register_agent_service(self): - return shellutil.run("systemctl enable waagent", chk_err=False) + return shellutil.run("systemctl enable {0}".format(self.service_name), chk_err=False) def unregister_agent_service(self): - return shellutil.run("systemctl disable waagent", chk_err=False) - - + return shellutil.run("systemctl disable {0}".format(self.service_name), chk_err=False) diff -Nru waagent-2.2.34/azurelinuxagent/common/osutil/ubuntu.py waagent-2.2.45/azurelinuxagent/common/osutil/ubuntu.py --- waagent-2.2.34/azurelinuxagent/common/osutil/ubuntu.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/osutil/ubuntu.py 2019-11-07 00:36:56.000000000 +0000 @@ -29,15 +29,20 @@ def __init__(self): super(Ubuntu14OSUtil, self).__init__() self.jit_enabled = True + self.service_name = self.get_service_name() + + @staticmethod + def get_service_name(): + return "walinuxagent" def start_network(self): return shellutil.run("service networking start", chk_err=False) def stop_agent_service(self): - return shellutil.run("service walinuxagent stop", chk_err=False) + return shellutil.run("service {0} stop".format(self.service_name), chk_err=False) def start_agent_service(self): - return shellutil.run("service walinuxagent start", chk_err=False) + return shellutil.run("service {0} start".format(self.service_name), chk_err=False) def remove_rules_files(self, rules_files=""): pass @@ -55,8 +60,7 @@ # Override def get_dhcp_pid(self): - ret = shellutil.run_get_output("pidof dhclient3", chk_err=False) - return ret[1] if ret[0] == 0 else None + return self._get_dhcp_pid(["pidof", "dhclient3"]) def mount_cgroups(self): pass @@ -68,12 +72,13 @@ """ def __init__(self): super(Ubuntu16OSUtil, self).__init__() + self.service_name = self.get_service_name() def register_agent_service(self): - return shellutil.run("systemctl unmask walinuxagent", chk_err=False) + return shellutil.run("systemctl unmask {0}".format(self.service_name), chk_err=False) def unregister_agent_service(self): - return shellutil.run("systemctl mask walinuxagent", chk_err=False) + return shellutil.run("systemctl mask {0}".format(self.service_name), chk_err=False) def mount_cgroups(self): """ @@ -88,10 +93,10 @@ """ def __init__(self): super(Ubuntu18OSUtil, self).__init__() + self.service_name = self.get_service_name() def get_dhcp_pid(self): - ret = shellutil.run_get_output("pidof systemd-networkd") - return ret[1] if ret[0] == 0 else None + return self._get_dhcp_pid(["pidof", "systemd-networkd"]) def start_network(self): return shellutil.run("systemctl start systemd-networkd", chk_err=False) @@ -106,10 +111,10 @@ return self.stop_network() def start_agent_service(self): - return shellutil.run("systemctl start walinuxagent", chk_err=False) + return shellutil.run("systemctl start {0}".format(self.service_name), chk_err=False) def stop_agent_service(self): - return shellutil.run("systemctl stop walinuxagent", chk_err=False) + return shellutil.run("systemctl stop {0}".format(self.service_name), chk_err=False) class UbuntuOSUtil(Ubuntu16OSUtil): diff -Nru waagent-2.2.34/azurelinuxagent/common/protocol/hostplugin.py waagent-2.2.45/azurelinuxagent/common/protocol/hostplugin.py --- waagent-2.2.34/azurelinuxagent/common/protocol/hostplugin.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/protocol/hostplugin.py 2019-11-07 00:36:56.000000000 +0000 @@ -23,9 +23,8 @@ from azurelinuxagent.common import logger from azurelinuxagent.common.errorstate import ErrorState, ERROR_STATE_HOST_PLUGIN_FAILURE -from azurelinuxagent.common.exception import HttpError, ProtocolError, \ - ResourceGoneError -from azurelinuxagent.common.future import ustr, httpclient +from azurelinuxagent.common.exception import HttpError, ProtocolError +from azurelinuxagent.common.future import ustr from azurelinuxagent.common.protocol.healthservice import HealthService from azurelinuxagent.common.utils import restutil from azurelinuxagent.common.utils import textutil @@ -110,6 +109,7 @@ try: headers = {HEADER_CONTAINER_ID: self.container_id} response = restutil.http_get(url, headers) + if restutil.request_failed(response): error_response = restutil.read_response_error(response) logger.error("HostGAPlugin: Failed Get API versions: {0}".format(error_response)) diff -Nru waagent-2.2.34/azurelinuxagent/common/protocol/imds.py waagent-2.2.45/azurelinuxagent/common/protocol/imds.py --- waagent-2.2.34/azurelinuxagent/common/protocol/imds.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/protocol/imds.py 2019-11-07 00:36:56.000000000 +0000 @@ -2,23 +2,31 @@ # Licensed under the Apache License, Version 2.0 (the "License"); import json import re +from collections import namedtuple import azurelinuxagent.common.utils.restutil as restutil -from azurelinuxagent.common.exception import HttpError +from azurelinuxagent.common.exception import HttpError, ResourceGoneError from azurelinuxagent.common.future import ustr import azurelinuxagent.common.logger as logger -from azurelinuxagent.common.protocol.restapi import DataContract, set_properties +from azurelinuxagent.common.datacontract import DataContract, set_properties +from azurelinuxagent.common.protocol.util import get_protocol_util from azurelinuxagent.common.utils.flexible_version import FlexibleVersion IMDS_ENDPOINT = '169.254.169.254' APIVERSION = '2018-02-01' -BASE_URI = "http://{0}/metadata/instance/{1}?api-version={2}" +BASE_METADATA_URI = "http://{0}/metadata/{1}?api-version={2}" IMDS_IMAGE_ORIGIN_UNKNOWN = 0 IMDS_IMAGE_ORIGIN_CUSTOM = 1 IMDS_IMAGE_ORIGIN_ENDORSED = 2 IMDS_IMAGE_ORIGIN_PLATFORM = 3 +MetadataResult = namedtuple('MetadataResult', ['success', 'service_error', 'response']) +IMDS_RESPONSE_SUCCESS = 0 +IMDS_RESPONSE_ERROR = 1 +IMDS_CONNECTION_ERROR = 2 +IMDS_INTERNAL_SERVER_ERROR = 3 + def get_imds_client(): return ImdsClient() @@ -48,7 +56,7 @@ "14.04.6-LTS", "14.04.7-LTS", "14.04.8-LTS", - + "16.04-LTS", "16.04.0-LTS", "18.04-LTS", @@ -76,7 +84,7 @@ "CENTOS-HPC": { "Minimum": "6.3" } }, "REDHAT": { - "RHEL": { + "RHEL": { "Minimum": "6.7", "List": [ "7-LVM", @@ -201,7 +209,6 @@ self.vmScaleSetName = vmScaleSetName self.zone = zone - @property def image_info(self): return "{0}:{1}:{2}:{3}".format(self.publisher, self.offer, self.sku, self.version) @@ -227,7 +234,8 @@ return IMDS_IMAGE_ORIGIN_PLATFORM except Exception as e: - logger.warn("Could not determine the image origin from IMDS: {0}", str(e)) + logger.periodic_warn(logger.EVERY_FIFTEEN_MINUTES, + "[PERIODIC] Could not determine the image origin from IMDS: {0}".format(ustr(e))) return IMDS_IMAGE_ORIGIN_UNKNOWN @@ -242,15 +250,75 @@ 'User-Agent': restutil.HTTP_USER_AGENT_HEALTH, 'Metadata': True, } - pass + self._regex_ioerror = re.compile(r".*HTTP Failed. GET http://[^ ]+ -- IOError .*") + self._regex_throttled = re.compile(r".*HTTP Retry. GET http://[^ ]+ -- Status Code 429 .*") + self._protocol_util = get_protocol_util() + + def _get_metadata_url(self, endpoint, resource_path): + return BASE_METADATA_URI.format(endpoint, resource_path, self._api_version) + + def _http_get(self, endpoint, resource_path, headers): + url = self._get_metadata_url(endpoint, resource_path) + return restutil.http_get(url, headers=headers, use_proxy=False) - @property - def compute_url(self): - return BASE_URI.format(IMDS_ENDPOINT, 'compute', self._api_version) + def _get_metadata_from_endpoint(self, endpoint, resource_path, headers): + """ + Get metadata from one of the IMDS endpoints. - @property - def instance_url(self): - return BASE_URI.format(IMDS_ENDPOINT, '', self._api_version) + :param str endpoint: IMDS endpoint to call + :param str resource_path: path of IMDS resource + :param bool headers: headers to send in the request + :return: Tuple + status: one of the following response status codes: IMDS_RESPONSE_SUCCESS, IMDS_RESPONSE_ERROR, + IMDS_CONNECTION_ERROR, IMDS_INTERNAL_SERVER_ERROR + response: IMDS response on IMDS_RESPONSE_SUCCESS, failure message otherwise + """ + try: + resp = self._http_get(endpoint=endpoint, resource_path=resource_path, headers=headers) + except ResourceGoneError: + return IMDS_INTERNAL_SERVER_ERROR, "IMDS error in /metadata/{0}: HTTP Failed with Status Code 410: Gone".format(resource_path) + except HttpError as e: + msg = str(e) + if self._regex_throttled.match(msg): + return IMDS_RESPONSE_ERROR, "IMDS error in /metadata/{0}: Throttled".format(resource_path) + if self._regex_ioerror.match(msg): + logger.periodic_warn(logger.EVERY_FIFTEEN_MINUTES, + "[PERIODIC] [IMDS_CONNECTION_ERROR] Unable to connect to IMDS endpoint {0}".format(endpoint)) + return IMDS_CONNECTION_ERROR, "IMDS error in /metadata/{0}: Unable to connect to endpoint".format(resource_path) + return IMDS_INTERNAL_SERVER_ERROR, "IMDS error in /metadata/{0}: {1}".format(resource_path, msg) + + if resp.status >= 500: + return IMDS_INTERNAL_SERVER_ERROR, "IMDS error in /metadata/{0}: {1}".format( + resource_path, restutil.read_response_error(resp)) + + if restutil.request_failed(resp): + return IMDS_RESPONSE_ERROR, "IMDS error in /metadata/{0}: {1}".format( + resource_path, restutil.read_response_error(resp)) + + return IMDS_RESPONSE_SUCCESS, resp.read() + + def get_metadata(self, resource_path, is_health): + """ + Get metadata from IMDS, falling back to Wireserver endpoint if necessary. + + :param str resource_path: path of IMDS resource + :param bool is_health: True if for health/heartbeat, False otherwise + :return: instance of MetadataResult + :rtype: MetadataResult + """ + headers = self._health_headers if is_health else self._headers + endpoint = IMDS_ENDPOINT + + status, resp = self._get_metadata_from_endpoint(endpoint, resource_path, headers) + if status == IMDS_CONNECTION_ERROR: + endpoint = self._protocol_util.get_wireserver_endpoint() + status, resp = self._get_metadata_from_endpoint(endpoint, resource_path, headers) + + if status == IMDS_RESPONSE_SUCCESS: + return MetadataResult(True, False, resp) + elif status == IMDS_INTERNAL_SERVER_ERROR: + return MetadataResult(False, True, resp) + return MetadataResult(False, False, resp) def get_compute(self): """ @@ -260,13 +328,12 @@ :rtype: ComputeInfo """ - resp = restutil.http_get(self.compute_url, headers=self._headers) - - if restutil.request_failed(resp): - raise HttpError("{0} - GET: {1}".format(resp.status, self.compute_url)) + # ensure we get a 200 + result = self.get_metadata('instance/compute', is_health=False) + if not result.success: + raise HttpError(result.response) - data = resp.read() - data = json.loads(ustr(data, encoding="utf-8")) + data = json.loads(ustr(result.response, encoding="utf-8")) compute_info = ComputeInfo() set_properties('compute', compute_info, data) @@ -279,19 +346,20 @@ is valid: compute should contain location, name, subscription id, and vm size and network should contain mac address and private ip address. :return: Tuple - is_healthy: True when validation succeeds, False otherwise + is_healthy: False when service returns an error, True on successful + response and connection failures. error_response: validation failure details to assist with debugging """ # ensure we get a 200 - resp = restutil.http_get(self.instance_url, headers=self._health_headers) - if restutil.request_failed(resp): - return False, "{0}".format(restutil.read_response_error(resp)) + result = self.get_metadata('instance', is_health=True) + if not result.success: + # we should only return False when the service is unhealthy + return (not result.service_error), result.response # ensure the response is valid json - data = resp.read() try: - json_data = json.loads(ustr(data, encoding="utf-8")) + json_data = json.loads(ustr(result.response, encoding="utf-8")) except Exception as e: return False, "JSON parsing failed: {0}".format(ustr(e)) diff -Nru waagent-2.2.34/azurelinuxagent/common/protocol/metadata.py waagent-2.2.45/azurelinuxagent/common/protocol/metadata.py --- waagent-2.2.34/azurelinuxagent/common/protocol/metadata.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/protocol/metadata.py 2019-11-07 00:36:56.000000000 +0000 @@ -21,11 +21,18 @@ import os import shutil import re +import sys +import traceback import azurelinuxagent.common.conf as conf +from azurelinuxagent.common.datacontract import get_properties, set_properties, validate_param +from azurelinuxagent.common.exception import HttpError, ProtocolError +import azurelinuxagent.common.logger as logger +from azurelinuxagent.common.utils import restutil import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil import azurelinuxagent.common.utils.textutil as textutil +from azurelinuxagent.common.telemetryevent import TelemetryEventList from azurelinuxagent.common.future import httpclient from azurelinuxagent.common.protocol.restapi import * @@ -41,6 +48,8 @@ P7B_FILE_NAME = "Certificates.p7b" PEM_FILE_NAME = "Certificates.pem" +IF_NONE_MATCH_HEADER = "If-None-Match" + KEY_AGENT_VERSION_URIS = "versionsManifestUris" KEY_URI = "uri" @@ -49,6 +58,14 @@ RETRY_PING_INTERVAL = 10 +def get_traceback(e): + if sys.version_info[0] == 3: + return e.__traceback__ + elif sys.version_info[0] == 2: + ex_type, ex, tb = sys.exc_info() + return tb + + def _add_content_type(headers): if headers is None: headers = {} @@ -81,6 +98,7 @@ self.certs = None self.agent_manifests = None self.agent_etag = None + self.cert_etag = None def _get_data(self, url, headers=None): try: @@ -88,13 +106,21 @@ except HttpError as e: raise ProtocolError(ustr(e)) - if restutil.request_failed(resp): + # NOT_MODIFIED (304) response means the call was successful, so allow that to proceed. + is_not_modified = restutil.request_not_modified(resp) + if restutil.request_failed(resp) and not is_not_modified: raise ProtocolError("{0} - GET: {1}".format(resp.status, url)) data = resp.read() etag = resp.getheader('ETag') + + # If the response was 304, then explicilty set data to None + if is_not_modified: + data = None + if data is not None: data = json.loads(ustr(data, encoding="utf-8")) + return data, etag def _put_data(self, url, data, headers=None): @@ -123,6 +149,10 @@ content = fileutil.read_file(trans_crt_file) return textutil.get_bytes_from_pem(content) + def supports_overprovisioning(self): + # Metadata protocol does not support overprovisioning + return False + def detect(self): self.get_vminfo() trans_prv_file = os.path.join(conf.get_lib_dir(), @@ -151,23 +181,27 @@ def get_certs(self): certlist = CertList() certificatedata = CertificateData() - data, etag = self._get_data(self.cert_uri) + headers = None if self.cert_etag is None else {IF_NONE_MATCH_HEADER: self.cert_etag} + data, etag = self._get_data(self.cert_uri, headers=headers) - set_properties("certlist", certlist, data) + if self.cert_etag is None or self.cert_etag != etag: + self.cert_etag = etag - cert_list = get_properties(certlist) + set_properties("certlist", certlist, data) - headers = { - "x-ms-vmagent-public-x509-cert": self._get_trans_cert() - } + cert_list = get_properties(certlist) + + headers = { + "x-ms-vmagent-public-x509-cert": self._get_trans_cert() + } - for cert_i in cert_list["certificates"]: - certificate_data_uri = cert_i['certificateDataUri'] - data, etag = self._get_data(certificate_data_uri, headers=headers) - set_properties("certificatedata", certificatedata, data) - json_certificate_data = get_properties(certificatedata) + for cert_i in cert_list["certificates"]: + certificate_data_uri = cert_i['certificateDataUri'] + data, etag = self._get_data(certificate_data_uri, headers=headers) + set_properties("certificatedata", certificatedata, data) + json_certificate_data = get_properties(certificatedata) - self.certs = Certificates(self, json_certificate_data) + self.certs = Certificates(self, json_certificate_data) if self.certs is None: return None @@ -181,8 +215,10 @@ def get_vmagent_manifests(self): self.update_goal_state() - data, etag = self._get_data(self.vmagent_uri) - if self.agent_etag is None or self.agent_etag < etag: + headers = None if self.agent_etag is None else {IF_NONE_MATCH_HEADER: self.agent_etag} + + data, etag = self._get_data(self.vmagent_uri, headers=headers) + if self.agent_etag is None or self.agent_etag != etag: self.agent_etag = etag # Create a list with a single manifest @@ -236,7 +272,7 @@ } ext_list = ExtHandlerList() data, etag = self._get_data(self.ext_uri, headers=headers) - if last_etag is None or last_etag < etag: + if last_etag is None or last_etag != etag: set_properties("extensionHandlers", ext_list.extHandlers, data) return ext_list, etag @@ -300,8 +336,12 @@ try: self.update_certs() return - except: + except Exception as e: logger.verbose("Incarnation is out of date. Update goalstate.") + msg = u"Exception updating certs: {0}".format(ustr(e)) + logger.warn(msg) + detailed_msg = '{0} {1}'.format(msg, traceback.extract_tb(get_traceback(e))) + logger.verbose(detailed_msg) raise ProtocolError("Exceeded max retry updating goal state") def download_ext_handler_pkg(self, uri, destination, headers=None, use_proxy=True): diff -Nru waagent-2.2.34/azurelinuxagent/common/protocol/restapi.py waagent-2.2.45/azurelinuxagent/common/protocol/restapi.py --- waagent-2.2.34/azurelinuxagent/common/protocol/restapi.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/protocol/restapi.py 2019-11-07 00:36:56.000000000 +0000 @@ -16,76 +16,11 @@ # # Requires Python 2.6+ and Openssl 1.0+ # + import socket -import azurelinuxagent.common.logger as logger -import azurelinuxagent.common.utils.restutil as restutil -from azurelinuxagent.common.exception import ProtocolError, HttpError from azurelinuxagent.common.future import ustr -from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common.version import DISTRO_VERSION, DISTRO_NAME, CURRENT_VERSION - - -def validate_param(name, val, expected_type): - if val is None: - raise ProtocolError("{0} is None".format(name)) - if not isinstance(val, expected_type): - raise ProtocolError(("{0} type should be {1} not {2}" - "").format(name, expected_type, type(val))) - - -def set_properties(name, obj, data): - if isinstance(obj, DataContract): - validate_param("Property '{0}'".format(name), data, dict) - for prob_name, prob_val in data.items(): - prob_full_name = "{0}.{1}".format(name, prob_name) - try: - prob = getattr(obj, prob_name) - except AttributeError: - logger.warn("Unknown property: {0}", prob_full_name) - continue - prob = set_properties(prob_full_name, prob, prob_val) - setattr(obj, prob_name, prob) - return obj - elif isinstance(obj, DataContractList): - validate_param("List '{0}'".format(name), data, list) - for item_data in data: - item = obj.item_cls() - item = set_properties(name, item, item_data) - obj.append(item) - return obj - else: - return data - - -def get_properties(obj): - if isinstance(obj, DataContract): - data = {} - props = vars(obj) - for prob_name, prob in list(props.items()): - data[prob_name] = get_properties(prob) - return data - elif isinstance(obj, DataContractList): - data = [] - for item in obj: - item_data = get_properties(item) - data.append(item_data) - return data - else: - return obj - - -class DataContract(object): - pass - - -class DataContractList(list): - def __init__(self, item_cls): - self.item_cls = item_cls - - -""" -Data contract between guest and host -""" +from azurelinuxagent.common.datacontract import DataContract, DataContractList class VMInfo(DataContract): @@ -286,24 +221,6 @@ self.vmAgent = VMAgentStatus(status=status, message=message) -class TelemetryEventParam(DataContract): - def __init__(self, name=None, value=None): - self.name = name - self.value = value - - -class TelemetryEvent(DataContract): - def __init__(self, eventId=None, providerId=None): - self.eventId = eventId - self.providerId = providerId - self.parameters = DataContractList(TelemetryEventParam) - - -class TelemetryEventList(DataContract): - def __init__(self): - self.events = DataContractList(TelemetryEvent) - - class RemoteAccessUser(DataContract): def __init__(self, name, encrypted_password, expiration): self.name = name @@ -358,3 +275,6 @@ def report_event(self, event): raise NotImplementedError() + + def supports_overprovisioning(self): + return True diff -Nru waagent-2.2.34/azurelinuxagent/common/protocol/util.py waagent-2.2.45/azurelinuxagent/common/protocol/util.py --- waagent-2.2.34/azurelinuxagent/common/protocol/util.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/protocol/util.py 2019-11-07 00:36:56.000000000 +0000 @@ -152,7 +152,7 @@ conf.get_lib_dir(), TAG_FILE_NAME) - def _get_wireserver_endpoint(self): + def get_wireserver_endpoint(self): try: file_path = os.path.join(conf.get_lib_dir(), ENDPOINT_FILE_NAME) return fileutil.read_file(file_path) @@ -182,7 +182,7 @@ endpoint = self.dhcp_handler.endpoint else: logger.info("_detect_wire_protocol: DHCP not available") - endpoint = self._get_wireserver_endpoint() + endpoint = self.get_wireserver_endpoint() if endpoint == None: endpoint = conf_endpoint logger.info("Using hardcoded WireServer endpoint {0}", endpoint) @@ -239,7 +239,7 @@ protocol_name = fileutil.read_file(protocol_file_path) if protocol_name == prots.WireProtocol: - endpoint = self._get_wireserver_endpoint() + endpoint = self.get_wireserver_endpoint() return WireProtocol(endpoint) elif protocol_name == prots.MetadataProtocol: return MetadataProtocol() diff -Nru waagent-2.2.34/azurelinuxagent/common/protocol/wire.py waagent-2.2.45/azurelinuxagent/common/protocol/wire.py --- waagent-2.2.34/azurelinuxagent/common/protocol/wire.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/protocol/wire.py 2019-11-07 00:36:56.000000000 +0000 @@ -15,34 +15,33 @@ # limitations under the License. # # Requires Python 2.6+ and Openssl 1.0+ + import datetime import json import os import random import re -import sys import time -import traceback import xml.sax.saxutils as saxutils - from datetime import datetime import azurelinuxagent.common.conf as conf -import azurelinuxagent.common.utils.fileutil as fileutil +from azurelinuxagent.common.datacontract import validate_param, set_properties +from azurelinuxagent.common.event import add_event, add_periodic, WALAEventOperation, CONTAINER_ID_ENV_VARIABLE import azurelinuxagent.common.utils.textutil as textutil - from azurelinuxagent.common.exception import ProtocolNotFoundError, \ - ResourceGoneError + ResourceGoneError, ExtensionDownloadError, InvalidContainerError, ProtocolError, HttpError from azurelinuxagent.common.future import httpclient, bytebuffer -from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol, URI_FORMAT_GET_EXTENSION_ARTIFACT, \ - HOST_PLUGIN_PORT +import azurelinuxagent.common.logger as logger +from azurelinuxagent.common.utils import fileutil, restutil +from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol from azurelinuxagent.common.protocol.restapi import * +from azurelinuxagent.common.telemetryevent import TelemetryEventList from azurelinuxagent.common.utils.archive import StateFlusher from azurelinuxagent.common.utils.cryptutil import CryptUtil from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, \ findtext, getattrib, gettext, remove_bom, get_bytes_from_pem, parse_json -from azurelinuxagent.common.version import AGENT_NAME -from azurelinuxagent.common.osutil import get_osutil +from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION VERSION_INFO_URI = "http://{0}/?comp=versions" GOAL_STATE_URI = "http://{0}/machine/?comp=goalstate" @@ -64,12 +63,16 @@ AGENTS_MANIFEST_FILE_NAME = "{0}.{1}.agentsManifest" TRANSPORT_CERT_FILE_NAME = "TransportCert.pem" TRANSPORT_PRV_FILE_NAME = "TransportPrivate.pem" +# Store the last retrieved container id as an environment variable to be shared between threads for telemetry purposes +CONTAINER_ID_ENV_VARIABLE = "AZURE_GUEST_AGENT_CONTAINER_ID" PROTOCOL_VERSION = "2012-11-30" ENDPOINT_FINE_NAME = "WireServer" SHORT_WAITING_INTERVAL = 1 # 1 second +MAX_EVENT_BUFFER_SIZE = 2 ** 16 - 2 ** 10 + class UploadError(HttpError): pass @@ -118,7 +121,6 @@ vminfo.tenantName = hosting_env.deployment_name vminfo.roleName = hosting_env.role_name vminfo.roleInstanceName = goal_state.role_instance_id - vminfo.containerId = goal_state.container_id return vminfo def get_certs(self): @@ -164,14 +166,22 @@ logger.verbose("Get In-VM Artifacts Profile") return self.client.get_artifacts_profile() + def download_ext_handler_pkg_through_host(self, uri, destination): + host = self.client.get_host_plugin() + uri, headers = host.get_artifact_request(uri, host.manifest_uri) + success = self.client.stream(uri, destination, headers=headers, use_proxy=False) + return success + def download_ext_handler_pkg(self, uri, destination, headers=None, use_proxy=True): - success = self.client.stream(uri, destination, headers=headers, use_proxy=use_proxy) + direct_func = lambda: self.client.stream(uri, destination, headers=None, use_proxy=True) + # NOTE: the host_func may be called after refreshing the goal state, be careful about any goal state data + # in the lambda. + host_func = lambda: self.download_ext_handler_pkg_through_host(uri, destination) - if not success: - logger.verbose("Download did not succeed, falling back to host plugin") - host = self.client.get_host_plugin() - uri, headers = host.get_artifact_request(uri, host.manifest_uri) - success = self.client.stream(uri, destination, headers=headers, use_proxy=False) + try: + success = self.client.send_request_using_appropriate_channel(direct_func, host_func) + except Exception: + success = False return success @@ -261,10 +271,10 @@ Convert VMStatus object to status blob format """ v1_ga_guest_info = { - "computerName" : ga_status.hostname, - "osName" : ga_status.osname, - "osVersion" : ga_status.osversion, - "version" : ga_status.version, + "computerName": ga_status.hostname, + "osName": ga_status.osname, + "osVersion": ga_status.osversion, + "version": ga_status.version, } return v1_ga_guest_info @@ -275,9 +285,9 @@ 'message': ga_status.message } v1_ga_status = { - "version" : ga_status.version, - "status" : ga_status.status, - "formattedMessage" : formatted_msg + "version": ga_status.version, + "status": ga_status.status, + "formattedMessage": formatted_msg } return v1_ga_status @@ -370,7 +380,7 @@ 'version': '1.1', 'timestampUTC': timestamp, 'aggregateStatus': v1_agg_status, - 'guestOSInfo' : v1_ga_guest_info + 'guestOSInfo': v1_ga_guest_info } return v1_vm_status @@ -596,6 +606,12 @@ return http_req(*args, **kwargs) + def fetch_manifest_through_host(self, uri): + host = self.get_host_plugin() + uri, headers = host.get_artifact_request(uri) + response = self.fetch(uri, headers, use_proxy=False) + return response + def fetch_manifest(self, version_uris): logger.verbose("Fetch manifest") version_uris_shuffled = version_uris @@ -608,38 +624,22 @@ logger.verbose('The specified manifest URL is empty, ignored.') continue - response = None - if not HostPluginProtocol.is_default_channel(): - response = self.fetch(version.uri) - - if not response: - if HostPluginProtocol.is_default_channel(): - logger.verbose("Using host plugin as default channel") - else: - logger.verbose("Failed to download manifest, " - "switching to host plugin") - - try: - host = self.get_host_plugin() - uri, headers = host.get_artifact_request(version.uri) - response = self.fetch(uri, headers, use_proxy=False) + direct_func = lambda: self.fetch(version.uri) + # NOTE: the host_func may be called after refreshing the goal state, be careful about any goal state data + # in the lambda. + host_func = lambda: self.fetch_manifest_through_host(version.uri) - # If the HostPlugin rejects the request, - # let the error continue, but set to use the HostPlugin - except ResourceGoneError: - HostPluginProtocol.set_default_channel(True) - raise - - host.manifest_uri = version.uri - logger.verbose("Manifest downloaded successfully from host plugin") - if not HostPluginProtocol.is_default_channel(): - logger.info("Setting host plugin as default channel") - HostPluginProtocol.set_default_channel(True) + try: + response = self.send_request_using_appropriate_channel(direct_func, host_func) - if response: - return response + if response: + host = self.get_host_plugin() + host.manifest_uri = version.uri + return response + except Exception as e: + logger.warn("Exception when fetching manifest. Error: {0}".format(ustr(e))) - raise ProtocolError("Failed to fetch manifest from all sources") + raise ExtensionDownloadError("Failed to fetch manifest from all sources") def stream(self, uri, destination, headers=None, use_proxy=None): success = False @@ -674,10 +674,10 @@ resp = None try: resp = self.call_storage_service( - restutil.http_get, - uri, - headers=headers, - use_proxy=use_proxy) + restutil.http_get, + uri, + headers=headers, + use_proxy=use_proxy) if restutil.request_failed(resp): error_response = restutil.read_response_error(resp) @@ -695,7 +695,7 @@ except (HttpError, ProtocolError, IOError) as e: logger.verbose("Fetch failed from [{0}]: {1}", uri, e) - if isinstance(e, ResourceGoneError): + if isinstance(e, ResourceGoneError) or isinstance(e, InvalidContainerError): raise return resp @@ -730,7 +730,7 @@ if goal_state.remote_access_uri is None: # Nothing in accounts data. Just return, nothing to do. return - xml_text = self.fetch_config(goal_state.remote_access_uri, + xml_text = self.fetch_config(goal_state.remote_access_uri, self.get_header_for_cert()) self.remote_access = RemoteAccess(xml_text) local_file = os.path.join(conf.get_lib_dir(), REMOTE_ACCESS_FILE_NAME.format(self.remote_access.incarnation)) @@ -748,7 +748,7 @@ xml_text = self.fetch_cache(remote_access_file) remote_access = RemoteAccess(xml_text) return remote_access - + def update_ext_conf(self, goal_state): if goal_state.ext_uri is None: logger.info("ExtensionsConfig.xml uri is empty") @@ -761,9 +761,20 @@ self.save_cache(local_file, xml_text) self.ext_conf = ExtensionsConfig(xml_text) + def save_or_update_goal_state_file(self, incarnation, xml_text): + # It should create a new file if the incarnation number is new. + # It should overwrite the existing file if the incarnation number is the same. + file_name = GOAL_STATE_FILE_NAME.format(incarnation) + goal_state_file = os.path.join(conf.get_lib_dir(), file_name) + self.save_cache(goal_state_file, xml_text) + + def update_host_plugin(self, container_id, role_config_name): + if self.host_plugin is not None: + self.host_plugin.container_id = container_id + self.host_plugin.role_config_name = role_config_name + def update_goal_state(self, forced=False, max_retry=3): - incarnation_file = os.path.join(conf.get_lib_dir(), - INCARNATION_FILE_NAME) + incarnation_file = os.path.join(conf.get_lib_dir(), INCARNATION_FILE_NAME) uri = GOAL_STATE_URI.format(self.endpoint) current_goal_state_from_configuration = None @@ -776,29 +787,31 @@ if not forced: last_incarnation = None if os.path.isfile(incarnation_file): - last_incarnation = fileutil.read_file( - incarnation_file) + last_incarnation = fileutil.read_file(incarnation_file) new_incarnation = current_goal_state_from_configuration.incarnation - if last_incarnation is not None and \ - last_incarnation == new_incarnation: - # Goalstate is not updated. - return + + if last_incarnation is not None and last_incarnation == new_incarnation: + # Incarnation number is not updated, but role config file and container ID + # can change without the incarnation number changing. Ensure they are updated in + # the goal state file on disk, as well as in the HostGA plugin instance. + self.goal_state = current_goal_state_from_configuration + self.save_or_update_goal_state_file(new_incarnation, xml_text) + self.update_host_plugin(current_goal_state_from_configuration.container_id, + current_goal_state_from_configuration.role_config_name) + + return self.goal_state_flusher.flush(datetime.utcnow()) self.goal_state = current_goal_state_from_configuration - file_name = GOAL_STATE_FILE_NAME.format(current_goal_state_from_configuration.incarnation) - goal_state_file = os.path.join(conf.get_lib_dir(), file_name) - self.save_cache(goal_state_file, xml_text) + self.save_or_update_goal_state_file(current_goal_state_from_configuration.incarnation, xml_text) self.update_hosting_env(current_goal_state_from_configuration) self.update_shared_conf(current_goal_state_from_configuration) self.update_certs(current_goal_state_from_configuration) self.update_ext_conf(current_goal_state_from_configuration) self.update_remote_access_conf(current_goal_state_from_configuration) self.save_cache(incarnation_file, current_goal_state_from_configuration.incarnation) - - if self.host_plugin is not None: - self.host_plugin.container_id = current_goal_state_from_configuration.container_id - self.host_plugin.role_config_name = current_goal_state_from_configuration.role_config_name + self.update_host_plugin(current_goal_state_from_configuration.container_id, + current_goal_state_from_configuration.role_config_name) return @@ -816,7 +829,7 @@ logger.error("ProtocolError processing goal state, giving up [{0}]", ustr(e)) except Exception as e: - if retry < max_retry-1: + if retry < max_retry - 1: logger.verbose("Exception processing goal state, retrying: [{0}]", ustr(e)) else: logger.error("Exception processing goal state, giving up: [{0}]", ustr(e)) @@ -886,27 +899,18 @@ local_file = os.path.join(conf.get_lib_dir(), local_file) xml_text = self.fetch_cache(local_file) self.ext_conf = ExtensionsConfig(xml_text) - return self.ext_conf + return self.ext_conf def get_ext_manifest(self, ext_handler, goal_state): - for update_goal_state in [False, True]: - try: - if update_goal_state: - self.update_goal_state(forced=True) - goal_state = self.get_goal_state() - - local_file = MANIFEST_FILE_NAME.format( - ext_handler.name, - goal_state.incarnation) - local_file = os.path.join(conf.get_lib_dir(), local_file) - xml_text = self.fetch_manifest(ext_handler.versionUris) - self.save_cache(local_file, xml_text) - return ExtensionManifest(xml_text) - - except ResourceGoneError: - continue + local_file = MANIFEST_FILE_NAME.format(ext_handler.name, goal_state.incarnation) + local_file = os.path.join(conf.get_lib_dir(), local_file) - raise ProtocolError("Failed to retrieve extension manifest") + try: + xml_text = self.fetch_manifest(ext_handler.versionUris) + self.save_cache(local_file, xml_text) + return ExtensionManifest(xml_text) + except Exception as e: + raise ExtensionDownloadError("Failed to retrieve extension manifest. Error: {0}".format(ustr(e))) def filter_package_list(self, family, ga_manifest, goal_state): complete_list = ga_manifest.pkg_list @@ -944,29 +948,17 @@ return allowed_list def get_gafamily_manifest(self, vmagent_manifest, goal_state): - for update_goal_state in [False, True]: - try: - if update_goal_state: - self.update_goal_state(forced=True) - goal_state = self.get_goal_state() - - self._remove_stale_agent_manifest( - vmagent_manifest.family, - goal_state.incarnation) - - local_file = MANIFEST_FILE_NAME.format( - vmagent_manifest.family, - goal_state.incarnation) - local_file = os.path.join(conf.get_lib_dir(), local_file) - xml_text = self.fetch_manifest( - vmagent_manifest.versionsManifestUris) - fileutil.write_file(local_file, xml_text) - return ExtensionManifest(xml_text) + self._remove_stale_agent_manifest(vmagent_manifest.family, goal_state.incarnation) - except ResourceGoneError: - continue + local_file = MANIFEST_FILE_NAME.format(vmagent_manifest.family, goal_state.incarnation) + local_file = os.path.join(conf.get_lib_dir(), local_file) - raise ProtocolError("Failed to retrieve GAFamily manifest") + try: + xml_text = self.fetch_manifest(vmagent_manifest.versionsManifestUris) + fileutil.write_file(local_file, xml_text) + return ExtensionManifest(xml_text) + except Exception as e: + raise ProtocolError("Failed to retrieve GAFamily manifest. Error: {0}".format(ustr(e))) def _remove_stale_agent_manifest(self, family, incarnation): """ @@ -1003,6 +995,96 @@ "advised by Fabric.").format(PROTOCOL_VERSION) raise ProtocolNotFoundError(error) + def send_request_using_appropriate_channel(self, direct_func, host_func): + # A wrapper method for all function calls that send HTTP requests. The purpose of the method is to + # define which channel to use, direct or through the host plugin. For the host plugin channel, + # also implement a retry mechanism. + + # By default, the direct channel is the default channel. If that is the case, try getting a response + # through that channel. On failure, fall back to the host plugin channel. + + # When using the host plugin channel, regardless if it's set as default or not, try sending the request first. + # On specific failures that indicate a stale goal state (such as resource gone or invalid container parameter), + # refresh the goal state and try again. If successful, set the host plugin channel as default. If failed, + # raise the exception. + + # NOTE: direct_func and host_func are passed as lambdas. Be careful about capturing goal state data in them as + # they will not be refreshed even if a goal state refresh is called before retrying the host_func. + + if not HostPluginProtocol.is_default_channel(): + ret = None + try: + ret = direct_func() + + # Different direct channel functions report failure in different ways: by returning None, False, + # or raising ResourceGone or InvalidContainer exceptions. + if not ret: + logger.periodic_info(logger.EVERY_HOUR, "[PERIODIC] Request failed using the direct channel, " + "switching to host plugin.") + except (ResourceGoneError, InvalidContainerError) as e: + logger.periodic_info(logger.EVERY_HOUR, "[PERIODIC] Request failed using the direct channel, " + "switching to host plugin. Error: {0}".format(ustr(e))) + + if ret: + return ret + else: + logger.periodic_info(logger.EVERY_HALF_DAY, "[PERIODIC] Using host plugin as default channel.") + + try: + ret = host_func() + except (ResourceGoneError, InvalidContainerError) as e: + old_container_id = self.host_plugin.container_id + old_role_config_name = self.host_plugin.role_config_name + + msg = "[PERIODIC] Request failed with the current host plugin configuration. " \ + "ContainerId: {0}, role config file: {1}. Fetching new goal state and retrying the call." \ + "Error: {2}".format(old_container_id, old_role_config_name, ustr(e)) + logger.periodic_info(logger.EVERY_SIX_HOURS, msg) + + self.update_goal_state(forced=True) + + new_container_id = self.host_plugin.container_id + new_role_config_name = self.host_plugin.role_config_name + msg = "[PERIODIC] Host plugin reconfigured with new parameters. " \ + "ContainerId: {0}, role config file: {1}.".format(new_container_id, new_role_config_name) + logger.periodic_info(logger.EVERY_SIX_HOURS, msg) + + try: + ret = host_func() + if ret: + msg = "[PERIODIC] Request succeeded using the host plugin channel after goal state refresh. " \ + "ContainerId changed from {0} to {1}, " \ + "role config file changed from {2} to {3}.".format(old_container_id, new_container_id, + old_role_config_name, new_role_config_name) + add_periodic(delta=logger.EVERY_SIX_HOURS, + name=AGENT_NAME, + version=CURRENT_VERSION, + op=WALAEventOperation.HostPlugin, + is_success=True, + message=msg, + log_event=True) + + except (ResourceGoneError, InvalidContainerError) as e: + msg = "[PERIODIC] Request failed using the host plugin channel after goal state refresh. " \ + "ContainerId changed from {0} to {1}, role config file changed from {2} to {3}. " \ + "Exception type: {4}.".format(old_container_id, new_container_id, old_role_config_name, + new_role_config_name, type(e).__name__) + add_periodic(delta=logger.EVERY_SIX_HOURS, + name=AGENT_NAME, + version=CURRENT_VERSION, + op=WALAEventOperation.HostPlugin, + is_success=False, + message=msg, + log_event=True) + raise + + if not HostPluginProtocol.is_default_channel(): + logger.info("Setting host plugin as default channel from now on. " + "Restart the agent to reset the default channel.") + HostPluginProtocol.set_default_channel(True) + + return ret + def upload_status_blob(self): self.update_goal_state() ext_conf = self.get_ext_conf() @@ -1029,15 +1111,13 @@ # direct route. # # The code previously preferred the "direct" route always, and only fell back - # to the HostPlugin *if* there was an error. We would like to move to + # to the HostPlugin *if* there was an error. We would like to move to # the HostPlugin for all traffic, but this is a big change. We would like # to see how this behaves at scale, and have a fallback should things go - # wrong. This is why we try HostPlugin then direct. + # wrong. This is why we try HostPlugin then direct. try: host = self.get_host_plugin() - host.put_vm_status(self.status_blob, - ext_conf.status_upload_blob, - ext_conf.status_upload_blob_type) + host.put_vm_status(self.status_blob, ext_conf.status_upload_blob, ext_conf.status_upload_blob_type) return except ResourceGoneError: # do not attempt direct, force goal state update and wait to try again @@ -1117,6 +1197,8 @@ data = data_format.format(provider_id, event_str) try: header = self.get_header_for_xml_content() + # NOTE: The call to wireserver requests utf-8 encoding in the headers, but the body should not + # be encoded: some nodes in the telemetry pipeline do not support utf-8 encoding. resp = self.call_wireserver(restutil.http_post, uri, data, header) except HttpError as e: raise ProtocolError("Failed to send events:{0}".format(e)) @@ -1133,10 +1215,14 @@ if event.providerId not in buf: buf[event.providerId] = "" event_str = event_to_v1(event) - if len(event_str) >= 63 * 1024: - logger.warn("Single event too large: {0}", event_str[300:]) + if len(event_str) >= MAX_EVENT_BUFFER_SIZE: + details_of_event = [ustr(x.name) + ":" + ustr(x.value) for x in event.parameters if x.name in + ["Name", "Version", "Operation", "OperationSuccess"]] + logger.periodic_warn(logger.EVERY_HALF_HOUR, + "Single event too large: {0}, with the length: {1} more than the limit({2})" + .format(str(details_of_event), len(event_str), MAX_EVENT_BUFFER_SIZE)) continue - if len(buf[event.providerId] + event_str) >= 63 * 1024: + if len(buf[event.providerId] + event_str) >= MAX_EVENT_BUFFER_SIZE: self.send_event(event.providerId, buf[event.providerId]) buf[event.providerId] = "" buf[event.providerId] = buf[event.providerId] + event_str @@ -1148,7 +1234,7 @@ def report_status_event(self, message, is_success): from azurelinuxagent.common.event import report_event, \ - WALAEventOperation + WALAEventOperation report_event(op=WALAEventOperation.ReportStatus, is_success=is_success, @@ -1190,59 +1276,48 @@ def has_artifacts_profile_blob(self): return self.ext_conf and not \ - textutil.is_str_none_or_whitespace(self.ext_conf.artifacts_profile_blob) + textutil.is_str_none_or_whitespace(self.ext_conf.artifacts_profile_blob) + + def get_artifacts_profile_through_host(self, blob): + host = self.get_host_plugin() + uri, headers = host.get_artifact_request(blob) + profile = self.fetch(uri, headers, use_proxy=False) + return profile def get_artifacts_profile(self): artifacts_profile = None - for update_goal_state in [False, True]: - try: - if update_goal_state: - self.update_goal_state(forced=True) - - if self.has_artifacts_profile_blob(): - blob = self.ext_conf.artifacts_profile_blob - profile = None - if not HostPluginProtocol.is_default_channel(): - logger.verbose("Retrieving the artifacts profile") - profile = self.fetch(blob) - - if profile is None: - if HostPluginProtocol.is_default_channel(): - logger.verbose("Using host plugin as default channel") - else: - logger.verbose("Failed to download artifacts profile, " - "switching to host plugin") - - host = self.get_host_plugin() - uri, headers = host.get_artifact_request(blob) - profile = self.fetch(uri, headers, use_proxy=False) - - if not textutil.is_str_empty(profile): - logger.verbose("Artifacts profile downloaded") - try: - artifacts_profile = InVMArtifactsProfile(profile) - except Exception: - logger.warn("Could not parse artifacts profile blob") - msg = "Content: [{0}]".format(profile) - logger.verbose(msg) - - from azurelinuxagent.common.event import report_event, WALAEventOperation - report_event(op=WALAEventOperation.ArtifactsProfileBlob, - is_success=False, - message=msg, - log_event=False) + if self.has_artifacts_profile_blob(): + blob = self.ext_conf.artifacts_profile_blob + direct_func = lambda: self.fetch(blob) + # NOTE: the host_func may be called after refreshing the goal state, be careful about any goal state data + # in the lambda. + host_func = lambda: self.get_artifacts_profile_through_host(blob) - return artifacts_profile - - except ResourceGoneError: - HostPluginProtocol.set_default_channel(True) - continue + logger.verbose("Retrieving the artifacts profile") + try: + profile = self.send_request_using_appropriate_channel(direct_func, host_func) except Exception as e: logger.warn("Exception retrieving artifacts profile: {0}".format(ustr(e))) + return None - return None + if not textutil.is_str_empty(profile): + logger.verbose("Artifacts profile downloaded") + try: + artifacts_profile = InVMArtifactsProfile(profile) + except Exception: + logger.warn("Could not parse artifacts profile blob") + msg = "Content: [{0}]".format(profile) + logger.verbose(msg) + + from azurelinuxagent.common.event import report_event, WALAEventOperation + report_event(op=WALAEventOperation.ArtifactsProfileBlob, + is_success=False, + message=msg, + log_event=False) + + return artifacts_profile class VersionInfo(object): @@ -1314,6 +1389,7 @@ self.role_config_name = findtext(role_config, "ConfigName") container = find(xml_doc, "Container") self.container_id = findtext(container, "ContainerId") + os.environ[CONTAINER_ID_ENV_VARIABLE] = self.container_id self.remote_access_uri = findtext(container, "RemoteAccessInfo") lbprobe_ports = find(xml_doc, "LBProbePorts") self.load_balancer_probe_port = findtext(lbprobe_ports, "Port") @@ -1372,6 +1448,7 @@ """ Object containing information about user accounts """ + # # # @@ -1420,10 +1497,12 @@ remote_access_user = RemoteAccessUser(name, encrypted_password, expiration) return remote_access_user + class UserAccount(object): """ Stores information about single user account """ + def __init__(self): self.Name = None self.EncryptedPassword = None @@ -1452,6 +1531,12 @@ if data is None: return + # if the certificates format is not Pkcs7BlobWithPfxContents do not parse it + certificateFormat = findtext(xml_doc, "Format") + if certificateFormat and certificateFormat != "Pkcs7BlobWithPfxContents": + logger.warn("The Format is not Pkcs7BlobWithPfxContents. Format is " + certificateFormat) + return + cryptutil = CryptUtil(conf.get_openssl_cmd()) p7m_file = os.path.join(conf.get_lib_dir(), P7M_FILE_NAME) p7m = ("MIME-Version:1.0\n" @@ -1517,6 +1602,18 @@ tmp_file = prvs[pubkey] prv = "{0}.prv".format(thumbprint) os.rename(tmp_file, os.path.join(conf.get_lib_dir(), prv)) + logger.info("Found private key matching thumbprint {0}".format(thumbprint)) + else: + # Since private key has *no* matching certificate, + # it will not be named correctly + logger.warn("Found NO matching cert/thumbprint for private key!") + + # Log if any certificates were found without matching private keys + # This can happen (rarely), and is useful to know for debugging + for pubkey in thumbprints: + if not pubkey in prvs: + msg = "Certificate with thumbprint {0} has no matching private key." + logger.info(msg.format(thumbprints[pubkey])) for v1_cert in v1_cert_list: cert = Cert() @@ -1704,6 +1801,7 @@ * encryptedHealthChecks (optional) * encryptedApplicationProfile (optional) """ + def __init__(self, artifacts_profile): if not textutil.is_str_empty(artifacts_profile): self.__dict__.update(parse_json(artifacts_profile)) @@ -1711,5 +1809,5 @@ def is_on_hold(self): # hasattr() is not available in Python 2.6 if 'onHold' in self.__dict__: - return self.onHold.lower() == 'true' + return str(self.onHold).lower() == 'true' return False diff -Nru waagent-2.2.34/azurelinuxagent/common/rdma.py waagent-2.2.45/azurelinuxagent/common/rdma.py --- waagent-2.2.34/azurelinuxagent/common/rdma.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/rdma.py 2019-11-07 00:36:56.000000000 +0000 @@ -22,7 +22,6 @@ import os import re import time -import threading import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger @@ -39,7 +38,7 @@ '/usr/local/etc/dat.conf' ] -def setup_rdma_device(): +def setup_rdma_device(nd_version): logger.verbose("Parsing SharedConfig XML contents for RDMA details") xml_doc = parse_doc( fileutil.read_file(os.path.join(conf.get_lib_dir(), SHARED_CONF_FILE_NAME))) @@ -71,19 +70,22 @@ rdma_ipv4_addr, rdma_mac_addr)) # Set up the RDMA device with collected informatino - RDMADeviceHandler(rdma_ipv4_addr, rdma_mac_addr).start() + RDMADeviceHandler(rdma_ipv4_addr, rdma_mac_addr, nd_version).start() logger.info("RDMA: device is set up") return class RDMAHandler(object): driver_module_name = 'hv_network_direct' + nd_version = None - @staticmethod - def get_rdma_version(): + def get_rdma_version(self): """Retrieve the firmware version information from the system. This depends on information provided by the Linux kernel.""" + if self.nd_version : + return self.nd_version + kvp_key_size = 512 kvp_value_size = 2048 driver_info_source = '/var/lib/hyperv/.kvp_pool_0' @@ -104,7 +106,8 @@ value_0 = value.split("\x00")[0] if key_0 == "NdDriverVersion" : f.close() - return value_0 + self.nd_version = value_0 + return self.nd_version else : break f.close() @@ -148,6 +151,15 @@ logger.info('RDMA: Loaded the kernel driver successfully.') return True + def install_driver_if_needed(self): + if self.nd_version: + if conf.enable_check_rdma_driver(): + self.install_driver() + else: + logger.info('RDMA: check RDMA driver is disabled, skip installing driver') + else: + logger.info('RDMA: skip installing driver when ndversion not present\n') + def install_driver(self): """Install the driver. This is distribution specific and must be overwritten in the child implementation.""" @@ -184,61 +196,109 @@ """ rdma_dev = '/dev/hvnd_rdma' + sriov_dir = '/sys/class/infiniband' device_check_timeout_sec = 120 device_check_interval_sec = 1 + ipoib_check_timeout_sec = 60 + ipoib_check_interval_sec = 1 ipv4_addr = None mac_adr = None + nd_version = None - def __init__(self, ipv4_addr, mac_addr): + def __init__(self, ipv4_addr, mac_addr, nd_version): self.ipv4_addr = ipv4_addr self.mac_addr = mac_addr + self.nd_version = nd_version def start(self): - """ - Start a thread in the background to process the RDMA tasks and returns. - """ - logger.info("RDMA: starting device processing in the background.") - threading.Thread(target=self.process).start() + logger.info("RDMA: starting device processing.") + self.process() + logger.info("RDMA: completed device processing.") def process(self): try: - RDMADeviceHandler.update_dat_conf(dapl_config_paths, self.ipv4_addr) + if not self.nd_version : + logger.info("RDMA: provisioning SRIOV RDMA device.") + self.provision_sriov_rdma() + else : + logger.info("RDMA: provisioning Network Direct RDMA device.") + self.provision_network_direct_rdma() + except Exception as e: + logger.error("RDMA: device processing failed: {0}".format(e)) - skip_rdma_device = False - module_name = "hv_network_direct" - retcode,out = shellutil.run_get_output("modprobe -R %s" % module_name, chk_err=False) - if retcode == 0: - module_name = out.strip() - else: - logger.info("RDMA: failed to resolve module name. Use original name") - retcode,out = shellutil.run_get_output("modprobe %s" % module_name) - if retcode != 0: - logger.error("RDMA: failed to load module %s" % module_name) - return - retcode,out = shellutil.run_get_output("modinfo %s" % module_name) - if retcode == 0: - version = re.search("version:\s+(\d+)\.(\d+)\.(\d+)\D", out, re.IGNORECASE) - if version: - v1 = int(version.groups(0)[0]) - v2 = int(version.groups(0)[1]) - if v1>4 or v1==4 and v2>0: - logger.info("Skip setting /dev/hvnd_rdma on 4.1 or later") - skip_rdma_device = True - else: - logger.info("RDMA: hv_network_direct driver version not present, assuming 4.0.x or older.") + def provision_network_direct_rdma(self) : + RDMADeviceHandler.update_dat_conf(dapl_config_paths, self.ipv4_addr) + + if not conf.enable_check_rdma_driver(): + logger.info("RDMA: skip checking RDMA driver version") + RDMADeviceHandler.update_network_interface(self.mac_addr, self.ipv4_addr) + return + + skip_rdma_device = False + module_name = "hv_network_direct" + retcode,out = shellutil.run_get_output("modprobe -R %s" % module_name, chk_err=False) + if retcode == 0: + module_name = out.strip() + else: + logger.info("RDMA: failed to resolve module name. Use original name") + retcode,out = shellutil.run_get_output("modprobe %s" % module_name) + if retcode != 0: + logger.error("RDMA: failed to load module %s" % module_name) + return + retcode,out = shellutil.run_get_output("modinfo %s" % module_name) + if retcode == 0: + version = re.search("version:\s+(\d+)\.(\d+)\.(\d+)\D", out, re.IGNORECASE) + if version: + v1 = int(version.groups(0)[0]) + v2 = int(version.groups(0)[1]) + if v1>4 or v1==4 and v2>0: + logger.info("Skip setting /dev/hvnd_rdma on 4.1 or later") + skip_rdma_device = True else: - logger.warn("RDMA: failed to get module info on hv_network_direct.") + logger.info("RDMA: hv_network_direct driver version not present, assuming 4.0.x or older.") + else: + logger.warn("RDMA: failed to get module info on hv_network_direct.") + + if not skip_rdma_device: + RDMADeviceHandler.wait_rdma_device( + self.rdma_dev, self.device_check_timeout_sec, self.device_check_interval_sec) + RDMADeviceHandler.write_rdma_config_to_device( + self.rdma_dev, self.ipv4_addr, self.mac_addr) + + RDMADeviceHandler.update_network_interface(self.mac_addr, self.ipv4_addr) + + def provision_sriov_rdma(self) : + RDMADeviceHandler.wait_any_rdma_device( + self.sriov_dir, self.device_check_timeout_sec, self.device_check_interval_sec) + RDMADeviceHandler.update_iboip_interface(self.ipv4_addr, self.ipoib_check_timeout_sec, self.ipoib_check_interval_sec) + return - if not skip_rdma_device: - RDMADeviceHandler.wait_rdma_device( - self.rdma_dev, self.device_check_timeout_sec, self.device_check_interval_sec) - RDMADeviceHandler.write_rdma_config_to_device( - self.rdma_dev, self.ipv4_addr, self.mac_addr) + @staticmethod + def update_iboip_interface(ipv4_addr, timeout_sec, check_interval_sec) : + logger.info("Wait for ib0 become available") + total_retries = timeout_sec/check_interval_sec + n = 0 + found_ib0 = None + while not found_ib0 and n < total_retries: + ret, output = shellutil.run_get_output("ifconfig -a") + if ret != 0: + raise Exception("Failed to list network interfaces") + found_ib0 = re.search("ib0", output, re.IGNORECASE) + if found_ib0: + break + time.sleep(check_interval_sec) + n += 1 - RDMADeviceHandler.update_network_interface(self.mac_addr, self.ipv4_addr) - except Exception as e: - logger.error("RDMA: device processing failed: {0}".format(e)) + if not found_ib0: + raise Exception("ib0 is not available") + + netmask = 16 + logger.info("RDMA: configuring IPv4 addr and netmask on ipoib interface") + addr = '{0}/{1}'.format(ipv4_addr, netmask) + if shellutil.run("ifconfig ib0 {0}".format(addr)) != 0: + raise Exception("Could set addr to {0} on ib0".format(addr)) + logger.info("RDMA: ipoib address and netmask configured on interface") @staticmethod def update_dat_conf(paths, ipv4_addr): @@ -295,6 +355,26 @@ return logger.verbose( "RDMA: device not ready, sleep {0}s".format(check_interval_sec)) + time.sleep(check_interval_sec) + n += 1 + logger.error("RDMA device wait timed out") + raise Exception("The device did not show up in {0} seconds ({1} retries)".format( + timeout_sec, total_retries)) + + @staticmethod + def wait_any_rdma_device(dir, timeout_sec, check_interval_sec): + logger.info( + "RDMA: waiting for any Infiniband device at directory={0} timeout={1}s".format( + dir, timeout_sec)) + total_retries = timeout_sec/check_interval_sec + n = 0 + while n < total_retries: + r = os.listdir(dir) + if r: + logger.info("RDMA: device found in {0}".format(dir)) + return + logger.verbose( + "RDMA: device not ready, sleep {0}s".format(check_interval_sec)) time.sleep(check_interval_sec) n += 1 logger.error("RDMA device wait timed out") diff -Nru waagent-2.2.34/azurelinuxagent/common/telemetryevent.py waagent-2.2.45/azurelinuxagent/common/telemetryevent.py --- waagent-2.2.34/azurelinuxagent/common/telemetryevent.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/telemetryevent.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,45 @@ +# Microsoft Azure Linux Agent +# +# Copyright 2019 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# + +from azurelinuxagent.common.datacontract import DataContract, DataContractList + + +class TelemetryEventParam(DataContract): + def __init__(self, name=None, value=None): + self.name = name + self.value = value + + def __eq__(self, other): + return isinstance(other, TelemetryEventParam) and other.name == self.name and other.value == self.value + + +class TelemetryEvent(DataContract): + def __init__(self, eventId=None, providerId=None): + self.eventId = eventId + self.providerId = providerId + self.parameters = DataContractList(TelemetryEventParam) + + # Checking if the particular param name is in the TelemetryEvent. + def __contains__(self, param_name): + return param_name in [param.name for param in self.parameters] + + +class TelemetryEventList(DataContract): + def __init__(self): + self.events = DataContractList(TelemetryEvent) diff -Nru waagent-2.2.34/azurelinuxagent/common/utils/cryptutil.py waagent-2.2.45/azurelinuxagent/common/utils/cryptutil.py --- waagent-2.2.34/azurelinuxagent/common/utils/cryptutil.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/utils/cryptutil.py 2019-11-07 00:36:56.000000000 +0000 @@ -20,7 +20,6 @@ import base64 import errno import struct -import sys import os.path import subprocess @@ -29,10 +28,11 @@ import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.shellutil as shellutil -import azurelinuxagent.common.utils.textutil as textutil + DECRYPT_SECRET_CMD = "{0} cms -decrypt -inform DER -inkey {1} -in /dev/stdin" + class CryptUtil(object): def __init__(self, openssl_cmd): self.openssl_cmd = openssl_cmd @@ -46,34 +46,30 @@ "-out {2}").format(self.openssl_cmd, prv_file, crt_file) rc = shellutil.run(cmd) if rc != 0: - logger.error("Failed to create {0} and {1} certificates".format( - prv_file, crt_file)) + logger.error("Failed to create {0} and {1} certificates".format(prv_file, crt_file)) def get_pubkey_from_prv(self, file_name): if not os.path.exists(file_name): raise IOError(errno.ENOENT, "File not found", file_name) else: - cmd = "{0} rsa -in {1} -pubout 2>/dev/null".format(self.openssl_cmd, - file_name) - pub = shellutil.run_get_output(cmd)[1] + cmd = [self.openssl_cmd, "rsa", "-in", file_name, "-pubout"] + pub = shellutil.run_command(cmd, log_error=True) return pub def get_pubkey_from_crt(self, file_name): if not os.path.exists(file_name): raise IOError(errno.ENOENT, "File not found", file_name) else: - cmd = "{0} x509 -in {1} -pubkey -noout".format(self.openssl_cmd, - file_name) - pub = shellutil.run_get_output(cmd)[1] + cmd = [self.openssl_cmd, "x509", "-in", file_name, "-pubkey", "-noout"] + pub = shellutil.run_command(cmd, log_error=True) return pub def get_thumbprint_from_crt(self, file_name): if not os.path.exists(file_name): raise IOError(errno.ENOENT, "File not found", file_name) else: - cmd = "{0} x509 -in {1} -fingerprint -noout".format(self.openssl_cmd, - file_name) - thumbprint = shellutil.run_get_output(cmd)[1] + cmd = [self.openssl_cmd, "x509", "-in", file_name, "-fingerprint", "-noout"] + thumbprint = shellutil.run_command(cmd) thumbprint = thumbprint.rstrip().split('=')[1].replace(':', '').upper() return thumbprint @@ -117,7 +113,7 @@ keydata.extend(b"\0") keydata.extend(self.num_to_bytes(n)) keydata_base64 = base64.b64encode(bytebuffer(keydata)) - return ustr(b"ssh-rsa " + keydata_base64 + b"\n", + return ustr(b"ssh-rsa " + keydata_base64 + b"\n", encoding='utf-8') except ImportError as e: raise CryptError("Failed to load pyasn1.codec.der") diff -Nru waagent-2.2.34/azurelinuxagent/common/utils/extensionprocessutil.py waagent-2.2.45/azurelinuxagent/common/utils/extensionprocessutil.py --- waagent-2.2.34/azurelinuxagent/common/utils/extensionprocessutil.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/utils/extensionprocessutil.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,141 @@ +# Microsoft Azure Linux Agent +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# + +from azurelinuxagent.common.exception import ExtensionErrorCodes, ExtensionOperationError, ExtensionError +from azurelinuxagent.common.future import ustr +import os +import signal +import time + +TELEMETRY_MESSAGE_MAX_LEN = 3200 + + +def wait_for_process_completion_or_timeout(process, timeout): + """ + Utility function that waits for the process to complete within the given time frame. This function will terminate + the process if when the given time frame elapses. + :param process: Reference to a running process + :param timeout: Number of seconds to wait for the process to complete before killing it + :return: Two parameters: boolean for if the process timed out and the return code of the process (None if timed out) + """ + while timeout > 0 and process.poll() is None: + time.sleep(1) + timeout -= 1 + + return_code = None + + if timeout == 0: + os.killpg(os.getpgid(process.pid), signal.SIGKILL) + else: + # process completed or forked; sleep 1 sec to give the child process (if any) a chance to start + time.sleep(1) + return_code = process.wait() + + return timeout == 0, return_code + + +def handle_process_completion(process, command, timeout, stdout, stderr, error_code): + """ + Utility function that waits for process completion and retrieves its output (stdout and stderr) if it completed + before the timeout period. Otherwise, the process will get killed and an ExtensionError will be raised. + In case the return code is non-zero, ExtensionError will be raised. + :param process: Reference to a running process + :param command: The extension command to run + :param timeout: Number of seconds to wait before killing the process + :param stdout: Must be a file since we seek on it when parsing the subprocess output + :param stderr: Must be a file since we seek on it when parsing the subprocess outputs + :param error_code: The error code to set if we raise an ExtensionError + :return: + """ + # Wait for process completion or timeout + timed_out, return_code = wait_for_process_completion_or_timeout(process, timeout) + process_output = read_output(stdout, stderr) + + if timed_out: + raise ExtensionError("Timeout({0}): {1}\n{2}".format(timeout, command, process_output), + code=ExtensionErrorCodes.PluginHandlerScriptTimedout) + + if return_code != 0: + raise ExtensionOperationError("Non-zero exit code: {0}, {1}\n{2}".format(return_code, command, process_output), + code=error_code, exit_code=return_code) + + return process_output + + +def read_output(stdout, stderr): + """ + Read the output of the process sent to stdout and stderr and trim them to the max appropriate length. + :param stdout: File containing the stdout of the process + :param stderr: File containing the stderr of the process + :return: Returns the formatted concatenated stdout and stderr of the process + """ + try: + stdout.seek(0) + stderr.seek(0) + + stdout = ustr(stdout.read(TELEMETRY_MESSAGE_MAX_LEN), encoding='utf-8', + errors='backslashreplace') + stderr = ustr(stderr.read(TELEMETRY_MESSAGE_MAX_LEN), encoding='utf-8', + errors='backslashreplace') + + return format_stdout_stderr(stdout, stderr) + except Exception as e: + return format_stdout_stderr("", "Cannot read stdout/stderr: {0}".format(ustr(e))) + + +def format_stdout_stderr(stdout, stderr, max_len=TELEMETRY_MESSAGE_MAX_LEN): + """ + Format stdout and stderr's output to make it suitable in telemetry. + The goal is to maximize the amount of output given the constraints + of telemetry. + + For example, if there is more stderr output than stdout output give + more buffer space to stderr. + + :param str stdout: characters captured from stdout + :param str stderr: characters captured from stderr + :param int max_len: maximum length of the string to return + + :return: a string formatted with stdout and stderr that is less than + or equal to max_len. + :rtype: str + """ + template = "[stdout]\n{0}\n\n[stderr]\n{1}" + # +6 == len("{0}") + len("{1}") + max_len_each = int((max_len - len(template) + 6) / 2) + + if max_len_each <= 0: + return '' + + def to_s(captured_stdout, stdout_offset, captured_stderr, stderr_offset): + s = template.format(captured_stdout[stdout_offset:], captured_stderr[stderr_offset:]) + return s + + if len(stdout) + len(stderr) < max_len: + return to_s(stdout, 0, stderr, 0) + elif len(stdout) < max_len_each: + bonus = max_len_each - len(stdout) + stderr_len = min(max_len_each + bonus, len(stderr)) + return to_s(stdout, 0, stderr, -1*stderr_len) + elif len(stderr) < max_len_each: + bonus = max_len_each - len(stderr) + stdout_len = min(max_len_each + bonus, len(stdout)) + return to_s(stdout, -1*stdout_len, stderr, 0) + else: + return to_s(stdout, -1*max_len_each, stderr, -1*max_len_each) diff -Nru waagent-2.2.34/azurelinuxagent/common/utils/processutil.py waagent-2.2.45/azurelinuxagent/common/utils/processutil.py --- waagent-2.2.34/azurelinuxagent/common/utils/processutil.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/utils/processutil.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,207 +0,0 @@ -# Microsoft Azure Linux Agent -# -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the Apache License, Version 2.0 (the "License"); -# -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Requires Python 2.6+ and Openssl 1.0+ -# -import multiprocessing -import subprocess -import sys -import os -import time -import signal -from errno import ESRCH -from multiprocessing import Process - -import azurelinuxagent.common.logger as logger -from azurelinuxagent.common.exception import ExtensionError -from azurelinuxagent.common.future import ustr - -TELEMETRY_MESSAGE_MAX_LEN = 3200 - - -def sanitize(s): - return ustr(s, encoding='utf-8', errors='backslashreplace') - - -def format_stdout_stderr(stdout, stderr, max_len=TELEMETRY_MESSAGE_MAX_LEN): - """ - Format stdout and stderr's output to make it suitable in telemetry. - The goal is to maximize the amount of output given the constraints - of telemetry. - - For example, if there is more stderr output than stdout output give - more buffer space to stderr. - - :param str stdout: characters captured from stdout - :param str stderr: characters captured from stderr - :param int max_len: maximum length of the string to return - - :return: a string formatted with stdout and stderr that is less than - or equal to max_len. - :rtype: str - """ - template = "[stdout]\n{0}\n\n[stderr]\n{1}" - # +6 == len("{0}") + len("{1}") - max_len_each = int((max_len - len(template) + 6) / 2) - - if max_len_each <= 0: - return '' - - def to_s(captured_stdout, stdout_offset, captured_stderr, stderr_offset): - s = template.format(captured_stdout[stdout_offset:], captured_stderr[stderr_offset:]) - return s - - if len(stdout) + len(stderr) < max_len: - return to_s(stdout, 0, stderr, 0) - elif len(stdout) < max_len_each: - bonus = max_len_each - len(stdout) - stderr_len = min(max_len_each + bonus, len(stderr)) - return to_s(stdout, 0, stderr, -1*stderr_len) - elif len(stderr) < max_len_each: - bonus = max_len_each - len(stderr) - stdout_len = min(max_len_each + bonus, len(stdout)) - return to_s(stdout, -1*stdout_len, stderr, 0) - else: - return to_s(stdout, -1*max_len_each, stderr, -1*max_len_each) - - -def _destroy_process(process, signal_to_send=signal.SIGKILL): - """ - Completely destroy the target process. Close the stdout/stderr pipes, kill the process, reap the zombie. - If process is the leader of a process group, kill the entire process group. - - :param Popen process: Process to be sent a signal - :param int signal_to_send: Signal number to be sent - """ - process.stdout.close() - process.stderr.close() - try: - pid = process.pid - if os.getpgid(pid) == pid: - os.killpg(pid, signal_to_send) - else: - os.kill(pid, signal_to_send) - process.wait() - except OSError as e: - if e.errno != ESRCH: - raise - pass # If the process is already gone, that's fine - - -def capture_from_process_poll(process, cmd, timeout, code): - """ - Capture output from the process if it does not fork, or forks - and completes quickly. - """ - retry = timeout - while retry > 0 and process.poll() is None: - time.sleep(1) - retry -= 1 - - # process did not fork, timeout expired - if retry == 0: - os.killpg(os.getpgid(process.pid), signal.SIGKILL) - stdout, stderr = process.communicate() - msg = format_stdout_stderr(sanitize(stdout), sanitize(stderr)) - raise ExtensionError("Timeout({0}): {1}\n{2}".format(timeout, cmd, msg), code=code) - - # process completed or forked - return_code = process.wait() - if return_code != 0: - raise ExtensionError("Non-zero exit code: {0}, {1}".format(return_code, cmd), code=code) - - stderr = b'' - stdout = b'cannot collect stdout' - - # attempt non-blocking process communication to capture output - def proc_comm(_process, _return): - try: - _stdout, _stderr = _process.communicate() - _return[0] = _stdout - _return[1] = _stderr - except Exception: - pass - - try: - mgr = multiprocessing.Manager() - ret_dict = mgr.dict() - - cproc = Process(target=proc_comm, args=(process, ret_dict)) - cproc.start() - - # allow 1s to capture output - cproc.join(1) - - if len(ret_dict) == 2: - stdout = ret_dict[0] - stderr = ret_dict[1] - - except Exception: - pass - - return stdout, stderr - - -def capture_from_process_no_timeout(process, cmd, code): - try: - stdout, stderr = process.communicate() - except OSError as e: - _destroy_process(process, signal.SIGKILL) - raise ExtensionError("Error while running '{0}': {1}".format(cmd, e.strerror), code=code) - except Exception as e: - _destroy_process(process, signal.SIGKILL) - raise ExtensionError("Exception while running '{0}': {1}".format(cmd, e), code=code) - - return stdout, stderr - - -def capture_from_process_raw(process, cmd, timeout, code): - """ - Captures stdout and stderr from an already-created process. - - :param subprocess.Popen process: Created by subprocess.Popen() - :param str cmd: The command string to be included in any exceptions - :param int timeout: Number of seconds the process is permitted to run - :return: The stdout and stderr captured from the process - :rtype: (str, str) - :raises ExtensionError: if a timeout occurred or if anything was raised by Popen.communicate() - """ - if not timeout: - stdout, stderr = capture_from_process_no_timeout(process, cmd, code) - else: - if os.getpgid(process.pid) != process.pid: - _destroy_process(process, signal.SIGKILL) - raise ExtensionError("Subprocess was not root of its own process group", code=code) - - stdout, stderr = capture_from_process_poll(process, cmd, timeout, code) - - return stdout, stderr - - -def capture_from_process(process, cmd, timeout=0, code=-1): - """ - Captures stdout and stderr from an already-created process. The output is "cooked" - into a string of reasonable length. - - :param subprocess.Popen process: Created by subprocess.Popen() - :param str cmd: The command string to be included in any exceptions - :param int timeout: Number of seconds the process is permitted to run - :return: The stdout and stderr captured from the process - :rtype: (str, str) - :raises ExtensionError: if a timeout occurred or if anything was raised by Popen.communicate() - """ - stdout, stderr = capture_from_process_raw(process, cmd, timeout, code) - return format_stdout_stderr(sanitize(stdout), sanitize(stderr)) diff -Nru waagent-2.2.34/azurelinuxagent/common/utils/restutil.py waagent-2.2.45/azurelinuxagent/common/utils/restutil.py --- waagent-2.2.34/azurelinuxagent/common/utils/restutil.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/utils/restutil.py 2019-11-07 00:36:56.000000000 +0000 @@ -22,12 +22,14 @@ import threading import time import traceback +import socket +import struct import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.textutil as textutil -from azurelinuxagent.common.exception import HttpError, ResourceGoneError +from azurelinuxagent.common.exception import HttpError, ResourceGoneError, InvalidContainerError from azurelinuxagent.common.future import httpclient, urlparse, ustr from azurelinuxagent.common.version import PY_VERSION_MAJOR, AGENT_NAME, GOAL_STATE_AGENT_VERSION @@ -65,6 +67,10 @@ httpclient.ACCEPTED ] +NOT_MODIFIED_CODES = [ + httpclient.NOT_MODIFIED +] + HOSTPLUGIN_UPSTREAM_FAILURE_CODES = [ 502 ] @@ -82,11 +88,15 @@ httpclient.BadStatusLine ] +# http://www.gnu.org/software/wget/manual/html_node/Proxies.html HTTP_PROXY_ENV = "http_proxy" HTTPS_PROXY_ENV = "https_proxy" +NO_PROXY_ENV = "no_proxy" + HTTP_USER_AGENT = "{0}/{1}".format(AGENT_NAME, GOAL_STATE_AGENT_VERSION) HTTP_USER_AGENT_HEALTH = "{0}+health".format(HTTP_USER_AGENT) INVALID_CONTAINER_CONFIGURATION = "InvalidContainerConfiguration" +REQUEST_ROLE_CONFIG_FILE_NOT_FOUND = "RequestRoleConfigFileNotFound" DEFAULT_PROTOCOL_ENDPOINT = '168.63.129.16' HOST_PLUGIN_PORT = 32526 @@ -144,15 +154,11 @@ return status in THROTTLE_CODES -def _is_invalid_container_configuration(response): - result = False - if response is not None and response.status == httpclient.BAD_REQUEST: - error_detail = read_response_error(response) - result = INVALID_CONTAINER_CONFIGURATION in error_detail - return result - - def _parse_url(url): + """ + Parse URL to get the components of the URL broken down to host, port + :rtype: string, int, bool, string + """ o = urlparse(url) rel_uri = o.path if o.fragment: @@ -165,6 +171,95 @@ return o.hostname, o.port, secure, rel_uri +def is_valid_cidr(string_network): + """ + Very simple check of the cidr format in no_proxy variable. + :rtype: bool + """ + if string_network.count('/') == 1: + try: + mask = int(string_network.split('/')[1]) + except ValueError: + return False + + if mask < 1 or mask > 32: + return False + + try: + socket.inet_aton(string_network.split('/')[0]) + except socket.error: + return False + else: + return False + return True + + +def dotted_netmask(mask): + """Converts mask from /xx format to xxx.xxx.xxx.xxx + Example: if mask is 24 function returns 255.255.255.0 + :rtype: str + """ + bits = 0xffffffff ^ (1 << 32 - mask) - 1 + return socket.inet_ntoa(struct.pack('>I', bits)) + + +def address_in_network(ip, net): + """This function allows you to check if an IP belongs to a network subnet + Example: returns True if ip = 192.168.1.1 and net = 192.168.1.0/24 + returns False if ip = 192.168.1.1 and net = 192.168.100.0/24 + :rtype: bool + """ + ipaddr = struct.unpack('=L', socket.inet_aton(ip))[0] + netaddr, bits = net.split('/') + netmask = struct.unpack('=L', socket.inet_aton(dotted_netmask(int(bits))))[0] + network = struct.unpack('=L', socket.inet_aton(netaddr))[0] & netmask + return (ipaddr & netmask) == (network & netmask) + + +def is_ipv4_address(string_ip): + """ + :rtype: bool + """ + try: + socket.inet_aton(string_ip) + except socket.error: + return False + return True + + +def get_no_proxy(): + no_proxy = os.environ.get(NO_PROXY_ENV) or os.environ.get(NO_PROXY_ENV.upper()) + + if no_proxy: + no_proxy = [host for host in no_proxy.replace(' ', '').split(',') if host] + + # no_proxy in the proxies argument takes precedence + return no_proxy + + +def bypass_proxy(host): + no_proxy = get_no_proxy() + + if no_proxy: + if is_ipv4_address(host): + for proxy_ip in no_proxy: + if is_valid_cidr(proxy_ip): + if address_in_network(host, proxy_ip): + return True + elif host == proxy_ip: + # If no_proxy ip was defined in plain IP notation instead of cidr notation & + # matches the IP of the index + return True + else: + for proxy_domain in no_proxy: + if host.lower().endswith(proxy_domain.lower()): + # The URL does match something in no_proxy, so we don't want + # to apply the proxies on this URL. + return True + + return False + + def _get_http_proxy(secure=False): # Prefer the configuration settings over environment variables host = conf.get_httpproxy_host() @@ -247,7 +342,7 @@ # Use the HTTP(S) proxy proxy_host, proxy_port = (None, None) - if use_proxy: + if use_proxy and not bypass_proxy(host): proxy_host, proxy_port = _get_http_proxy(secure=secure) if proxy_host or proxy_port: @@ -329,15 +424,18 @@ max_retry = max(max_retry, THROTTLE_RETRIES) continue + # If we got a 410 (resource gone) for any reason, raise an exception. The caller will handle it by + # forcing a goal state refresh and retrying the call. if resp.status in RESOURCE_GONE_CODES: - raise ResourceGoneError() + response_error = read_response_error(resp) + raise ResourceGoneError(response_error) - # Map invalid container configuration errors to resource gone in - # order to force a goal state refresh, which in turn updates the - # container-id header passed to HostGAPlugin. - # See #1294. - if _is_invalid_container_configuration(resp): - raise ResourceGoneError() + # If we got a 400 (bad request) because the container id is invalid, it could indicate a stale goal + # state. The caller will handle this exception by forcing a goal state refresh and retrying the call. + if resp.status == httpclient.BAD_REQUEST: + response_error = read_response_error(resp) + if INVALID_CONTAINER_CONFIGURATION in response_error: + raise InvalidContainerError(response_error) return resp @@ -442,6 +540,10 @@ return resp is not None and resp.status in ok_codes +def request_not_modified(resp): + return resp is not None and resp.status in NOT_MODIFIED_CODES + + def request_failed_at_hostplugin(resp, upstream_failure_codes=HOSTPLUGIN_UPSTREAM_FAILURE_CODES): """ Host plugin will return 502 for any upstream issue, so a failure is any 5xx except 502 diff -Nru waagent-2.2.34/azurelinuxagent/common/utils/shellutil.py waagent-2.2.45/azurelinuxagent/common/utils/shellutil.py --- waagent-2.2.34/azurelinuxagent/common/utils/shellutil.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/utils/shellutil.py 2019-11-07 00:36:56.000000000 +0000 @@ -63,22 +63,25 @@ """ return not run(cmd, False) -def run(cmd, chk_err=True): +def run(cmd, chk_err=True, expected_errors=[]): """ Calls run_get_output on 'cmd', returning only the return code. If chk_err=True then errors will be reported in the log. If chk_err=False then errors will be suppressed from the log. """ - retcode, out = run_get_output(cmd, chk_err) + retcode, out = run_get_output(cmd, chk_err=chk_err, expected_errors=expected_errors) return retcode -def run_get_output(cmd, chk_err=True, log_cmd=True): +def run_get_output(cmd, chk_err=True, log_cmd=True, expected_errors=[]): """ Wrapper for subprocess.check_output. Execute 'cmd'. Returns return code and STDOUT, trapping expected exceptions. Reports exceptions to Error if chk_err parameter is True + + For new callers, consider using run_command instead as it separates stdout from stderr, + returns only stdout on success, logs both outputs and return code on error and raises an exception. """ if log_cmd: logger.verbose(u"Command: [{0}]", cmd) @@ -86,18 +89,18 @@ output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) - output = ustr(output, - encoding='utf-8', - errors="backslashreplace") + output = _encode_command_output(output) except subprocess.CalledProcessError as e: - output = ustr(e.output, - encoding='utf-8', - errors="backslashreplace") + output = _encode_command_output(e.output) + if chk_err: msg = u"Command: [{0}], " \ u"return code: [{1}], " \ u"result: [{2}]".format(cmd, e.returncode, output) - logger.error(msg) + if e.returncode in expected_errors: + logger.info(msg) + else: + logger.error(msg) return e.returncode, output except Exception as e: if chk_err: @@ -107,6 +110,60 @@ return 0, output +def _encode_command_output(output): + return ustr(output, encoding='utf-8', errors="backslashreplace") + + +class CommandError(Exception): + """ + Exception raised by run_command when the command returns an error + """ + @staticmethod + def _get_message(command, returncode): + command_name = command[0] if isinstance(command, list) and len(command) > 0 else command + return "'{0}' failed: {1}".format(command_name, returncode) + + def __init__(self, command, returncode, stdout, stderr): + super(Exception, self).__init__(CommandError._get_message(command, returncode)) + self.command = command + self.returncode = returncode + self.stdout = stdout + self.stderr = stderr + + +def run_command(command, log_error=False): + """ + Executes the given command and returns its stdout as a string. + If there are any errors executing the command it logs details about the failure and raises a RunCommandException; + if 'log_error' is True, it also logs details about the error. + """ + def format_command(cmd): + return " ".join(cmd) if isinstance(cmd, list) else command + + try: + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) + stdout, stderr = process.communicate() + returncode = process.returncode + except Exception as e: + if log_error: + logger.error(u"Command [{0}] raised unexpected exception: [{1}]", format_command(command), ustr(e)) + raise + + if returncode != 0: + encoded_stdout = _encode_command_output(stdout) + encoded_stderr = _encode_command_output(stderr) + if log_error: + logger.error( + "Command: [{0}], return code: [{1}], stdout: [{2}] stderr: [{3}]", + format_command(command), + returncode, + encoded_stdout, + encoded_stderr) + raise CommandError(command=command, returncode=returncode, stdout=encoded_stdout, stderr=encoded_stderr) + + return _encode_command_output(stdout) + + def quote(word_list): """ Quote a list or tuple of strings for Unix Shell as words, using the diff -Nru waagent-2.2.34/azurelinuxagent/common/utils/textutil.py waagent-2.2.45/azurelinuxagent/common/utils/textutil.py --- waagent-2.2.34/azurelinuxagent/common/utils/textutil.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/utils/textutil.py 2019-11-07 00:36:56.000000000 +0000 @@ -383,3 +383,16 @@ for item in string_list: sha1_hash.update(item.encode()) return sha1_hash.digest() + + +def format_memory_value(unit, value): + units = {'bytes': 1, 'kilobytes': 1024, 'megabytes': 1024*1024, 'gigabytes': 1024*1024*1024} + + if unit not in units: + raise ValueError("Unit must be one of {0}".format(units.keys())) + try: + value = float(value) + except TypeError: + raise TypeError('Value must be convertible to a float') + + return int(value * units[unit]) diff -Nru waagent-2.2.34/azurelinuxagent/common/version.py waagent-2.2.45/azurelinuxagent/common/version.py --- waagent-2.2.34/azurelinuxagent/common/version.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/common/version.py 2019-11-07 00:36:56.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright 2018 Microsoft Corporation +# Copyright 2019 Microsoft Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -119,9 +119,9 @@ AGENT_NAME = "WALinuxAgent" AGENT_LONG_NAME = "Azure Linux Agent" -AGENT_VERSION = '2.2.34' +AGENT_VERSION = '2.2.45' AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION) -AGENT_DESCRIPTION = """test_upgrade_available_purges_old_agents +AGENT_DESCRIPTION = """ The Azure Linux Agent supports the provisioning and running of Linux VMs in the Azure cloud. This package should be installed on Linux disk images that are built to run in the Azure environment. @@ -135,6 +135,9 @@ AGENT_PKG_PATTERN = re.compile(AGENT_PATTERN+"\.zip") AGENT_DIR_PATTERN = re.compile(".*/{0}".format(AGENT_PATTERN)) +# The execution mode of the VM - IAAS or PAAS. Linux VMs are only executed in IAAS mode. +AGENT_EXECUTION_MODE = "IAAS" + EXT_HANDLER_PATTERN = b".*/WALinuxAgent-(\d+.\d+.\d+[.\d+]*).*-run-exthandlers" EXT_HANDLER_REGEX = re.compile(EXT_HANDLER_PATTERN) diff -Nru waagent-2.2.34/azurelinuxagent/daemon/main.py waagent-2.2.45/azurelinuxagent/daemon/main.py --- waagent-2.2.34/azurelinuxagent/daemon/main.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/daemon/main.py 2019-11-07 00:36:56.000000000 +0000 @@ -26,7 +26,7 @@ import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.fileutil as fileutil -from azurelinuxagent.common.cgroups import CGroups +from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator from azurelinuxagent.common.event import add_event, WALAEventOperation from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil @@ -68,7 +68,7 @@ self.check_pid() self.initialize_environment() - CGroups.setup() + CGroupConfigurator.get_instance().create_agent_cgroups(track_cgroups=False) # If FIPS is enabled, set the OpenSSL environment variable # Note: @@ -140,7 +140,8 @@ # Enable RDMA, continue in errors if conf.enable_rdma(): - self.rdma_handler.install_driver() + nd_version = self.rdma_handler.get_rdma_version() + self.rdma_handler.install_driver_if_needed() logger.info("RDMA capabilities are enabled in configuration") try: @@ -154,7 +155,7 @@ raise Exception("Attempt to setup RDMA without Wireserver") client.update_goal_state(forced=True) - setup_rdma_device() + setup_rdma_device(nd_version) except Exception as e: logger.error("Error setting up rdma device: %s" % e) else: diff -Nru waagent-2.2.34/azurelinuxagent/daemon/resourcedisk/default.py waagent-2.2.45/azurelinuxagent/daemon/resourcedisk/default.py --- waagent-2.2.34/azurelinuxagent/daemon/resourcedisk/default.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/daemon/resourcedisk/default.py 2019-11-07 00:36:56.000000000 +0000 @@ -17,6 +17,7 @@ import os import re +import stat import sys import threading from time import sleep @@ -124,12 +125,13 @@ force_option = 'F' if self.fs == 'xfs': force_option = 'f' - mkfs_string = "mkfs.{0} -{2} {1}".format(self.fs, partition, force_option) + mkfs_string = "mkfs.{0} -{2} {1}".format( + self.fs, partition, force_option) if "gpt" in ret[1]: logger.info("GPT detected, finding partitions") parts = [x for x in ret[1].split("\n") if - re.match("^\s*[0-9]+", x)] + re.match(r"^\s*[0-9]+", x)] logger.info("Found {0} GPT partition(s).", len(parts)) if len(parts) > 1: logger.info("Removing old GPT partitions") @@ -138,18 +140,23 @@ shellutil.run("parted {0} rm {1}".format(device, i)) logger.info("Creating new GPT partition") - shellutil.run("parted {0} mkpart primary 0% 100%".format(device)) + shellutil.run( + "parted {0} mkpart primary 0% 100%".format(device)) logger.info("Format partition [{0}]", mkfs_string) shellutil.run(mkfs_string) else: logger.info("GPT not detected, determining filesystem") - ret = self.change_partition_type(suppress_message=True, option_str="{0} 1 -n".format(device)) + ret = self.change_partition_type( + suppress_message=True, + option_str="{0} 1 -n".format(device)) ptype = ret[1].strip() if ptype == "7" and self.fs != "ntfs": logger.info("The partition is formatted with ntfs, updating " "partition type to 83") - self.change_partition_type(suppress_message=False, option_str="{0} 1 83".format(device)) + self.change_partition_type( + suppress_message=False, + option_str="{0} 1 83".format(device)) self.reread_partition_table(device) logger.info("Format partition [{0}]", mkfs_string) shellutil.run(mkfs_string) @@ -169,7 +176,8 @@ attempts -= 1 if not os.path.exists(partition): - raise ResourceDiskError("Partition was not created [{0}]".format(partition)) + raise ResourceDiskError( + "Partition was not created [{0}]".format(partition)) logger.info("Mount resource disk [{0}]", mount_string) ret, output = shellutil.run_get_output(mount_string, chk_err=False) @@ -187,7 +195,7 @@ self.reread_partition_table(device) - ret, output = shellutil.run_get_output(mount_string) + ret, output = shellutil.run_get_output(mount_string, chk_err=False) if ret: logger.warn("Failed to mount resource disk. " "Attempting to format and retry mount. [{0}]", @@ -215,14 +223,19 @@ """ command_to_use = '--part-type' - input = "sfdisk {0} {1} {2}".format(command_to_use, '-f' if suppress_message else '', option_str) - err_code, output = shellutil.run_get_output(input, chk_err=False, log_cmd=True) + input = "sfdisk {0} {1} {2}".format( + command_to_use, '-f' if suppress_message else '', option_str) + err_code, output = shellutil.run_get_output( + input, chk_err=False, log_cmd=True) # fall back to -c if err_code != 0: - logger.info("sfdisk with --part-type failed [{0}], retrying with -c", err_code) + logger.info( + "sfdisk with --part-type failed [{0}], retrying with -c", + err_code) command_to_use = '-c' - input = "sfdisk {0} {1} {2}".format(command_to_use, '-f' if suppress_message else '', option_str) + input = "sfdisk {0} {1} {2}".format( + command_to_use, '-f' if suppress_message else '', option_str) err_code, output = shellutil.run_get_output(input, log_cmd=True) if err_code == 0: @@ -245,16 +258,30 @@ else: return 'mount {0} {1}'.format(partition, mount_point) + @staticmethod + def check_existing_swap_file(swapfile, swaplist, size): + if swapfile in swaplist and os.path.isfile( + swapfile) and os.path.getsize(swapfile) == size: + logger.info("Swap already enabled") + # restrict access to owner (remove all access from group, others) + swapfile_mode = os.stat(swapfile).st_mode + if swapfile_mode & (stat.S_IRWXG | stat.S_IRWXO): + swapfile_mode = swapfile_mode & ~(stat.S_IRWXG | stat.S_IRWXO) + logger.info( + "Changing mode of {0} to {1:o}".format( + swapfile, swapfile_mode)) + os.chmod(swapfile, swapfile_mode) + return True + + return False + def create_swap_space(self, mount_point, size_mb): size_kb = size_mb * 1024 size = size_kb * 1024 swapfile = os.path.join(mount_point, 'swapfile') swaplist = shellutil.run_get_output("swapon -s")[1] - if swapfile in swaplist \ - and os.path.isfile(swapfile) \ - and os.path.getsize(swapfile) == size: - logger.info("Swap already enabled") + if self.check_existing_swap_file(swapfile, swaplist, size): return if os.path.isfile(swapfile) and os.path.getsize(swapfile) != size: @@ -296,7 +323,8 @@ os.remove(filename) # If file system is xfs, use dd right away as we have been reported that - # swap enabling fails in xfs fs when disk space is allocated with fallocate + # swap enabling fails in xfs fs when disk space is allocated with + # fallocate ret = 0 fn_sh = shellutil.quote((filename,)) if self.fs != 'xfs': @@ -305,13 +333,21 @@ # Probable errors: # - OSError: Seen on Cygwin, libc notimpl? # - AttributeError: What if someone runs this under... + fd = None + try: - with open(filename, 'w') as f: - os.posix_fallocate(f.fileno(), 0, nbytes) - return 0 - except: + fd = os.open( + filename, + os.O_CREAT | os.O_WRONLY | os.O_EXCL, + stat.S_IRUSR | stat.S_IWUSR) + os.posix_fallocate(fd, 0, nbytes) + return 0 + except BaseException: # Not confident with this thing, just keep trying... pass + finally: + if fd is not None: + os.close(fd) # fallocate command ret = shellutil.run( diff -Nru waagent-2.2.34/azurelinuxagent/daemon/resourcedisk/factory.py waagent-2.2.45/azurelinuxagent/daemon/resourcedisk/factory.py --- waagent-2.2.34/azurelinuxagent/daemon/resourcedisk/factory.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/daemon/resourcedisk/factory.py 2019-11-07 00:36:56.000000000 +0000 @@ -21,6 +21,7 @@ from .default import ResourceDiskHandler from .freebsd import FreeBSDResourceDiskHandler from .openbsd import OpenBSDResourceDiskHandler +from .openwrt import OpenWRTResourceDiskHandler from distutils.version import LooseVersion as Version @@ -34,5 +35,8 @@ if distro_name == "openbsd": return OpenBSDResourceDiskHandler() + if distro_name == "openwrt": + return OpenWRTResourceDiskHandler() + return ResourceDiskHandler() diff -Nru waagent-2.2.34/azurelinuxagent/daemon/resourcedisk/freebsd.py waagent-2.2.45/azurelinuxagent/daemon/resourcedisk/freebsd.py --- waagent-2.2.34/azurelinuxagent/daemon/resourcedisk/freebsd.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/daemon/resourcedisk/freebsd.py 2019-11-07 00:36:56.000000000 +0000 @@ -24,6 +24,7 @@ from azurelinuxagent.common.exception import ResourceDiskError from azurelinuxagent.daemon.resourcedisk.default import ResourceDiskHandler + class FreeBSDResourceDiskHandler(ResourceDiskHandler): """ This class handles resource disk mounting for FreeBSD. @@ -36,6 +37,7 @@ 1. MBR: The resource disk partition is /dev/da1s1 2. GPT: The resource disk partition is /dev/da1p2, /dev/da1p1 is for reserved usage. """ + def __init__(self): super(FreeBSDResourceDiskHandler, self).__init__() @@ -52,25 +54,30 @@ def mount_resource_disk(self, mount_point): fs = self.fs if fs != 'ufs': - raise ResourceDiskError("Unsupported filesystem type:{0}, only ufs is supported.".format(fs)) + raise ResourceDiskError( + "Unsupported filesystem type:{0}, only ufs is supported.".format(fs)) # 1. Detect device err, output = shellutil.run_get_output('gpart list') if err: - raise ResourceDiskError("Unable to detect resource disk device:{0}".format(output)) + raise ResourceDiskError( + "Unable to detect resource disk device:{0}".format(output)) disks = self.parse_gpart_list(output) device = self.osutil.device_for_ide_port(1) - if device is None or not device in disks: - # fallback logic to find device - err, output = shellutil.run_get_output('camcontrol periphlist 2:1:0') + if device is None or device not in disks: + # fallback logic to find device + err, output = shellutil.run_get_output( + 'camcontrol periphlist 2:1:0') if err: # try again on "3:1:0" - err, output = shellutil.run_get_output('camcontrol periphlist 3:1:0') + err, output = shellutil.run_get_output( + 'camcontrol periphlist 3:1:0') if err: - raise ResourceDiskError("Unable to detect resource disk device:{0}".format(output)) + raise ResourceDiskError( + "Unable to detect resource disk device:{0}".format(output)) - # 'da1: generation: 4 index: 1 status: MORE\npass2: generation: 4 index: 2 status: LAST\n' + # 'da1: generation: 4 index: 1 status: MORE\npass2: generation: 4 index: 2 status: LAST\n' for line in output.split('\n'): index = line.find(':') if index > 0: @@ -91,9 +98,11 @@ elif partition_table_type == 'GPT': provider_name = device + 'p2' else: - raise ResourceDiskError("Unsupported partition table type:{0}".format(output)) + raise ResourceDiskError( + "Unsupported partition table type:{0}".format(output)) - err, output = shellutil.run_get_output('gpart show -p {0}'.format(device)) + err, output = shellutil.run_get_output( + 'gpart show -p {0}'.format(device)) if err or output.find(provider_name) == -1: raise ResourceDiskError("Resource disk partition not found.") @@ -112,16 +121,26 @@ mount_cmd = 'mount -t {0} {1} {2}'.format(fs, partition, mount_point) err = shellutil.run(mount_cmd, chk_err=False) if err: - logger.info('Creating {0} filesystem on partition {1}'.format(fs, partition)) - err, output = shellutil.run_get_output('newfs -U {0}'.format(partition)) + logger.info( + 'Creating {0} filesystem on partition {1}'.format( + fs, partition)) + err, output = shellutil.run_get_output( + 'newfs -U {0}'.format(partition)) if err: - raise ResourceDiskError("Failed to create new filesystem on partition {0}, error:{1}" - .format(partition, output)) + raise ResourceDiskError( + "Failed to create new filesystem on partition {0}, error:{1}" .format( + partition, output)) err, output = shellutil.run_get_output(mount_cmd, chk_err=False) if err: - raise ResourceDiskError("Failed to mount partition {0}, error {1}".format(partition, output)) - - logger.info("Resource disk partition {0} is mounted at {1} with fstype {2}", partition, mount_point, fs) + raise ResourceDiskError( + "Failed to mount partition {0}, error {1}".format( + partition, output)) + + logger.info( + "Resource disk partition {0} is mounted at {1} with fstype {2}", + partition, + mount_point, + fs) return mount_point def create_swap_space(self, mount_point, size_mb): @@ -130,10 +149,7 @@ swapfile = os.path.join(mount_point, 'swapfile') swaplist = shellutil.run_get_output("swapctl -l")[1] - if swapfile in swaplist \ - and os.path.isfile(swapfile) \ - and os.path.getsize(swapfile) == size: - logger.info("Swap already enabled") + if self.check_existing_swap_file(swapfile, swaplist, size): return if os.path.isfile(swapfile) and os.path.getsize(swapfile) != size: @@ -144,20 +160,24 @@ if not os.path.isfile(swapfile): logger.info("Create swap file") self.mkfile(swapfile, size_kb * 1024) - - mddevice = shellutil.run_get_output("mdconfig -a -t vnode -f {0}".format(swapfile))[1].rstrip() + + mddevice = shellutil.run_get_output( + "mdconfig -a -t vnode -f {0}".format(swapfile))[1].rstrip() shellutil.run("chmod 0600 /dev/{0}".format(mddevice)) - + if conf.get_resourcedisk_enable_swap_encryption(): shellutil.run("kldload aesni") shellutil.run("kldload cryptodev") shellutil.run("kldload geom_eli") - shellutil.run("geli onetime -e AES-XTS -l 256 -d /dev/{0}".format(mddevice)) + shellutil.run( + "geli onetime -e AES-XTS -l 256 -d /dev/{0}".format(mddevice)) shellutil.run("chmod 0600 /dev/{0}.eli".format(mddevice)) if shellutil.run("swapon /dev/{0}.eli".format(mddevice)): raise ResourceDiskError("/dev/{0}.eli".format(mddevice)) - logger.info("Enabled {0}KB of swap at /dev/{1}.eli ({2})".format(size_kb, mddevice, swapfile)) + logger.info( + "Enabled {0}KB of swap at /dev/{1}.eli ({2})".format(size_kb, mddevice, swapfile)) else: if shellutil.run("swapon /dev/{0}".format(mddevice)): raise ResourceDiskError("/dev/{0}".format(mddevice)) - logger.info("Enabled {0}KB of swap at /dev/{1} ({2})".format(size_kb, mddevice, swapfile)) + logger.info( + "Enabled {0}KB of swap at /dev/{1} ({2})".format(size_kb, mddevice, swapfile)) diff -Nru waagent-2.2.34/azurelinuxagent/daemon/resourcedisk/openwrt.py waagent-2.2.45/azurelinuxagent/daemon/resourcedisk/openwrt.py --- waagent-2.2.34/azurelinuxagent/daemon/resourcedisk/openwrt.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/daemon/resourcedisk/openwrt.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,135 @@ +# Microsoft Azure Linux Agent +# +# Copyright 2018 Microsoft Corporation +# Copyright 2018 Sonus Networks, Inc. (d.b.a. Ribbon Communications Operating Company) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +import os +import errno as errno + +import azurelinuxagent.common.logger as logger +import azurelinuxagent.common.utils.fileutil as fileutil +import azurelinuxagent.common.utils.shellutil as shellutil +import azurelinuxagent.common.conf as conf +from azurelinuxagent.common.exception import ResourceDiskError +from azurelinuxagent.daemon.resourcedisk.default import ResourceDiskHandler + +class OpenWRTResourceDiskHandler(ResourceDiskHandler): + def __init__(self): + super(OpenWRTResourceDiskHandler, self).__init__() + # Fase File System (FFS) is UFS + if self.fs == 'ufs' or self.fs == 'ufs2': + self.fs = 'ffs' + + def reread_partition_table(self, device): + ret, output = shellutil.run_get_output("hdparm -z {0}".format(device), chk_err=False) + if ret != 0: + logger.warn("Failed refresh the partition table.") + + def mount_resource_disk(self, mount_point): + device = self.osutil.device_for_ide_port(1) + if device is None: + raise ResourceDiskError("unable to detect disk topology") + logger.info('Resource disk device {0} found.', device) + + # 2. Get partition + device = "/dev/{0}".format(device) + partition = device + "1" + logger.info('Resource disk partition {0} found.', partition) + + # 3. Mount partition + mount_list = shellutil.run_get_output("mount")[1] + existing = self.osutil.get_mount_point(mount_list, device) + if existing: + logger.info("Resource disk [{0}] is already mounted [{1}]", + partition, + existing) + return existing + + try: + fileutil.mkdir(mount_point, mode=0o755) + except OSError as ose: + msg = "Failed to create mount point " \ + "directory [{0}]: {1}".format(mount_point, ose) + logger.error(msg) + raise ResourceDiskError(msg=msg, inner=ose) + + force_option = 'F' + if self.fs == 'xfs': + force_option = 'f' + mkfs_string = "mkfs.{0} -{2} {1}".format(self.fs, partition, force_option) + + # Compare to the Default mount_resource_disk, we don't check for GPT that is not supported on OpenWRT + ret = self.change_partition_type(suppress_message=True, option_str="{0} 1 -n".format(device)) + ptype = ret[1].strip() + if ptype == "7" and self.fs != "ntfs": + logger.info("The partition is formatted with ntfs, updating " + "partition type to 83") + self.change_partition_type(suppress_message=False, option_str="{0} 1 83".format(device)) + self.reread_partition_table(device) + logger.info("Format partition [{0}]", mkfs_string) + shellutil.run(mkfs_string) + else: + logger.info("The partition type is {0}", ptype) + + mount_options = conf.get_resourcedisk_mountoptions() + mount_string = self.get_mount_string(mount_options, + partition, + mount_point) + attempts = 5 + while not os.path.exists(partition) and attempts > 0: + logger.info("Waiting for partition [{0}], {1} attempts remaining", + partition, + attempts) + sleep(5) + attempts -= 1 + + if not os.path.exists(partition): + raise ResourceDiskError("Partition was not created [{0}]".format(partition)) + + if os.path.ismount(mount_point): + logger.warn("Disk is already mounted on {0}", mount_point) + else: + # Some kernels seem to issue an async partition re-read after a + # command invocation. This causes mount to fail if the + # partition re-read is not complete by the time mount is + # attempted. Seen in CentOS 7.2. Force a sequential re-read of + # the partition and try mounting. + logger.info("Mounting after re-reading partition info.") + + self.reread_partition_table(device) + + logger.info("Mount resource disk [{0}]", mount_string) + ret, output = shellutil.run_get_output(mount_string) + if ret: + logger.warn("Failed to mount resource disk. " + "Attempting to format and retry mount. [{0}]", + output) + + shellutil.run(mkfs_string) + ret, output = shellutil.run_get_output(mount_string) + if ret: + raise ResourceDiskError("Could not mount {0} " + "after syncing partition table: " + "[{1}] {2}".format(partition, + ret, + output)) + + logger.info("Resource disk {0} is mounted at {1} with {2}", + device, + mount_point, + self.fs) + return mount_point diff -Nru waagent-2.2.34/azurelinuxagent/ga/env.py waagent-2.2.45/azurelinuxagent/ga/env.py --- waagent-2.2.34/azurelinuxagent/ga/env.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/ga/env.py 2019-11-07 00:36:56.000000000 +0000 @@ -22,9 +22,6 @@ import socket import time import threading - -import operator - import datetime import azurelinuxagent.common.conf as conf @@ -32,10 +29,9 @@ from azurelinuxagent.common.dhcp import get_dhcp_handler from azurelinuxagent.common.event import add_periodic, WALAEventOperation +from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.protocol import get_protocol_util -from azurelinuxagent.common.protocol.wire import INCARNATION_FILE_NAME -from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common.utils.archive import StateArchiver from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION @@ -68,7 +64,7 @@ self.protocol_util = get_protocol_util() self.stopped = True self.hostname = None - self.dhcp_id = None + self.dhcp_id_list = [] self.server_thread = None self.dhcp_warning_enabled = True self.last_archive = None @@ -83,7 +79,7 @@ logger.info("Start env monitor service.") self.dhcp_handler.conf_routes() self.hostname = self.osutil.get_hostname_record() - self.dhcp_id = self.osutil.get_dhcp_pid() + self.dhcp_id_list = self.get_dhcp_client_pid() self.start() def is_alive(self): @@ -92,6 +88,7 @@ def start(self): self.server_thread = threading.Thread(target=self.monitor) self.server_thread.setDaemon(True) + self.server_thread.setName("EnvHandler") self.server_thread.start() def monitor(self): @@ -107,7 +104,6 @@ self.osutil.remove_rules_files() if conf.enable_firewall(): - # If the rules ever change we must reset all rules and start over again. # # There was a rule change at 2.2.26, which started dropping non-root traffic @@ -117,9 +113,8 @@ self.osutil.remove_firewall(dst_ip=protocol.endpoint, uid=os.getuid()) reset_firewall_fules = True - success = self.osutil.enable_firewall( - dst_ip=protocol.endpoint, - uid=os.getuid()) + success = self.osutil.enable_firewall(dst_ip=protocol.endpoint, uid=os.getuid()) + add_periodic( logger.EVERY_HOUR, AGENT_NAME, @@ -151,25 +146,37 @@ self.osutil.publish_hostname(curr_hostname) self.hostname = curr_hostname - def handle_dhclient_restart(self): - if self.dhcp_id is None: + def get_dhcp_client_pid(self): + pid = [] + + try: + # return a sorted list since handle_dhclient_restart needs to compare the previous value with + # the new value and the comparison should not be affected by the order of the items in the list + pid = sorted(self.osutil.get_dhcp_pid()) + + if len(pid) == 0 and self.dhcp_warning_enabled: + logger.warn("Dhcp client is not running.") + except Exception as exception: if self.dhcp_warning_enabled: - logger.warn("Dhcp client is not running. ") - self.dhcp_id = self.osutil.get_dhcp_pid() - # disable subsequent error logging - self.dhcp_warning_enabled = self.dhcp_id is not None + logger.error("Failed to get the PID of the DHCP client: {0}", ustr(exception)) + + self.dhcp_warning_enabled = len(pid) != 0 + + return pid + + def handle_dhclient_restart(self): + if len(self.dhcp_id_list) == 0: + self.dhcp_id_list = self.get_dhcp_client_pid() return - # the dhcp process has not changed since the last check - if self.osutil.check_pid_alive(self.dhcp_id.strip()): + if all(self.osutil.check_pid_alive(pid) for pid in self.dhcp_id_list): return - new_pid = self.osutil.get_dhcp_pid() - if new_pid is not None and new_pid != self.dhcp_id: - logger.info("EnvMonitor: Detected dhcp client restart. " - "Restoring routing table.") + new_pid = self.get_dhcp_client_pid() + if len(new_pid) != 0 and new_pid != self.dhcp_id_list: + logger.info("EnvMonitor: Detected dhcp client restart. Restoring routing table.") self.dhcp_handler.conf_routes() - self.dhcp_id = new_pid + self.dhcp_id_list = new_pid def archive_history(self): """ diff -Nru waagent-2.2.34/azurelinuxagent/ga/exthandlers.py waagent-2.2.45/azurelinuxagent/ga/exthandlers.py --- waagent-2.2.34/azurelinuxagent/ga/exthandlers.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/ga/exthandlers.py 2019-11-07 00:36:56.000000000 +0000 @@ -26,34 +26,31 @@ import re import shutil import stat -import subprocess +import sys +import tempfile import time import traceback import zipfile -import sys import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.version as version - -from azurelinuxagent.common.cgroups import CGroups, CGroupsTelemetry -from azurelinuxagent.common.errorstate import ErrorState, ERROR_STATE_DELTA_DEFAULT, ERROR_STATE_DELTA_INSTALL +from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator +from azurelinuxagent.common.datacontract import get_properties, set_properties +from azurelinuxagent.common.errorstate import ErrorState, ERROR_STATE_DELTA_INSTALL from azurelinuxagent.common.event import add_event, WALAEventOperation, elapsed_milliseconds, report_event from azurelinuxagent.common.exception import ExtensionError, ProtocolError, ProtocolNotFoundError, \ - ExtensionDownloadError, ExtensionOperationError + ExtensionDownloadError, ExtensionErrorCodes, ExtensionUpdateError, ExtensionOperationError from azurelinuxagent.common.future import ustr +from azurelinuxagent.common.protocol import get_protocol_util from azurelinuxagent.common.protocol.restapi import ExtHandlerStatus, \ - ExtensionStatus, \ - ExtensionSubStatus, \ - VMStatus, ExtHandler, \ - get_properties, \ - set_properties + ExtensionStatus, \ + ExtensionSubStatus, \ + VMStatus, ExtHandler from azurelinuxagent.common.utils.flexible_version import FlexibleVersion -from azurelinuxagent.common.utils.processutil import capture_from_process -from azurelinuxagent.common.protocol import get_protocol_util -from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION - +from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION, GOAL_STATE_AGENT_VERSION, \ + DISTRO_NAME, DISTRO_VERSION, PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO # HandlerEnvironment.json schema version HANDLER_ENVIRONMENT_VERSION = 1.0 @@ -66,7 +63,7 @@ VALID_HANDLER_STATUS = ['Ready', 'NotReady', "Installing", "Unresponsive"] HANDLER_PATTERN = "^([^-]+)-(\d+(?:\.\d+)*)" -HANDLER_NAME_PATTERN = re.compile(HANDLER_PATTERN+"$", re.IGNORECASE) +HANDLER_NAME_PATTERN = re.compile(HANDLER_PATTERN + "$", re.IGNORECASE) HANDLER_PKG_EXT = ".zip" HANDLER_PKG_PATTERN = re.compile(HANDLER_PATTERN + r"\.zip$", re.IGNORECASE) @@ -74,6 +71,22 @@ AGENT_STATUS_FILE = "waagent_status.json" +NUMBER_OF_DOWNLOAD_RETRIES = 5 + +# This is the default value for the env variables, whenever we call a command which is not an update scenario, we +# set the env variable value to NOT_RUN to reduce ambiguity for the extension publishers +NOT_RUN = "NOT_RUN" + + +class ExtCommandEnvVariable(object): + Prefix = "AZURE_GUEST_AGENT" + DisableReturnCode = "%s_DISABLE_CMD_EXIT_CODE" % Prefix + UninstallReturnCode = "%s_UNINSTALL_CMD_EXIT_CODE" % Prefix + ExtensionPath = "%s_EXTENSION_PATH" % Prefix + ExtensionVersion = "%s_EXTENSION_VERSION" % Prefix + ExtensionSeqNumber = "ConfigSequenceNumber" # At par with Windows Guest Agent + UpdatingFromVersion = "%s_UPDATING_FROM_VERSION" % Prefix + def get_traceback(e): if sys.version_info[0] == 3: @@ -82,6 +95,7 @@ ex_type, ex, tb = sys.exc_info() return tb + def validate_has_key(obj, key, fullname): if key not in obj: raise ExtensionError("Missing: {0}".format(fullname)) @@ -136,6 +150,9 @@ formatted_message = status_data.get('formattedMessage') ext_status.message = parse_formatted_message(formatted_message) substatus_list = status_data.get('substatus', []) + # some extensions incorrectly report an empty substatus with a null value + if substatus_list is None: + substatus_list = [] for substatus in substatus_list: if substatus is not None: ext_status.substatusList.append(parse_ext_substatus(substatus)) @@ -214,7 +231,7 @@ self.get_artifact_error_state.reset() except Exception as e: msg = u"Exception retrieving extension handlers: {0}".format(ustr(e)) - detailed_msg = '{0} {1}'.format(msg, traceback.print_tb(get_traceback(e))) + detailed_msg = '{0} {1}'.format(msg, traceback.extract_tb(get_traceback(e))) self.get_artifact_error_state.incr() @@ -224,7 +241,7 @@ op=WALAEventOperation.GetArtifactExtended, is_success=False, message="Failed to get extension artifact for over " - "{0): {1}".format(self.get_artifact_error_state.min_timedelta, msg)) + "{0}: {1}".format(self.get_artifact_error_state.min_timedelta, msg)) self.get_artifact_error_state.reset() else: logger.warn(msg) @@ -241,20 +258,22 @@ logger.verbose(msg) # Log status report success on new config self.log_report = True - self.handle_ext_handlers(etag) - self.last_etag = etag + + if self.extension_processing_allowed(): + self.handle_ext_handlers(etag) + self.last_etag = etag self.report_ext_handlers_status() self.cleanup_outdated_handlers() except Exception as e: - msg = u"Exception processing extension handlers: {0}".format( - ustr(e)) + msg = u"Exception processing extension handlers: {0}".format(ustr(e)) + detailed_msg = '{0} {1}'.format(msg, traceback.extract_tb(get_traceback(e))) logger.warn(msg) add_event(AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.ExtensionProcessing, is_success=False, - message=msg) + message=detailed_msg) return def cleanup_outdated_handlers(self): @@ -280,7 +299,7 @@ separator = item.rfind('-') eh.name = item[0:separator] - eh.properties.version = str(FlexibleVersion(item[separator+1:])) + eh.properties.version = str(FlexibleVersion(item[separator + 1:])) handler = ExtHandlerInstance(eh, self.protocol) except Exception: @@ -306,7 +325,7 @@ # Finally, remove the directories and packages of the # uninstalled handlers for handler in handlers: - handler.rm_ext_handler_dir() + handler.remove_ext_handler() pkg = os.path.join(conf.get_lib_dir(), handler.get_full_name() + HANDLER_PKG_EXT) if os.path.isfile(pkg): try: @@ -314,23 +333,29 @@ logger.verbose("Removed extension package {0}".format(pkg)) except OSError as e: logger.warn("Failed to remove extension package {0}: {1}".format(pkg, e.strerror)) - - def handle_ext_handlers(self, etag=None): + + def extension_processing_allowed(self): if not conf.get_extensions_enabled(): logger.verbose("Extension handling is disabled") - return + return False + if conf.get_enable_overprovisioning(): + if not self.protocol.supports_overprovisioning(): + logger.verbose("Overprovisioning is enabled but protocol does not support it.") + else: + artifacts_profile = self.protocol.get_artifacts_profile() + if artifacts_profile and artifacts_profile.is_on_hold(): + logger.info("Extension handling is on hold") + return False + + return True + + def handle_ext_handlers(self, etag=None): if self.ext_handlers.extHandlers is None or \ len(self.ext_handlers.extHandlers) == 0: logger.verbose("No extension handler config found") return - if conf.get_enable_overprovisioning(): - artifacts_profile = self.protocol.get_artifacts_profile() - if artifacts_profile and artifacts_profile.is_on_hold(): - logger.info("Extension handling is on hold") - return - wait_until = datetime.datetime.utcnow() + datetime.timedelta(minutes=DEFAULT_EXT_TIMEOUT_MINUTES) max_dep_level = max([handler.sort_key() for handler in self.ext_handlers.extHandlers]) @@ -366,7 +391,8 @@ # In case of timeout or terminal error state, we log it and return false # so that the extensions waiting on this one can be skipped processing if datetime.datetime.utcnow() > wait_until: - msg = "Extension {0} did not reach a terminal state within the allowed timeout. Last status was {1}".format(ext.name, status) + msg = "Extension {0} did not reach a terminal state within the allowed timeout. Last status was {1}".format( + ext.name, status) logger.warn(msg) add_event(AGENT_NAME, version=CURRENT_VERSION, @@ -402,7 +428,7 @@ return self.get_artifact_error_state.reset() - if not ext_handler_i.is_upgrade and self.last_etag == etag: + if self.last_etag == etag: if self.log_etag: ext_handler_i.logger.verbose("Version {0} is current for etag {1}", ext_handler_i.pkg.version, @@ -422,20 +448,37 @@ else: message = u"Unknown ext handler state:{0}".format(state) raise ExtensionError(message) - except ExtensionOperationError as e: - self.handle_handle_ext_handler_error(ext_handler_i, e, e.code) + except ExtensionUpdateError as e: + # Not reporting the error as it has already been reported from the old version + self.handle_ext_handler_error(ext_handler_i, e, e.code, report_telemetry_event=False) + except ExtensionDownloadError as e: + self.handle_ext_handler_download_error(ext_handler_i, e, e.code) except ExtensionError as e: - self.handle_handle_ext_handler_error(ext_handler_i, e, e.code) + self.handle_ext_handler_error(ext_handler_i, e, e.code) except Exception as e: - self.handle_handle_ext_handler_error(ext_handler_i, e) + self.handle_ext_handler_error(ext_handler_i, e) + + def handle_ext_handler_error(self, ext_handler_i, e, code=-1, report_telemetry_event=True): + msg = ustr(e) + ext_handler_i.set_handler_status(message=msg, code=code) - def handle_handle_ext_handler_error(self, ext_handler_i, e, code=-1): + if report_telemetry_event: + ext_handler_i.report_event(message=msg, is_success=False, log_event=True) + + def handle_ext_handler_download_error(self, ext_handler_i, e, code=-1): msg = ustr(e) ext_handler_i.set_handler_status(message=msg, code=code) - ext_handler_i.report_event(message=msg, is_success=False, log_event=True) + + self.get_artifact_error_state.incr() + if self.get_artifact_error_state.is_triggered(): + report_event(op=WALAEventOperation.Download, is_success=False, log_event=True, + message="Failed to get artifact for over " + "{0}: {1}".format(self.get_artifact_error_state.min_timedelta, msg)) + self.get_artifact_error_state.reset() def handle_enable(self, ext_handler_i): self.log_process = True + uninstall_exit_code = None old_ext_handler_i = ext_handler_i.get_installed_ext_handler() handler_state = ext_handler_i.get_handler_state() @@ -444,23 +487,63 @@ if handler_state == ExtHandlerState.NotInstalled: ext_handler_i.set_handler_state(ExtHandlerState.NotInstalled) ext_handler_i.download() + ext_handler_i.initialize() ext_handler_i.update_settings() if old_ext_handler_i is None: ext_handler_i.install() elif ext_handler_i.version_ne(old_ext_handler_i): - old_ext_handler_i.disable() - ext_handler_i.copy_status_files(old_ext_handler_i) - if ext_handler_i.version_gt(old_ext_handler_i): - ext_handler_i.update() - else: - old_ext_handler_i.update(version=ext_handler_i.ext_handler.properties.version) - old_ext_handler_i.uninstall() - old_ext_handler_i.rm_ext_handler_dir() - ext_handler_i.update_with_install() + uninstall_exit_code = ExtHandlersHandler._update_extension_handler_and_return_if_failed( + old_ext_handler_i, ext_handler_i) else: ext_handler_i.update_settings() - ext_handler_i.enable() + ext_handler_i.enable(uninstall_exit_code=uninstall_exit_code) + + @staticmethod + def _update_extension_handler_and_return_if_failed(old_ext_handler_i, ext_handler_i): + + def execute_old_handler_command_and_return_if_succeeds(func): + """ + Created a common wrapper to execute all commands that need to be executed from the old handler + so that it can have a common exception handling mechanism + :param func: The command to be executed on the old handler + :return: True if command execution succeeds and False if it fails + """ + continue_on_update_failure = False + exit_code = 0 + try: + continue_on_update_failure = ext_handler_i.load_manifest().is_continue_on_update_failure() + func() + except ExtensionError as e: + # Reporting the event with the old handler and raising a new ExtensionUpdateError to set the + # handler status on the new version + msg = "%s; ContinueOnUpdate: %s" % (ustr(e), continue_on_update_failure) + old_ext_handler_i.report_event(message=msg, is_success=False) + if not continue_on_update_failure: + raise ExtensionUpdateError(msg) + + exit_code = e.code + if isinstance(e, ExtensionOperationError): + exit_code = e.exit_code + + logger.info("Continue on Update failure flag is set, proceeding with update") + return exit_code + + disable_exit_code = execute_old_handler_command_and_return_if_succeeds( + func=lambda: old_ext_handler_i.disable()) + ext_handler_i.copy_status_files(old_ext_handler_i) + if ext_handler_i.version_gt(old_ext_handler_i): + ext_handler_i.update(disable_exit_code=disable_exit_code, + updating_from_version=old_ext_handler_i.ext_handler.properties.version) + else: + updating_from_version = ext_handler_i.ext_handler.properties.version + old_ext_handler_i.update(version=updating_from_version, + disable_exit_code=disable_exit_code, updating_from_version=updating_from_version) + uninstall_exit_code = execute_old_handler_command_and_return_if_succeeds( + func=lambda: old_ext_handler_i.uninstall()) + old_ext_handler_i.remove_ext_handler() + ext_handler_i.update_with_install(uninstall_exit_code=uninstall_exit_code) + return uninstall_exit_code def handle_disable(self, ext_handler_i): self.log_process = True @@ -478,8 +561,14 @@ if handler_state != ExtHandlerState.NotInstalled: if handler_state == ExtHandlerState.Enabled: ext_handler_i.disable() - ext_handler_i.uninstall() - ext_handler_i.rm_ext_handler_dir() + + # Try uninstalling the extension and swallow any exceptions in case of failures after logging them + try: + ext_handler_i.uninstall() + except ExtensionError as e: + ext_handler_i.report_event(message=ustr(e), is_success=False) + + ext_handler_i.remove_ext_handler() def report_ext_handlers_status(self): """ @@ -518,7 +607,7 @@ message=message) if self.report_status_error_state.is_triggered(): - message = "Failed to report vm agent status for more than {0}"\ + message = "Failed to report vm agent status for more than {0}" \ .format(self.report_status_error_state.min_timedelta) add_event(AGENT_NAME, @@ -529,10 +618,44 @@ self.report_status_error_state.reset() + self.write_ext_handlers_status_to_info_file(vm_status) + + @staticmethod + def write_ext_handlers_status_to_info_file(vm_status): + status_path = os.path.join(conf.get_lib_dir(), AGENT_STATUS_FILE) + + agent_details = { + "agent_name": AGENT_NAME, + "current_version": str(CURRENT_VERSION), + "goal_state_version": str(GOAL_STATE_AGENT_VERSION), + "distro_details": "{0}:{1}".format(DISTRO_NAME, DISTRO_VERSION), + "last_successful_status_upload_time": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "python_version": "Python: {0}.{1}.{2}".format(PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO) + } + + # Convert VMStatus class to Dict. + data = get_properties(vm_status) + + # The above class contains vmAgent.extensionHandlers + # (more info: azurelinuxagent.common.protocol.restapi.VMAgentStatus) + handler_statuses = data['vmAgent']['extensionHandlers'] + for handler_status in handler_statuses: + try: + handler_status.pop('code', None) + handler_status.pop('message', None) + handler_status.pop('extensions', None) + except KeyError: + pass + + agent_details['extensions_status'] = handler_statuses + agent_details_json = json.dumps(agent_details) + + fileutil.write_file(status_path, agent_details_json) + def report_ext_handler_status(self, vm_status, ext_handler): ext_handler_i = ExtHandlerInstance(ext_handler, self.protocol) - - handler_status = ext_handler_i.get_handler_status() + + handler_status = ext_handler_i.get_handler_status() if handler_status is None: return @@ -561,10 +684,9 @@ self.operation = None self.pkg = None self.pkg_file = None - self.is_upgrade = False self.logger = None self.set_logger() - + try: fileutil.mkdir(self.get_log_dir(), mode=0o755) except IOError as e: @@ -580,6 +702,9 @@ pkg_list = self.protocol.get_ext_handler_pkgs(self.ext_handler) except ProtocolError as e: raise ExtensionError("Failed to get ext handler pkgs", e) + except ExtensionDownloadError: + self.set_operation(WALAEventOperation.Download) + raise # Determine the desired and installed versions requested_version = FlexibleVersion(str(self.ext_handler.properties.version)) @@ -609,21 +734,16 @@ if target_state == u"uninstall" or target_state == u"disabled": if installed_pkg is None: msg = "Failed to find installed version of {0} " \ - "to uninstall".format(self.ext_handler.name) + "to uninstall".format(self.ext_handler.name) self.logger.warn(msg) self.pkg = installed_pkg self.ext_handler.properties.version = str(installed_version) \ - if installed_version is not None else None + if installed_version is not None else None else: self.pkg = selected_pkg if self.pkg is not None: self.ext_handler.properties.version = str(selected_pkg.version) - # Note if the selected package is different than that installed - if installed_pkg is None \ - or (self.pkg is not None and FlexibleVersion(self.pkg.version) != FlexibleVersion(installed_pkg.version)): - self.is_upgrade = True - if self.pkg is not None: self.logger.verbose("Use version: {0}", self.pkg.version) self.set_logger() @@ -647,7 +767,7 @@ lastest_version = self.get_installed_version() if lastest_version is None: return None - + installed_handler = ExtHandler() set_properties("ExtHandler", installed_handler, get_properties(self.ext_handler)) installed_handler.properties.version = lastest_version @@ -661,21 +781,21 @@ continue separator = path.rfind('-') - version_from_path = FlexibleVersion(path[separator+1:]) + version_from_path = FlexibleVersion(path[separator + 1:]) state_path = os.path.join(path, 'config', 'HandlerState') if not os.path.exists(state_path) or \ - fileutil.read_file(state_path) == \ + fileutil.read_file(state_path) == \ ExtHandlerState.NotInstalled: logger.verbose("Ignoring version of uninstalled extension: " - "{0}".format(path)) + "{0}".format(path)) continue if lastest_version is None or lastest_version < version_from_path: lastest_version = version_from_path return str(lastest_version) if lastest_version is not None else None - + def copy_status_files(self, old_ext_handler_i): self.logger.info("Copy status files from old plugin to new") old_ext_dir = old_ext_handler_i.get_base_dir() @@ -702,49 +822,85 @@ add_event(name=self.ext_handler.name, version=ext_handler_version, message=message, op=self.operation, is_success=is_success, duration=duration, log_event=log_event) + def _download_extension_package(self, source_uri, target_file): + self.logger.info("Downloading extension package: {0}", source_uri) + try: + if not self.protocol.download_ext_handler_pkg(source_uri, target_file): + raise Exception("Failed to download extension package - no error information is available") + except Exception as exception: + self.logger.info("Error downloading extension package: {0}", ustr(exception)) + if os.path.exists(target_file): + os.remove(target_file) + return False + return True + + def _unzip_extension_package(self, source_file, target_directory): + self.logger.info("Unzipping extension package: {0}", source_file) + try: + zipfile.ZipFile(source_file).extractall(target_directory) + except Exception as exception: + logger.info("Error while unzipping extension package: {0}", ustr(exception)) + os.remove(source_file) + if os.path.exists(target_directory): + shutil.rmtree(target_directory) + return False + return True + def download(self): begin_utc = datetime.datetime.utcnow() - self.logger.verbose("Download extension package") self.set_operation(WALAEventOperation.Download) - if self.pkg is None: + if self.pkg is None or self.pkg.uris is None or len(self.pkg.uris) == 0: raise ExtensionDownloadError("No package uri found") - uris_shuffled = self.pkg.uris - random.shuffle(uris_shuffled) - file_downloaded = False + destination = os.path.join(conf.get_lib_dir(), self.get_extension_package_zipfile_name()) - for uri in uris_shuffled: - try: - destination = os.path.join(conf.get_lib_dir(), os.path.basename(uri.uri) + ".zip") - file_downloaded = self.protocol.download_ext_handler_pkg(uri.uri, destination) + package_exists = False + if os.path.exists(destination): + self.logger.info("Using existing extension package: {0}", destination) + if self._unzip_extension_package(destination, self.get_base_dir()): + package_exists = True + else: + self.logger.info("The existing extension package is invalid, will ignore it.") - if file_downloaded and os.path.exists(destination): - self.pkg_file = destination + if not package_exists: + downloaded = False + i = 0 + while i < NUMBER_OF_DOWNLOAD_RETRIES: + uris_shuffled = self.pkg.uris + random.shuffle(uris_shuffled) + + for uri in uris_shuffled: + if not self._download_extension_package(uri.uri, destination): + continue + + if self._unzip_extension_package(destination, self.get_base_dir()): + downloaded = True + break + + if downloaded: break - except Exception as e: - logger.warn("Error while downloading extension: {0}", ustr(e)) - - if not file_downloaded: - raise ExtensionDownloadError("Failed to download extension", code=1001) + self.logger.info("Failed to download the extension package from all uris, will retry after a minute") + time.sleep(60) + i += 1 - self.logger.verbose("Unzip extension package") - try: - zipfile.ZipFile(self.pkg_file).extractall(self.get_base_dir()) - os.remove(self.pkg_file) - except IOError as e: - fileutil.clean_ioerror(e, paths=[self.get_base_dir(), self.pkg_file]) - raise ExtensionError(u"Failed to unzip extension package", e, code=1001) + if not downloaded: + raise ExtensionDownloadError("Failed to download extension", + code=ExtensionErrorCodes.PluginManifestDownloadError) + + duration = elapsed_milliseconds(begin_utc) + self.report_event(message="Download succeeded", duration=duration) + + self.pkg_file = destination + + def initialize(self): + self.logger.info("Initializing extension {0}".format(self.get_full_name())) # Add user execute permission to all files under the base dir for file in fileutil.get_all_files(self.get_base_dir()): fileutil.chmod(file, os.stat(file).st_mode | stat.S_IXUSR) - duration = elapsed_milliseconds(begin_utc) - self.report_event(message="Download succeeded", duration=duration) - - self.logger.info("Initialize extension directory") # Save HandlerManifest.json man_file = fileutil.search_file(self.get_base_dir(), 'HandlerManifest.json') @@ -783,17 +939,24 @@ except IOError as e: fileutil.clean_ioerror(e, paths=[self.get_base_dir(), self.pkg_file]) - raise ExtensionDownloadError(u"Failed to create status or config dir", e) + raise ExtensionDownloadError(u"Failed to initialize extension '{0}'".format(self.get_full_name()), e) + + # Create cgroups for the extension + CGroupConfigurator.get_instance().create_extension_cgroups(self.get_full_name()) # Save HandlerEnvironment.json self.create_handler_env() - def enable(self): + def enable(self, uninstall_exit_code=None): + uninstall_exit_code = str(uninstall_exit_code) if uninstall_exit_code is not None else NOT_RUN + env = {ExtCommandEnvVariable.UninstallReturnCode: uninstall_exit_code} + self.set_operation(WALAEventOperation.Enable) man = self.load_manifest() enable_cmd = man.get_enable_command() self.logger.info("Enable extension [{0}]".format(enable_cmd)) - self.launch_command(enable_cmd, timeout=300, extension_error_code=1009) + self.launch_command(enable_cmd, timeout=300, + extension_error_code=ExtensionErrorCodes.PluginEnableProcessingFailed, env=env) self.set_handler_state(ExtHandlerState.Enabled) self.set_handler_status(status="Ready", message="Plugin enabled") @@ -803,44 +966,63 @@ disable_cmd = man.get_disable_command() self.logger.info("Disable extension [{0}]".format(disable_cmd)) self.launch_command(disable_cmd, timeout=900, - extension_error_code=1010) + extension_error_code=ExtensionErrorCodes.PluginDisableProcessingFailed) self.set_handler_state(ExtHandlerState.Installed) self.set_handler_status(status="NotReady", message="Plugin disabled") - def install(self): + def install(self, uninstall_exit_code=None): + uninstall_exit_code = str(uninstall_exit_code) if uninstall_exit_code is not None else NOT_RUN + env = {ExtCommandEnvVariable.UninstallReturnCode: uninstall_exit_code} + man = self.load_manifest() install_cmd = man.get_install_command() self.logger.info("Install extension [{0}]".format(install_cmd)) self.set_operation(WALAEventOperation.Install) - self.launch_command(install_cmd, timeout=900, extension_error_code=1007) + self.launch_command(install_cmd, timeout=900, + extension_error_code=ExtensionErrorCodes.PluginInstallProcessingFailed, env=env) self.set_handler_state(ExtHandlerState.Installed) def uninstall(self): - try: - self.set_operation(WALAEventOperation.UnInstall) - man = self.load_manifest() - uninstall_cmd = man.get_uninstall_command() - self.logger.info("Uninstall extension [{0}]".format(uninstall_cmd)) - self.launch_command(uninstall_cmd) - except ExtensionError as e: - self.report_event(message=ustr(e), is_success=False) - - def rm_ext_handler_dir(self): - try: + self.set_operation(WALAEventOperation.UnInstall) + man = self.load_manifest() + uninstall_cmd = man.get_uninstall_command() + self.logger.info("Uninstall extension [{0}]".format(uninstall_cmd)) + self.launch_command(uninstall_cmd) + + def remove_ext_handler(self): + try: + zip_filename = os.path.join(conf.get_lib_dir(), self.get_extension_package_zipfile_name()) + if os.path.exists(zip_filename): + os.remove(zip_filename) + self.logger.verbose("Deleted the extension zip at path {0}", zip_filename) + base_dir = self.get_base_dir() if os.path.isdir(base_dir): - self.logger.info("Remove extension handler directory: {0}", - base_dir) - shutil.rmtree(base_dir) + self.logger.info("Remove extension handler directory: {0}", base_dir) + + # some extensions uninstall asynchronously so ignore error 2 while removing them + def on_rmtree_error(_, __, exc_info): + _, exception, _ = exc_info + if not isinstance(exception, OSError) or exception.errno != 2: # [Errno 2] No such file or directory + raise exception + + shutil.rmtree(base_dir, onerror=on_rmtree_error) except IOError as e: message = "Failed to remove extension handler directory: {0}".format(e) self.report_event(message=message, is_success=False) self.logger.warn(message) - def update(self, version=None): + # Also remove the cgroups for the extension + CGroupConfigurator.get_instance().remove_extension_cgroups(self.get_full_name()) + + def update(self, version=None, disable_exit_code=None, updating_from_version=None): if version is None: version = self.ext_handler.properties.version + disable_exit_code = str(disable_exit_code) if disable_exit_code is not None else NOT_RUN + env = {'VERSION': version, ExtCommandEnvVariable.DisableReturnCode: disable_exit_code, + ExtCommandEnvVariable.UpdatingFromVersion: updating_from_version} + try: self.set_operation(WALAEventOperation.Update) man = self.load_manifest() @@ -848,17 +1030,17 @@ self.logger.info("Update extension [{0}]".format(update_cmd)) self.launch_command(update_cmd, timeout=900, - extension_error_code=1008, - env={'VERSION': version}) + extension_error_code=ExtensionErrorCodes.PluginUpdateProcessingFailed, + env=env) except ExtensionError: # prevent the handler update from being retried self.set_handler_state(ExtHandlerState.Failed) raise - - def update_with_install(self): + + def update_with_install(self, uninstall_exit_code=None): man = self.load_manifest() if man.is_update_with_install(): - self.install() + self.install(uninstall_exit_code=uninstall_exit_code) else: self.logger.info("UpdateWithInstall not set. " "Skip install during upgrade.") @@ -904,8 +1086,8 @@ if seq_no > -1: path = os.path.join( - self.get_status_dir(), - "{0}.status".format(seq_no)) + self.get_status_dir(), + "{0}.status".format(seq_no)) return seq_no, path @@ -927,7 +1109,7 @@ ext_status.status = "error" except ExtensionError as e: ext_status.message = u"Malformed status file {0}".format(e) - ext_status.code = e.code + ext_status.code = ExtensionErrorCodes.PluginSettingsStatusInvalid ext_status.status = "error" except ValueError as e: ext_status.message = u"Malformed status file {0}".format(e) @@ -935,7 +1117,7 @@ ext_status.status = "error" return ext_status - + def get_ext_handling_status(self, ext): seq_no, ext_status_file = self.get_status_file_path(ext) if seq_no < 0 or ext_status_file is None: @@ -973,13 +1155,13 @@ if ext_status is None: continue try: - self.protocol.report_ext_status(self.ext_handler.name, ext.name, + self.protocol.report_ext_status(self.ext_handler.name, ext.name, ext_status) active_exts.append(ext.name) except ProtocolError as e: self.logger.error(u"Failed to report extension status: {0}", e) return active_exts - + def collect_heartbeat(self): man = self.load_manifest() if not man.is_report_heartbeat(): @@ -991,9 +1173,9 @@ raise ExtensionError("Failed to get heart beat file") if not self.is_responsive(heartbeat_file): return { - "status": "Unresponsive", - "code": -1, - "message": "Extension heartbeat is not responsive" + "status": "Unresponsive", + "code": -1, + "message": "Extension heartbeat is not responsive" } try: heartbeat_json = fileutil.read_file(heartbeat_file) @@ -1015,67 +1197,61 @@ last_update = int(time.time() - os.stat(heartbeat_file).st_mtime) return last_update <= 600 - def launch_command(self, cmd, timeout=300, extension_error_code=1000, env=None): + def launch_command(self, cmd, timeout=300, extension_error_code=ExtensionErrorCodes.PluginProcessingError, + env=None): begin_utc = datetime.datetime.utcnow() self.logger.verbose("Launch command: [{0}]", cmd) + base_dir = self.get_base_dir() - if env is None: - env = {} - env.update(os.environ) - - try: - # This should be .run(), but due to the wide variety - # of Python versions we must support we must use .communicate(). - # Some extensions erroneously begin cmd with a slash; don't interpret those - # as root-relative. (Issue #1170) - full_path = os.path.join(base_dir, cmd.lstrip(os.path.sep)) - - def pre_exec_function(): - """ - Change process state before the actual target process is started. Effectively, this runs between - the fork() and the exec() of sub-process creation. - :return: - """ - os.setsid() - CGroups.add_to_extension_cgroup(self.ext_handler.name) - - process = subprocess.Popen(full_path, - shell=True, - cwd=base_dir, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=env, - preexec_fn=pre_exec_function) - except OSError as e: - raise ExtensionOperationError("Failed to launch '{0}': {1}".format(full_path, e.strerror), - code=extension_error_code) - - cg = CGroups.for_extension(self.ext_handler.name) - CGroupsTelemetry.track_extension(self.ext_handler.name, cg) - msg = capture_from_process(process, cmd, timeout, extension_error_code) - - ret = process.poll() - if ret is None: - raise ExtensionOperationError("Process {0} was not terminated: {1}\n{2}".format(process.pid, cmd, msg), - code=extension_error_code) - if ret != 0: - raise ExtensionOperationError("Non-zero exit code: {0}, {1}\n{2}".format(ret, cmd, msg), - code=extension_error_code) - - duration = elapsed_milliseconds(begin_utc) - log_msg = "{0}\n{1}".format(cmd, "\n".join([line for line in msg.split('\n') if line != ""])) - self.logger.verbose(log_msg) - self.report_event(message=log_msg, duration=duration, log_event=False) + with tempfile.TemporaryFile(dir=base_dir, mode="w+b") as stdout: + with tempfile.TemporaryFile(dir=base_dir, mode="w+b") as stderr: + if env is None: + env = {} + env.update(os.environ) + # Always add Extension Path and version to the current launch_command (Ask from publishers) + env.update({ExtCommandEnvVariable.ExtensionPath: base_dir, + ExtCommandEnvVariable.ExtensionVersion: str(self.ext_handler.properties.version), + ExtCommandEnvVariable.ExtensionSeqNumber: str(self.get_seq_no())}) + + try: + # Some extensions erroneously begin cmd with a slash; don't interpret those + # as root-relative. (Issue #1170) + full_path = os.path.join(base_dir, cmd.lstrip(os.path.sep)) + + process_output = CGroupConfigurator.get_instance().start_extension_command( + extension_name=self.get_full_name(), + command=full_path, + timeout=timeout, + shell=True, + cwd=base_dir, + env=env, + stdout=stdout, + stderr=stderr, + error_code=extension_error_code) + + except OSError as e: + raise ExtensionError("Failed to launch '{0}': {1}".format(full_path, e.strerror), + code=extension_error_code) + + duration = elapsed_milliseconds(begin_utc) + log_msg = "{0}\n{1}".format(cmd, "\n".join([line for line in process_output.split('\n') if line != ""])) + + self.logger.verbose(log_msg) + self.report_event(message=log_msg, duration=duration, log_event=False) + + return process_output def load_manifest(self): man_file = self.get_manifest_file() try: data = json.loads(fileutil.read_file(man_file)) except (IOError, OSError) as e: - raise ExtensionError('Failed to load manifest file ({0}): {1}'.format(man_file, e.strerror), code=1002) + raise ExtensionError('Failed to load manifest file ({0}): {1}'.format(man_file, e.strerror), + code=ExtensionErrorCodes.PluginHandlerManifestNotFound) except ValueError: - raise ExtensionError('Malformed manifest file ({0}).'.format(man_file), code=1003) + raise ExtensionError('Malformed manifest file ({0}).'.format(man_file), + code=ExtensionErrorCodes.PluginHandlerManifestDeserializationError) return HandlerManifest(data[0]) @@ -1085,7 +1261,7 @@ fileutil.write_file(settings_file, settings) except IOError as e: fileutil.clean_ioerror(e, - paths=[settings_file]) + paths=[settings_file]) raise ExtensionError(u"Failed to update settings file", e) def update_settings(self): @@ -1096,7 +1272,7 @@ self.logger.info("Extension has no settings, write empty 0.settings") self.update_settings_file("0.settings", "") return - + for ext in self.ext_handler.properties.extensions: settings = { 'publicSettings': ext.publicSettings, @@ -1140,7 +1316,7 @@ except IOError as e: fileutil.clean_ioerror(e, paths=[state_file]) self.logger.error("Failed to set state: {0}", e) - + def get_handler_state(self): state_dir = self.get_conf_dir() state_file = os.path.join(state_dir, "HandlerState") @@ -1152,7 +1328,7 @@ except IOError as e: self.logger.error("Failed to get state: {0}", e) return ExtHandlerState.NotInstalled - + def set_handler_status(self, status="NotReady", message="", code=0): state_dir = self.get_conf_dir() @@ -1175,25 +1351,30 @@ except (IOError, ValueError, ProtocolError) as e: fileutil.clean_ioerror(e, paths=[status_file]) self.logger.error("Failed to save handler status: {0}, {1}", ustr(e), traceback.format_exc()) - + def get_handler_status(self): state_dir = self.get_conf_dir() status_file = os.path.join(state_dir, "HandlerStatus") if not os.path.isfile(status_file): return None - + try: data = json.loads(fileutil.read_file(status_file)) - handler_status = ExtHandlerStatus() + handler_status = ExtHandlerStatus() set_properties("ExtHandlerStatus", handler_status, data) return handler_status except (IOError, ValueError) as e: self.logger.error("Failed to get handler status: {0}", e) + def get_extension_package_zipfile_name(self): + return "{0}__{1}{2}".format(self.ext_handler.name, + self.ext_handler.properties.version, + HANDLER_PKG_EXT) + def get_full_name(self): - return "{0}-{1}".format(self.ext_handler.name, + return "{0}-{1}".format(self.ext_handler.name, self.ext_handler.properties.version) - + def get_base_dir(self): return os.path.join(conf.get_lib_dir(), self.get_full_name()) @@ -1215,6 +1396,17 @@ def get_log_dir(self): return os.path.join(conf.get_ext_log_dir(), self.ext_handler.name) + def get_seq_no(self): + runtime_settings = self.ext_handler.properties.extensions + # If no runtime_settings available for this ext_handler, then return 0 (this is the behavior we follow + # for update_settings) + if not runtime_settings or len(runtime_settings) == 0: + return "0" + # Currently for every runtime settings we use the same sequence number + # (Check : def parse_plugin_settings(self, ext_handler, plugin_settings) in wire.py) + # Will have to revisit once the feature to enable multiple runtime settings is rolled out by CRP + return self.ext_handler.properties.extensions[0].sequenceNumber + class HandlerEnvironment(object): def __init__(self, data): @@ -1271,3 +1463,6 @@ if update_mode is None: return True return update_mode.lower() == "updatewithinstall" + + def is_continue_on_update_failure(self): + return self.data['handlerManifest'].get('continueOnUpdateFailure', False) diff -Nru waagent-2.2.34/azurelinuxagent/ga/monitor.py waagent-2.2.45/azurelinuxagent/ga/monitor.py --- waagent-2.2.34/azurelinuxagent/ga/monitor.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/ga/monitor.py 2019-11-07 00:36:56.000000000 +0000 @@ -19,33 +19,30 @@ import json import os import platform -import time import threading -import traceback +import time import uuid import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger -from azurelinuxagent.common.errorstate import ErrorState +import azurelinuxagent.common.utils.networkutil as networkutil -from azurelinuxagent.common.cgroups import CGroups, CGroupsTelemetry -from azurelinuxagent.common.event import add_event, report_metric, WALAEventOperation +from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry +from azurelinuxagent.common.errorstate import ErrorState +from azurelinuxagent.common.event import add_event, WALAEventOperation, CONTAINER_ID_ENV_VARIABLE, \ + get_container_id_from_env from azurelinuxagent.common.exception import EventError, ProtocolError, OSUtilError, HttpError from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.protocol import get_protocol_util from azurelinuxagent.common.protocol.healthservice import HealthService from azurelinuxagent.common.protocol.imds import get_imds_client -from azurelinuxagent.common.protocol.restapi import TelemetryEventParam, \ - TelemetryEventList, \ - TelemetryEvent, \ - set_properties -import azurelinuxagent.common.utils.networkutil as networkutil +from azurelinuxagent.common.telemetryevent import TelemetryEvent, TelemetryEventParam, TelemetryEventList +from azurelinuxagent.common.datacontract import set_properties from azurelinuxagent.common.utils.restutil import IOErrorCounter from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, getattrib, hash_strings from azurelinuxagent.common.version import DISTRO_NAME, DISTRO_VERSION, \ - DISTRO_CODE_NAME, AGENT_LONG_VERSION, \ - AGENT_NAME, CURRENT_AGENT, CURRENT_VERSION + DISTRO_CODE_NAME, AGENT_NAME, CURRENT_AGENT, CURRENT_VERSION, AGENT_EXECUTION_MODE def parse_event(data_str): @@ -90,6 +87,17 @@ return event +def generate_extension_metrics_telemetry_dictionary(schema_version=1.0, + performance_metrics=None): + if schema_version == 1.0: + telemetry_dict = {"SchemaVersion": 1.0} + if performance_metrics: + telemetry_dict["PerfMetrics"] = performance_metrics + return telemetry_dict + else: + return None + + def get_monitor_handler(): return MonitorHandler() @@ -97,7 +105,9 @@ class MonitorHandler(object): EVENT_COLLECTION_PERIOD = datetime.timedelta(minutes=1) TELEMETRY_HEARTBEAT_PERIOD = datetime.timedelta(minutes=30) - CGROUP_TELEMETRY_PERIOD = datetime.timedelta(minutes=5) + # extension metrics period + CGROUP_TELEMETRY_POLLING_PERIOD = datetime.timedelta(minutes=5) + CGROUP_TELEMETRY_REPORTING_PERIOD = datetime.timedelta(minutes=30) # host plugin HOST_PLUGIN_HEARTBEAT_PERIOD = datetime.timedelta(minutes=1) HOST_PLUGIN_HEALTH_PERIOD = datetime.timedelta(minutes=5) @@ -105,15 +115,20 @@ IMDS_HEARTBEAT_PERIOD = datetime.timedelta(minutes=1) IMDS_HEALTH_PERIOD = datetime.timedelta(minutes=3) + # Resetting loggers period + RESET_LOGGERS_PERIOD = datetime.timedelta(hours=12) + def __init__(self): self.osutil = get_osutil() self.protocol_util = get_protocol_util() self.imds_client = get_imds_client() self.event_thread = None + self.last_reset_loggers_time = None self.last_event_collection = None self.last_telemetry_heartbeat = None - self.last_cgroup_telemetry = None + self.last_cgroup_polling_telemetry = None + self.last_cgroup_report_telemetry = None self.last_host_plugin_heartbeat = None self.last_imds_heartbeat = None self.protocol = None @@ -131,7 +146,6 @@ def run(self): self.init_protocols() self.init_sysinfo() - self.init_cgroups() self.start() def stop(self): @@ -149,6 +163,7 @@ def start(self): self.event_thread = threading.Thread(target=self.daemon) self.event_thread.setDaemon(True) + self.event_thread.setName("MonitorHandler") self.event_thread.start() def init_sysinfo(self): @@ -158,8 +173,7 @@ DISTRO_CODE_NAME, platform.release()) self.sysinfo.append(TelemetryEventParam("OSVersion", osversion)) - self.sysinfo.append( - TelemetryEventParam("GAVersion", CURRENT_AGENT)) + self.sysinfo.append(TelemetryEventParam("ExecutionMode", AGENT_EXECUTION_MODE)) try: ram = self.osutil.get_total_mem() @@ -167,7 +181,7 @@ self.sysinfo.append(TelemetryEventParam("RAM", ram)) self.sysinfo.append(TelemetryEventParam("Processors", processors)) except OSUtilError as e: - logger.warn("Failed to get system info: {0}", e) + logger.warn("Failed to get system info: {0}", ustr(e)) try: vminfo = self.protocol.get_vminfo() @@ -179,10 +193,8 @@ vminfo.roleName)) self.sysinfo.append(TelemetryEventParam("RoleInstanceName", vminfo.roleInstanceName)) - self.sysinfo.append(TelemetryEventParam("ContainerId", - vminfo.containerId)) except ProtocolError as e: - logger.warn("Failed to get system info: {0}", e) + logger.warn("Failed to get system info: {0}", ustr(e)) try: vminfo = self.imds_client.get_compute() @@ -197,22 +209,30 @@ self.sysinfo.append(TelemetryEventParam('ImageOrigin', vminfo.image_origin)) except (HttpError, ValueError) as e: - logger.warn("failed to get IMDS info: {0}", e) + logger.warn("failed to get IMDS info: {0}", ustr(e)) - def collect_event(self, evt_file_name): + @staticmethod + def collect_event(evt_file_name): try: logger.verbose("Found event file: {0}", evt_file_name) with open(evt_file_name, "rb") as evt_file: # if fail to open or delete the file, throw exception - data_str = evt_file.read().decode("utf-8", 'ignore') + data_str = evt_file.read().decode("utf-8") logger.verbose("Processed event file: {0}", evt_file_name) os.remove(evt_file_name) return data_str - except IOError as e: + except (IOError, UnicodeDecodeError) as e: + os.remove(evt_file_name) msg = "Failed to process {0}, {1}".format(evt_file_name, e) raise EventError(msg) def collect_and_send_events(self): + """ + Periodically read, parse, and send events located in the events folder. Currently, this is done every minute. + Any .tld file dropped in the events folder will be emitted. These event files can be created either by the + agent or the extensions. We don't have control over extension's events parameters, but we will override + any values they might have set for sys_info parameters. + """ if self.last_event_collection is None: self.last_event_collection = datetime.datetime.utcnow() - MonitorHandler.EVENT_COLLECTION_PERIOD @@ -228,7 +248,7 @@ try: data_str = self.collect_event(event_file_path) except EventError as e: - logger.error("{0}", e) + logger.error("{0}", ustr(e)) continue try: @@ -236,7 +256,7 @@ self.add_sysinfo(event) event_list.events.append(event) except (ValueError, ProtocolError) as e: - logger.warn("Failed to decode event file: {0}", e) + logger.warn("Failed to decode event file: {0}", ustr(e)) continue if len(event_list.events) == 0: @@ -245,36 +265,83 @@ try: self.protocol.report_event(event_list) except ProtocolError as e: - logger.error("{0}", e) + logger.error("{0}", ustr(e)) except Exception as e: - logger.warn("Failed to send events: {0}", e) + logger.warn("Failed to send events: {0}", ustr(e)) self.last_event_collection = datetime.datetime.utcnow() def daemon(self): min_delta = min(MonitorHandler.TELEMETRY_HEARTBEAT_PERIOD, - MonitorHandler.CGROUP_TELEMETRY_PERIOD, + MonitorHandler.CGROUP_TELEMETRY_POLLING_PERIOD, + MonitorHandler.CGROUP_TELEMETRY_REPORTING_PERIOD, MonitorHandler.EVENT_COLLECTION_PERIOD, MonitorHandler.HOST_PLUGIN_HEARTBEAT_PERIOD, MonitorHandler.IMDS_HEARTBEAT_PERIOD).seconds while self.should_run: self.send_telemetry_heartbeat() - self.send_cgroup_telemetry() + self.poll_telemetry_metrics() + self.send_telemetry_metrics() self.collect_and_send_events() self.send_host_plugin_heartbeat() self.send_imds_heartbeat() self.log_altered_network_configuration() + self.reset_loggers() time.sleep(min_delta) + def reset_loggers(self): + """ + The loggers maintain hash-tables in memory and they need to be cleaned up from time to time. + For reference, please check azurelinuxagent.common.logger.Logger and + azurelinuxagent.common.event.EventLogger classes + """ + time_now = datetime.datetime.utcnow() + if not self.last_reset_loggers_time: + self.last_reset_loggers_time = time_now + + if time_now >= (self.last_reset_loggers_time + + MonitorHandler.RESET_LOGGERS_PERIOD): + try: + logger.reset_periodic() + finally: + self.last_reset_loggers_time = time_now + def add_sysinfo(self, event): + """ + This method is called after parsing the event file in the events folder and before emitting it. This means + all events, either coming from the agent or from the extensions, are passed through this method. The purpose + is to add a static list of sys_info parameters such as VMName, Region, RAM, etc. If the sys_info parameters + are already populated in the event, they will be overwritten by the sys_info values obtained from the agent. + Since the ContainerId parameter is only populated on the fly for the agent events because it is not a static + sys_info parameter, an event coming from an extension will not have it, so we explicitly add it. + :param event: Event to be enriched with sys_info parameters + :return: Event with all parameters added, ready to be reported + """ sysinfo_names = [v.name for v in self.sysinfo] + final_parameters = [] + + # Refer: azurelinuxagent.common.event.EventLogger.add_default_parameters_to_event for agent specific values. + # + # Default fields are only populated by Agent and not the extension. Agent will fill up any event if they don't + # have the default params. Example: GAVersion and ContainerId are populated for agent events on the fly, + # but not for extension events. Add it if it's missing. + default_values = [("ContainerId", get_container_id_from_env()), ("GAVersion", CURRENT_AGENT), + ("OpcodeName", ""), ("EventTid", 0), ("EventPid", 0), ("TaskName", ""), ("KeywordName", "")] + for param in event.parameters: + # Discard any sys_info parameters already in the event, since they will be overwritten if param.name in sysinfo_names: - logger.verbose("Remove existing event parameter: [{0}:{1}]", - param.name, - param.value) - event.parameters.remove(param) - event.parameters.extend(self.sysinfo) + continue + final_parameters.append(param) + + # Add sys_info params populated by the agent + final_parameters.extend(self.sysinfo) + + for default_value in default_values: + if default_value[0] not in event: + final_parameters.append(TelemetryEventParam(default_value[0], default_value[1])) + + event.parameters = final_parameters def send_imds_heartbeat(self): """ @@ -395,73 +462,39 @@ message=msg, log_event=False) except Exception as e: - logger.warn("Failed to send heartbeat: {0}", e) + logger.warn("Failed to send heartbeat: {0}", ustr(e)) self.last_telemetry_heartbeat = datetime.datetime.utcnow() - @staticmethod - def init_cgroups(): - # Track metrics for the roll-up cgroup and for the agent cgroup - try: - CGroupsTelemetry.track_cgroup(CGroups.for_extension("")) - CGroupsTelemetry.track_agent() - except Exception as e: - # when a hierarchy is not mounted, we raise an exception - # and we should therefore only issue a warning, since this - # is not unexpected - logger.warn("Monitor: cgroups not initialized: {0}", ustr(e)) - logger.verbose(traceback.format_exc()) - - def send_cgroup_telemetry(self): - if self.last_cgroup_telemetry is None: - self.last_cgroup_telemetry = datetime.datetime.utcnow() - - if datetime.datetime.utcnow() >= (self.last_telemetry_heartbeat + MonitorHandler.CGROUP_TELEMETRY_PERIOD): - try: - metric_reported, metric_threshold = CGroupsTelemetry.collect_all_tracked() - for cgroup_name, metrics in metric_reported.items(): - thresholds = metric_threshold[cgroup_name] - - for metric_group, metric_name, value in metrics: - if value > 0: - report_metric(metric_group, metric_name, cgroup_name, value) - - if metric_group == "Memory": - # Memory is collected in bytes, and limit is set in megabytes. - if value >= CGroups._format_memory_value('megabytes', thresholds.memory_limit): - msg = "CGroup {0}: Crossed the Memory Threshold. " \ - "Current Value:{1} bytes, Threshold:{2} megabytes.".format(cgroup_name, value, - thresholds.memory_limit) - add_event(name=AGENT_NAME, - version=CURRENT_VERSION, - op=WALAEventOperation.CGroupsLimitsCrossed, - is_success=True, - message=msg, - log_event=True) - - if metric_group == "Process": - if value >= thresholds.cpu_limit: - msg = "CGroup {0}: Crossed the Processor Threshold. Current Value:{1}, Threshold:{2}.".format( - cgroup_name, value, thresholds.cpu_limit) - add_event(name=AGENT_NAME, - version=CURRENT_VERSION, - op=WALAEventOperation.CGroupsLimitsCrossed, - is_success=True, - message=msg, - log_event=True) - - except Exception as e: - logger.warn("Monitor: failed to collect cgroups performance metrics: {0}", ustr(e)) - logger.verbose(traceback.format_exc()) - - # Look for extension cgroups we're not already tracking and track them - try: - CGroupsTelemetry.update_tracked(self.protocol.client.get_current_handlers()) - except Exception as e: - logger.warn("Monitor: failed to update cgroups tracked extensions: {0}", ustr(e)) - logger.verbose(traceback.format_exc()) - - self.last_cgroup_telemetry = datetime.datetime.utcnow() + def poll_telemetry_metrics(self): + time_now = datetime.datetime.utcnow() + if not self.last_cgroup_polling_telemetry: + self.last_cgroup_polling_telemetry = time_now + + if time_now >= (self.last_cgroup_polling_telemetry + + MonitorHandler.CGROUP_TELEMETRY_POLLING_PERIOD): + CGroupsTelemetry.poll_all_tracked() + self.last_cgroup_polling_telemetry = time_now + + def send_telemetry_metrics(self): + time_now = datetime.datetime.utcnow() + + if not self.last_cgroup_report_telemetry: + self.last_cgroup_report_telemetry = time_now + + if time_now >= (self.last_cgroup_report_telemetry + MonitorHandler.CGROUP_TELEMETRY_REPORTING_PERIOD): + performance_metrics = CGroupsTelemetry.report_all_tracked() + self.last_cgroup_report_telemetry = time_now + + if performance_metrics: + message = generate_extension_metrics_telemetry_dictionary(schema_version=1.0, + performance_metrics=performance_metrics) + add_event(name=AGENT_NAME, + version=CURRENT_VERSION, + op=WALAEventOperation.ExtensionMetricsData, + is_success=True, + message=ustr(message), + log_event=False) def log_altered_network_configuration(self): """ diff -Nru waagent-2.2.34/azurelinuxagent/ga/remoteaccess.py waagent-2.2.45/azurelinuxagent/ga/remoteaccess.py --- waagent-2.2.34/azurelinuxagent/ga/remoteaccess.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/ga/remoteaccess.py 2019-11-07 00:36:56.000000000 +0000 @@ -18,46 +18,19 @@ # import datetime -import glob -import json -import operator import os import os.path -import pwd -import random -import re -import shutil -import stat -import subprocess -import textwrap -import time import traceback -import zipfile import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger -import azurelinuxagent.common.utils.fileutil as fileutil -import azurelinuxagent.common.version as version -import azurelinuxagent.common.protocol.wire -import azurelinuxagent.common.protocol.metadata as metadata from datetime import datetime, timedelta -from pwd import getpwall -from azurelinuxagent.common.errorstate import ErrorState -from azurelinuxagent.common.event import add_event, WALAEventOperation, elapsed_milliseconds -from azurelinuxagent.common.exception import ExtensionError, ProtocolError, RemoteAccessError +from azurelinuxagent.common.event import add_event, WALAEventOperation +from azurelinuxagent.common.exception import RemoteAccessError from azurelinuxagent.common.future import ustr -from azurelinuxagent.common.protocol.restapi import ExtHandlerStatus, \ - ExtensionStatus, \ - ExtensionSubStatus, \ - VMStatus, ExtHandler, \ - get_properties, \ - set_properties -from azurelinuxagent.common.protocol.metadata import MetadataProtocol from azurelinuxagent.common.utils.cryptutil import CryptUtil -from azurelinuxagent.common.utils.flexible_version import FlexibleVersion -from azurelinuxagent.common.utils.processutil import capture_from_process from azurelinuxagent.common.protocol import get_protocol_util from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION from azurelinuxagent.common.osutil import get_osutil @@ -69,9 +42,11 @@ MAX_TRY_ATTEMPT = 5 FAILED_ATTEMPT_THROTTLE = 1 + def get_remote_access_handler(): return RemoteAccessHandler() + class RemoteAccessHandler(object): def __init__(self): self.os_util = get_osutil() diff -Nru waagent-2.2.34/azurelinuxagent/ga/update.py waagent-2.2.45/azurelinuxagent/ga/update.py --- waagent-2.2.34/azurelinuxagent/ga/update.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/ga/update.py 2019-11-07 00:36:56.000000000 +0000 @@ -39,6 +39,7 @@ import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.restutil as restutil import azurelinuxagent.common.utils.textutil as textutil +from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator from azurelinuxagent.common.event import add_event, add_periodic, \ elapsed_milliseconds, \ @@ -55,7 +56,7 @@ from azurelinuxagent.common.version import AGENT_NAME, AGENT_VERSION, AGENT_LONG_VERSION, \ AGENT_DIR_GLOB, AGENT_PKG_GLOB, \ AGENT_PATTERN, AGENT_NAME_PATTERN, AGENT_DIR_PATTERN, \ - CURRENT_AGENT, CURRENT_VERSION, \ + CURRENT_AGENT, CURRENT_VERSION, DISTRO_NAME, DISTRO_VERSION, \ is_current_agent_installed from azurelinuxagent.ga.exthandlers import HandlerManifest @@ -165,14 +166,10 @@ logger.verbose(u"Agent {0} launched with command '{1}'", agent_name, agent_cmd) - # If the most current agent is the installed agent and update is enabled, - # assume updates are likely available and poll every second. - # This reduces the start-up impact of finding / launching agent updates on - # fresh VMs. - if latest_agent is None and conf.get_autoupdate_enabled(): - poll_interval = 1 - else: - poll_interval = CHILD_POLL_INTERVAL + # Setting the poll interval to poll every second to reduce the agent provisioning time; + # The daemon shouldn't wait for 60secs before starting the ext-handler in case the + # ext-handler kills itself during agent-update during the first 15 mins (CHILD_HEALTH_INTERVAL) + poll_interval = 1 ret = None start_time = time.time() @@ -245,20 +242,33 @@ self.child_process = None return - def run(self): + def run(self, debug=False): """ This is the main loop which watches for agent and extension updates. """ try: + # NOTE: Do not add any telemetry events until after the monitoring handler has been started with the + # call to 'monitor_thread.run()'. That method call initializes the protocol, which is needed in order to + # load the goal state and update the container id in memory. Any telemetry events sent before this happens + # will result in an uninitialized container id value. + logger.info(u"Agent {0} is running as the goal state agent", CURRENT_AGENT) + # Log OS-specific info locally. + os_info_msg = u"Distro info: {0} {1}, osutil class being used: {2}, " \ + u"agent service name: {3}".format(DISTRO_NAME, DISTRO_VERSION, + type(self.osutil).__name__, self.osutil.service_name) + logger.info(os_info_msg) + # Launch monitoring threads from azurelinuxagent.ga.monitor import get_monitor_handler monitor_thread = get_monitor_handler() monitor_thread.run() + # NOTE: Any telemetry events added from this point on will be properly populated with the container id. + from azurelinuxagent.ga.env import get_env_handler env_thread = get_env_handler() env_thread.run() @@ -274,13 +284,20 @@ self._emit_restart_event() self._ensure_partition_assigned() self._ensure_readonly_files() + self._ensure_cgroups_initialized() + + # Send OS-specific info as a telemetry event after the monitoring thread has been initialized, and with + # it the container id too. + add_event(AGENT_NAME, + op=WALAEventOperation.OSInfo, + message=os_info_msg) goal_state_interval = GOAL_STATE_INTERVAL \ if conf.get_extensions_enabled() \ else GOAL_STATE_INTERVAL_DISABLED while self.running: - if self._is_orphaned: + if not debug and self._is_orphaned: logger.info("Agent {0} is an orphan -- exiting", CURRENT_AGENT) break @@ -447,6 +464,12 @@ for path in glob.iglob(os.path.join(conf.get_lib_dir(), g)): os.chmod(path, stat.S_IRUSR) + def _ensure_cgroups_initialized(self): + configurator = CGroupConfigurator.get_instance() + configurator.create_agent_cgroups(track_cgroups=True) + configurator.cleanup_legacy_cgroups() + configurator.create_extension_cgroups_root() + def _evaluate_agent_health(self, latest_agent): """ Evaluate the health of the selected agent: If it is restarting @@ -491,10 +514,7 @@ def _get_host_plugin(self, protocol=None): return protocol.client.get_host_plugin() \ - if protocol and \ - type(protocol) is WireProtocol and \ - protocol.client \ - else None + if protocol and type(protocol) is WireProtocol and protocol.client else None def _get_pid_parts(self): pid_file = conf.get_agent_pid_file_path() @@ -640,57 +660,47 @@ self.last_attempt_time = now protocol = self.protocol_util.get_protocol() - for update_goal_state in [False, True]: - try: - if update_goal_state: - protocol.update_goal_state(forced=True) + try: + manifest_list, etag = protocol.get_vmagent_manifests() - manifest_list, etag = protocol.get_vmagent_manifests() + manifests = [m for m in manifest_list.vmAgentManifests \ + if m.family == family and len(m.versionsManifestUris) > 0] + if len(manifests) == 0: + logger.verbose(u"Incarnation {0} has no {1} agent updates", + etag, family) + return False - manifests = [m for m in manifest_list.vmAgentManifests \ - if m.family == family and \ - len(m.versionsManifestUris) > 0] - if len(manifests) == 0: - logger.verbose(u"Incarnation {0} has no {1} agent updates", - etag, family) - return False - - pkg_list = protocol.get_vmagent_pkgs(manifests[0]) - - # Set the agents to those available for download at least as - # current as the existing agent and remove from disk any agent - # no longer reported to the VM. - # Note: - # The code leaves on disk available, but blacklisted, agents - # so as to preserve the state. Otherwise, those agents could be - # again downloaded and inappropriately retried. - host = self._get_host_plugin(protocol=protocol) - self._set_agents([GuestAgent(pkg=pkg, host=host) \ - for pkg in pkg_list.versions]) - - self._purge_agents() - self._filter_blacklisted_agents() - - # Return True if current agent is no longer available or an - # agent with a higher version number is available - return not self._is_version_eligible(base_version) \ - or (len(self.agents) > 0 \ - and self.agents[0].version > base_version) + pkg_list = protocol.get_vmagent_pkgs(manifests[0]) - except Exception as e: - if isinstance(e, ResourceGoneError): - continue + # Set the agents to those available for download at least as + # current as the existing agent and remove from disk any agent + # no longer reported to the VM. + # Note: + # The code leaves on disk available, but blacklisted, agents + # so as to preserve the state. Otherwise, those agents could be + # again downloaded and inappropriately retried. + host = self._get_host_plugin(protocol=protocol) + self._set_agents([GuestAgent(pkg=pkg, host=host) for pkg in pkg_list.versions]) - msg = u"Exception retrieving agent manifests: {0}".format( - ustr(traceback.format_exc())) - logger.warn(msg) - add_event( - AGENT_NAME, - op=WALAEventOperation.Download, - version=CURRENT_VERSION, - is_success=False, - message=msg) - return False + self._purge_agents() + self._filter_blacklisted_agents() + + # Return True if current agent is no longer available or an + # agent with a higher version number is available + return not self._is_version_eligible(base_version) \ + or (len(self.agents) > 0 and self.agents[0].version > base_version) + + except Exception as e: + msg = u"Exception retrieving agent manifests: {0}".format( + ustr(traceback.format_exc())) + logger.warn(msg) + add_event( + AGENT_NAME, + op=WALAEventOperation.Download, + version=CURRENT_VERSION, + is_success=False, + message=msg) + return False def _write_pid_file(self): pid_files = self._get_pid_files() diff -Nru waagent-2.2.34/azurelinuxagent/pa/deprovision/factory.py waagent-2.2.45/azurelinuxagent/pa/deprovision/factory.py --- waagent-2.2.34/azurelinuxagent/pa/deprovision/factory.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/pa/deprovision/factory.py 2019-11-07 00:36:56.000000000 +0000 @@ -34,7 +34,7 @@ if distro_name == "arch": return ArchDeprovisionHandler() if distro_name == "ubuntu": - if Version(distro_version) in [Version('18.04')]: + if Version(distro_version) >= Version('18.04'): return Ubuntu1804DeprovisionHandler() else: return UbuntuDeprovisionHandler() diff -Nru waagent-2.2.34/azurelinuxagent/pa/provision/cloudinit.py waagent-2.2.45/azurelinuxagent/pa/provision/cloudinit.py --- waagent-2.2.34/azurelinuxagent/pa/provision/cloudinit.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/pa/provision/cloudinit.py 2019-11-07 00:36:56.000000000 +0000 @@ -19,6 +19,7 @@ import os import os.path +import subprocess import time from datetime import datetime @@ -41,13 +42,6 @@ super(CloudInitProvisionHandler, self).__init__() def run(self): - # If provision is enabled, run default provision handler - if conf.get_provision_enabled(): - logger.warn("Provisioning flag is enabled, which overrides using " - "cloud-init; running the default provisioning code") - super(CloudInitProvisionHandler, self).run() - return - try: if super(CloudInitProvisionHandler, self).is_provisioned(): logger.info("Provisioning already completed, skipping.") @@ -132,3 +126,69 @@ raise ProvisionError("Giving up, ssh host key was not found at {0} " "after {1}s".format(path, max_retry * sleep_time)) + +def _cloud_init_is_enabled_systemd(): + """ + Determine whether or not cloud-init is enabled on a systemd machine. + + Args: + None + + Returns: + bool: True if cloud-init is enabled, False if otherwise. + """ + + try: + systemctl_output = subprocess.check_output([ + 'systemctl', + 'is-enabled', + 'cloud-init-local.service' + ], stderr=subprocess.STDOUT).decode('utf-8').replace('\n', '') + + unit_is_enabled = systemctl_output == 'enabled' + except Exception as exc: + logger.info('Error getting cloud-init enabled status from systemctl: {0}'.format(exc)) + unit_is_enabled = False + + return unit_is_enabled + +def _cloud_init_is_enabled_service(): + """ + Determine whether or not cloud-init is enabled on a non-systemd machine. + + Args: + None + + Returns: + bool: True if cloud-init is enabled, False if otherwise. + """ + + try: + subprocess.check_output([ + 'service', + 'cloud-init', + 'status' + ], stderr=subprocess.STDOUT) + + unit_is_enabled = True + except Exception as exc: + logger.info('Error getting cloud-init enabled status from service: {0}'.format(exc)) + unit_is_enabled = False + + return unit_is_enabled + +def cloud_init_is_enabled(): + """ + Determine whether or not cloud-init is enabled. + + Args: + None + + Returns: + bool: True if cloud-init is enabled, False if otherwise. + """ + + unit_is_enabled = _cloud_init_is_enabled_systemd() or _cloud_init_is_enabled_service() + logger.info('cloud-init is enabled: {0}'.format(unit_is_enabled)) + + return unit_is_enabled diff -Nru waagent-2.2.34/azurelinuxagent/pa/provision/factory.py waagent-2.2.45/azurelinuxagent/pa/provision/factory.py --- waagent-2.2.34/azurelinuxagent/pa/provision/factory.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/pa/provision/factory.py 2019-11-07 00:36:56.000000000 +0000 @@ -16,19 +16,24 @@ # import azurelinuxagent.common.conf as conf - +from azurelinuxagent.common import logger from azurelinuxagent.common.version import DISTRO_NAME, DISTRO_VERSION, \ DISTRO_FULL_NAME from .default import ProvisionHandler -from .cloudinit import CloudInitProvisionHandler +from .cloudinit import CloudInitProvisionHandler, cloud_init_is_enabled -def get_provision_handler(distro_name=DISTRO_NAME, +def get_provision_handler(distro_name=DISTRO_NAME, distro_version=DISTRO_VERSION, distro_full_name=DISTRO_FULL_NAME): - if conf.get_provision_cloudinit(): + provisioning_agent = conf.get_provisioning_agent() + + if provisioning_agent == 'cloud-init' or ( + provisioning_agent == 'auto' and + cloud_init_is_enabled()): + logger.info('Using cloud-init for provisioning') return CloudInitProvisionHandler() + logger.info('Using waagent for provisioning') return ProvisionHandler() - diff -Nru waagent-2.2.34/azurelinuxagent/pa/rdma/centos.py waagent-2.2.45/azurelinuxagent/pa/rdma/centos.py --- waagent-2.2.34/azurelinuxagent/pa/rdma/centos.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/pa/rdma/centos.py 2019-11-07 00:36:56.000000000 +0000 @@ -59,7 +59,7 @@ # Find out RDMA firmware version and see if the existing package needs # updating or if the package is missing altogether (and install it) - fw_version = RDMAHandler.get_rdma_version() + fw_version = self.get_rdma_version() if not fw_version: raise Exception('Cannot determine RDMA firmware version') logger.info("RDMA: found firmware version: {0}".format(fw_version)) @@ -241,4 +241,4 @@ raise Exception("RDMA: failed to install kvp daemon package '%s'" % kvp_pkg_to_install) logger.info("RDMA: package '%s' successfully installed" % kvp_pkg_to_install) logger.info("RDMA: Machine will now be rebooted.") - self.reboot_system() \ No newline at end of file + self.reboot_system() diff -Nru waagent-2.2.34/azurelinuxagent/pa/rdma/factory.py waagent-2.2.45/azurelinuxagent/pa/rdma/factory.py --- waagent-2.2.34/azurelinuxagent/pa/rdma/factory.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/pa/rdma/factory.py 2019-11-07 00:36:56.000000000 +0000 @@ -23,6 +23,8 @@ from .centos import CentOSRDMAHandler from .ubuntu import UbuntuRDMAHandler +from distutils.version import LooseVersion as Version + def get_rdma_handler( distro_full_name=DISTRO_FULL_NAME, @@ -32,11 +34,11 @@ if ( (distro_full_name == 'SUSE Linux Enterprise Server' or distro_full_name == 'SLES') and - int(distro_version) > 11 + Version(distro_version) > Version('11') ): return SUSERDMAHandler() - if distro_full_name == 'CentOS Linux' or distro_full_name == 'CentOS': + if distro_full_name == 'CentOS Linux' or distro_full_name == 'CentOS' or distro_full_name == 'Red Hat Enterprise Linux Server': return CentOSRDMAHandler(distro_version) if distro_full_name == 'Ubuntu': diff -Nru waagent-2.2.34/azurelinuxagent/pa/rdma/suse.py waagent-2.2.45/azurelinuxagent/pa/rdma/suse.py --- waagent-2.2.34/azurelinuxagent/pa/rdma/suse.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/pa/rdma/suse.py 2019-11-07 00:36:56.000000000 +0000 @@ -1,6 +1,6 @@ # Microsoft Azure Linux Agent # -# Copyright 2017 Microsoft Corporation +# Copyright 2018 Microsoft Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ def install_driver(self): """Install the appropriate driver package for the RDMA firmware""" - fw_version = RDMAHandler.get_rdma_version() + fw_version = self.get_rdma_version() if not fw_version: error_msg = 'RDMA: Could not determine firmware version. ' error_msg += 'Therefore, no driver will be installed.' @@ -40,7 +40,23 @@ zypper_remove = 'zypper -n rm %s' zypper_search = 'zypper -n se -s %s' zypper_unlock = 'zypper removelock %s' - package_name = 'msft-rdma-kmp-default' + package_name = 'dummy' + # Figure out the kernel that is running to find the proper kmp + cmd = 'uname -r' + status, kernel_release = shellutil.run_get_output(cmd) + if 'default' in kernel_release: + package_name = 'msft-rdma-kmp-default' + info_msg = 'RDMA: Detected kernel-default' + logger.info(info_msg) + elif 'azure' in kernel_release: + package_name = 'msft-rdma-kmp-azure' + info_msg = 'RDMA: Detected kernel-azure' + logger.info(info_msg) + else: + error_msg = 'RDMA: Could not detect kernel build, unable to ' + error_msg += 'load kernel module. Kernel release: "%s"' + logger.error(error_msg % kernel_release) + return cmd = zypper_search % package_name status, repo_package_info = shellutil.run_get_output(cmd) driver_package_versions = [] diff -Nru waagent-2.2.34/azurelinuxagent/pa/rdma/ubuntu.py waagent-2.2.45/azurelinuxagent/pa/rdma/ubuntu.py --- waagent-2.2.34/azurelinuxagent/pa/rdma/ubuntu.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/azurelinuxagent/pa/rdma/ubuntu.py 2019-11-07 00:36:56.000000000 +0000 @@ -32,7 +32,7 @@ def install_driver(self): #Install the appropriate driver package for the RDMA firmware - nd_version = RDMAHandler.get_rdma_version() + nd_version = self.get_rdma_version() if not nd_version: logger.error("RDMA: Could not determine firmware version. No driver will be installed") return diff -Nru waagent-2.2.34/config/arch/waagent.conf waagent-2.2.45/config/arch/waagent.conf --- waagent-2.2.34/config/arch/waagent.conf 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/config/arch/waagent.conf 2019-11-07 00:36:56.000000000 +0000 @@ -61,6 +61,9 @@ # Enable verbose logging (y|n) Logs.Verbose=n +# Enable Console logging, default is y +# Logs.Console=y + # Is FIPS enabled OS.EnableFIPS=n diff -Nru waagent-2.2.34/config/bigip/waagent.conf waagent-2.2.45/config/bigip/waagent.conf --- waagent-2.2.34/config/bigip/waagent.conf 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/config/bigip/waagent.conf 2019-11-07 00:36:56.000000000 +0000 @@ -59,6 +59,9 @@ # Enable verbose logging (y|n) Logs.Verbose=n +# Enable Console logging, default is y +# Logs.Console=y + # Is FIPS enabled OS.EnableFIPS=n diff -Nru waagent-2.2.34/config/clearlinux/waagent.conf waagent-2.2.45/config/clearlinux/waagent.conf --- waagent-2.2.34/config/clearlinux/waagent.conf 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/config/clearlinux/waagent.conf 2019-11-07 00:36:56.000000000 +0000 @@ -60,6 +60,9 @@ # Enable verbose logging (y|n) Logs.Verbose=n +# Enable Console logging, default is y +# Logs.Console=y + # Is FIPS enabled OS.EnableFIPS=n diff -Nru waagent-2.2.34/config/coreos/waagent.conf waagent-2.2.45/config/coreos/waagent.conf --- waagent-2.2.34/config/coreos/waagent.conf 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/config/coreos/waagent.conf 2019-11-07 00:36:56.000000000 +0000 @@ -65,6 +65,9 @@ # Enable verbose logging (y|n) Logs.Verbose=n +# Enable Console logging, default is y +# Logs.Console=y + # Is FIPS enabled OS.EnableFIPS=n diff -Nru waagent-2.2.34/config/debian/waagent.conf waagent-2.2.45/config/debian/waagent.conf --- waagent-2.2.34/config/debian/waagent.conf 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/config/debian/waagent.conf 2019-11-07 00:36:56.000000000 +0000 @@ -62,6 +62,9 @@ # Enable verbose logging (y|n) Logs.Verbose=n +# Enable Console logging, default is y +# Logs.Console=y + # Is FIPS enabled OS.EnableFIPS=n diff -Nru waagent-2.2.34/config/freebsd/waagent.conf waagent-2.2.45/config/freebsd/waagent.conf --- waagent-2.2.34/config/freebsd/waagent.conf 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/config/freebsd/waagent.conf 2019-11-07 00:36:56.000000000 +0000 @@ -59,6 +59,9 @@ # Enable verbose logging (y|n) Logs.Verbose=n +# Enable Console logging, default is y +# Logs.Console=y + # Is FIPS enabled OS.EnableFIPS=n diff -Nru waagent-2.2.34/config/gaia/waagent.conf waagent-2.2.45/config/gaia/waagent.conf --- waagent-2.2.34/config/gaia/waagent.conf 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/config/gaia/waagent.conf 2019-11-07 00:36:56.000000000 +0000 @@ -62,6 +62,9 @@ # Enable verbose logging (y|n) Logs.Verbose=n +# Enable Console logging, default is y +# Logs.Console=y + # Is FIPS enabled OS.EnableFIPS=n diff -Nru waagent-2.2.34/config/iosxe/waagent.conf waagent-2.2.45/config/iosxe/waagent.conf --- waagent-2.2.34/config/iosxe/waagent.conf 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/config/iosxe/waagent.conf 2019-11-07 00:36:56.000000000 +0000 @@ -58,6 +58,9 @@ # Enable verbose logging (y|n) Logs.Verbose=n +# Enable Console logging, default is y +# Logs.Console=y + # Is FIPS enabled OS.EnableFIPS=n diff -Nru waagent-2.2.34/config/nsbsd/waagent.conf waagent-2.2.45/config/nsbsd/waagent.conf --- waagent-2.2.34/config/nsbsd/waagent.conf 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/config/nsbsd/waagent.conf 2019-11-07 00:36:56.000000000 +0000 @@ -55,6 +55,9 @@ # Enable verbose logging (y|n) TODO set n Logs.Verbose=n +# Enable Console logging, default is y +# Logs.Console=y + # Is FIPS enabled OS.EnableFIPS=n diff -Nru waagent-2.2.34/config/openbsd/waagent.conf waagent-2.2.45/config/openbsd/waagent.conf --- waagent-2.2.34/config/openbsd/waagent.conf 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/config/openbsd/waagent.conf 2019-11-07 00:36:56.000000000 +0000 @@ -55,6 +55,9 @@ # Enable verbose logging (y|n) Logs.Verbose=n +# Enable Console logging, default is y +# Logs.Console=y + # Is FIPS enabled OS.EnableFIPS=n diff -Nru waagent-2.2.34/config/suse/waagent.conf waagent-2.2.45/config/suse/waagent.conf --- waagent-2.2.34/config/suse/waagent.conf 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/config/suse/waagent.conf 2019-11-07 00:36:56.000000000 +0000 @@ -65,6 +65,9 @@ # Enable verbose logging (y|n) Logs.Verbose=n +# Enable Console logging, default is y +# Logs.Console=y + # Is FIPS enabled OS.EnableFIPS=n @@ -102,6 +105,9 @@ # Enable RDMA management and set up, should only be used in HPC images # OS.EnableRDMA=y +# Enable checking RDMA driver version and update +# OS.CheckRdmaDriver=y + # Enable or disable goal state processing auto-update, default is enabled # AutoUpdate.Enabled=y diff -Nru waagent-2.2.34/config/ubuntu/waagent.conf waagent-2.2.45/config/ubuntu/waagent.conf --- waagent-2.2.34/config/ubuntu/waagent.conf 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/config/ubuntu/waagent.conf 2019-11-07 00:36:56.000000000 +0000 @@ -65,6 +65,9 @@ # Enable verbose logging (y|n) Logs.Verbose=n +# Enable Console logging, default is y +# Logs.Console=y + # Is FIPS enabled OS.EnableFIPS=n @@ -90,6 +93,9 @@ # Enable RDMA kernel update, this value is effective on Ubuntu # OS.UpdateRdmaDriver=y +# Enable checking RDMA driver version and update +# OS.CheckRdmaDriver=y + # Enable or disable goal state processing auto-update, default is enabled # AutoUpdate.Enabled=y diff -Nru waagent-2.2.34/config/waagent.conf waagent-2.2.45/config/waagent.conf --- waagent-2.2.34/config/waagent.conf 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/config/waagent.conf 2019-11-07 00:36:56.000000000 +0000 @@ -62,6 +62,9 @@ # Enable verbose logging (y|n) Logs.Verbose=n +# Enable Console logging, default is y +# Logs.Console=y + # Is FIPS enabled OS.EnableFIPS=n @@ -102,6 +105,9 @@ # Enable RDMA management and set up, should only be used in HPC images # OS.EnableRDMA=y +# Enable checking RDMA driver version and update +# OS.CheckRdmaDriver=y + # Enable or disable goal state processing auto-update, default is enabled # AutoUpdate.Enabled=y diff -Nru waagent-2.2.34/debian/.gitignore waagent-2.2.45/debian/.gitignore --- waagent-2.2.34/debian/.gitignore 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/debian/.gitignore 2020-04-27 06:48:47.000000000 +0000 @@ -0,0 +1,6 @@ +*.debhelper +*.log +*.substvars +files +/patches +waagent diff -Nru waagent-2.2.34/debian/README.source.md waagent-2.2.45/debian/README.source.md --- waagent-2.2.34/debian/README.source.md 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/debian/README.source.md 2020-04-27 06:48:47.000000000 +0000 @@ -0,0 +1,9 @@ +# Source handling for waagent + +This source uses the `3.0 (gitarchive)` source format in the git repository. + +## Building a Debian source package from git + +* Install `dpkg-source-gitarchive`. + This is not a build-dependency, as it is only required to build the source, not binaries. +* Run `dpkg-buildpackage -S -nc` (or `dpkg-source --build .`). diff -Nru waagent-2.2.34/debian/changelog waagent-2.2.45/debian/changelog --- waagent-2.2.34/debian/changelog 2019-04-29 14:45:57.000000000 +0000 +++ waagent-2.2.45/debian/changelog 2020-04-27 06:48:47.000000000 +0000 @@ -1,3 +1,43 @@ +waagent (2.2.45-4~deb10u1) buster; urgency=medium + + * Upload to buster. + + -- Bastian Blank Mon, 27 Apr 2020 08:45:24 +0200 + +waagent (2.2.45-4) unstable; urgency=medium + + * Build-depend on required python3-distro. (closes: #954492) + + -- Bastian Blank Fri, 27 Mar 2020 14:41:41 +0100 + +waagent (2.2.45-3) unstable; urgency=medium + + * Disable resource disk if cloud-init is enabled. + * Open log before trying to log anything. + * Make compatible with Python 3.8. + + -- Bastian Blank Thu, 19 Mar 2020 16:13:51 +0100 + +waagent (2.2.45-2) unstable; urgency=medium + + * Use new source format. + * Fix resource disk setup. + + -- Bastian Blank Mon, 20 Jan 2020 16:34:23 +0100 + +waagent (2.2.45-1) unstable; urgency=medium + + * New upstream version. (closes: #911701) + * Support co-installation with cloud-init. + + -- Bastian Blank Fri, 06 Dec 2019 14:00:41 +0100 + +waagent (2.2.41-1) unstable; urgency=medium + + * New upstream version. + + -- Bastian Blank Thu, 08 Aug 2019 16:05:11 +0200 + waagent (2.2.34-4) unstable; urgency=medium * Fix stray backup file due to incorrect sed call. (closes: #928179) diff -Nru waagent-2.2.34/debian/control waagent-2.2.45/debian/control --- waagent-2.2.34/debian/control 2019-04-16 08:05:24.000000000 +0000 +++ waagent-2.2.45/debian/control 2020-04-27 06:48:47.000000000 +0000 @@ -7,6 +7,7 @@ dh-python, dh-systemd, python3, + python3-distro, python3-setuptools, Standards-Version: 3.9.6 Homepage: https://github.com/Azure/WALinuxAgent @@ -27,7 +28,7 @@ parted, sudo, net-tools, -Conflicts: cloud-init, network-manager, walinuxagent +Conflicts: network-manager, walinuxagent Description: Windows Azure Linux Agent The Windows Azure Linux Agent (waagent) manages VM interaction with the Windows Azure Fabric Controller. It provides the following functionality for IaaS diff -Nru waagent-2.2.34/debian/patches/agent-command-provision.patch waagent-2.2.45/debian/patches/agent-command-provision.patch --- waagent-2.2.34/debian/patches/agent-command-provision.patch 2019-04-16 08:05:24.000000000 +0000 +++ waagent-2.2.45/debian/patches/agent-command-provision.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,30 +0,0 @@ -From: Bastian Blank -Date: Wed, 4 Jan 2017 18:05:59 +0100 -Subject: Add provision command to agent - ---- - azurelinuxagent/agent.py | 4 +++- - 1 file changed, 3 insertions(+), 1 deletion(-) - -diff --git a/azurelinuxagent/agent.py b/azurelinuxagent/agent.py -index bf0a89b..b352aa2 100644 ---- a/azurelinuxagent/agent.py -+++ b/azurelinuxagent/agent.py -@@ -198,6 +198,8 @@ def parse_args(sys_args): - cmd = "deprovision+user" - elif re.match("^([-/]*)deprovision", a): - cmd = "deprovision" -+ elif re.match("^([-/]*)provision", a): -+ cmd = "provision" - elif re.match("^([-/]*)daemon", a): - cmd = "daemon" - elif re.match("^([-/]*)start", a): -@@ -240,7 +242,7 @@ def usage(): - s = "\n" - s += ("usage: {0} [-verbose] [-force] [-help] " - "-configuration-path:" -- "-deprovision[+user]|-register-service|-version|-daemon|-start|" -+ "-provision|-deprovision[+user]|-register-service|-version|-daemon|-start|" - "-run-exthandlers|-show-configuration]" - "").format(sys.argv[0]) - s += "\n" diff -Nru waagent-2.2.34/debian/patches/agent-command-resourcedisk.patch waagent-2.2.45/debian/patches/agent-command-resourcedisk.patch --- waagent-2.2.34/debian/patches/agent-command-resourcedisk.patch 2019-04-16 08:05:24.000000000 +0000 +++ waagent-2.2.45/debian/patches/agent-command-resourcedisk.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,51 +0,0 @@ -From: Bastian Blank -Date: Wed, 4 Jan 2017 18:06:30 +0100 -Subject: Add resourcedisk command to agent - ---- - azurelinuxagent/agent.py | 11 ++++++++++- - 1 file changed, 10 insertions(+), 1 deletion(-) - -diff --git a/azurelinuxagent/agent.py b/azurelinuxagent/agent.py -index b352aa2..bc56829 100644 ---- a/azurelinuxagent/agent.py -+++ b/azurelinuxagent/agent.py -@@ -135,6 +135,11 @@ class Agent(object): - update_handler = get_update_handler() - update_handler.run() - -+ def resourcedisk(self): -+ from azurelinuxagent.daemon.resourcedisk import get_resourcedisk_handler -+ resourcedisk_handler = get_resourcedisk_handler() -+ resourcedisk_handler.run() -+ - def show_configuration(self): - configuration = conf.get_configuration() - for k in sorted(configuration.keys()): -@@ -169,6 +174,8 @@ def main(args=[]): - agent.daemon() - elif command == "run-exthandlers": - agent.run_exthandlers() -+ elif command == "resourcedisk": -+ agent.resourcedisk() - elif command == "show-configuration": - agent.show_configuration() - except Exception: -@@ -208,6 +215,8 @@ def parse_args(sys_args): - cmd = "register-service" - elif re.match("^([-/]*)run-exthandlers", a): - cmd = "run-exthandlers" -+ elif re.match("^([-/]*)resourcedisk", a): -+ cmd = "resourcedisk" - elif re.match("^([-/]*)version", a): - cmd = "version" - elif re.match("^([-/]*)verbose", a): -@@ -243,7 +252,7 @@ def usage(): - s += ("usage: {0} [-verbose] [-force] [-help] " - "-configuration-path:" - "-provision|-deprovision[+user]|-register-service|-version|-daemon|-start|" -- "-run-exthandlers|-show-configuration]" -+ "-run-exthandlers||-resourcedisk-show-configuration]" - "").format(sys.argv[0]) - s += "\n" - return s diff -Nru waagent-2.2.34/debian/patches/cve-2019-0804.patch waagent-2.2.45/debian/patches/cve-2019-0804.patch --- waagent-2.2.34/debian/patches/cve-2019-0804.patch 2019-04-16 08:05:24.000000000 +0000 +++ waagent-2.2.45/debian/patches/cve-2019-0804.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,149 +0,0 @@ -From: Bastian Blank -Date: Mon, 11 Mar 2019 13:18:04 +0000 -Subject: Set proper access rights on swap file - -CVE-2019-0804 ---- - azurelinuxagent/daemon/resourcedisk/default.py | 31 ++++++++++++++++++++------ - azurelinuxagent/daemon/resourcedisk/freebsd.py | 5 +---- - tests/distro/test_resourceDisk.py | 31 ++++++++++++++++++++++++++ - 3 files changed, 56 insertions(+), 11 deletions(-) - -diff --git a/azurelinuxagent/daemon/resourcedisk/default.py b/azurelinuxagent/daemon/resourcedisk/default.py -index ce1309c..3e879f4 100644 ---- a/azurelinuxagent/daemon/resourcedisk/default.py -+++ b/azurelinuxagent/daemon/resourcedisk/default.py -@@ -16,6 +16,7 @@ - # - - import os -+import stat - import re - import subprocess - import sys -@@ -214,16 +215,27 @@ class ResourceDiskHandler(object): - else: - return 'mount {0} {1}'.format(partition, mount_point) - -+ @staticmethod -+ def check_existing_swap_file(swapfile, swaplist, size): -+ if swapfile in swaplist and os.path.isfile(swapfile) and os.path.getsize(swapfile) == size: -+ logger.info("Swap already enabled") -+ # restrict access to owner (remove all access from group, others) -+ swapfile_mode = os.stat(swapfile).st_mode -+ if swapfile_mode & (stat.S_IRWXG | stat.S_IRWXO): -+ swapfile_mode = swapfile_mode & ~(stat.S_IRWXG | stat.S_IRWXO) -+ logger.info("Changing mode of {0} to {1:o}".format(swapfile, swapfile_mode)) -+ os.chmod(swapfile, swapfile_mode) -+ return True -+ -+ return False -+ - def create_swap_space(self, mount_point, size_mb): - size_kb = size_mb * 1024 - size = size_kb * 1024 - swapfile = os.path.join(mount_point, 'swapfile') - swaplist = shellutil.run_get_output("swapon -s")[1] - -- if swapfile in swaplist \ -- and os.path.isfile(swapfile) \ -- and os.path.getsize(swapfile) == size: -- logger.info("Swap already enabled") -+ if self.check_existing_swap_file(swapfile, swaplist, size): - return - - if os.path.isfile(swapfile) and os.path.getsize(swapfile) != size: -@@ -274,13 +286,18 @@ class ResourceDiskHandler(object): - # Probable errors: - # - OSError: Seen on Cygwin, libc notimpl? - # - AttributeError: What if someone runs this under... -+ fd = None -+ - try: -- with open(filename, 'w') as f: -- os.posix_fallocate(f.fileno(), 0, nbytes) -- return 0 -+ fd = os.open(filename, os.O_CREAT | os.O_WRONLY | os.O_EXCL, stat.S_IRUSR | stat.S_IWUSR) -+ os.posix_fallocate(fd, 0, nbytes) -+ return 0 - except: - # Not confident with this thing, just keep trying... - pass -+ finally: -+ if fd is not None: -+ os.close(fd) - - # fallocate command - ret = shellutil.run( -diff --git a/azurelinuxagent/daemon/resourcedisk/freebsd.py b/azurelinuxagent/daemon/resourcedisk/freebsd.py -index ece166b..3d37285 100644 ---- a/azurelinuxagent/daemon/resourcedisk/freebsd.py -+++ b/azurelinuxagent/daemon/resourcedisk/freebsd.py -@@ -130,10 +130,7 @@ class FreeBSDResourceDiskHandler(ResourceDiskHandler): - swapfile = os.path.join(mount_point, 'swapfile') - swaplist = shellutil.run_get_output("swapctl -l")[1] - -- if swapfile in swaplist \ -- and os.path.isfile(swapfile) \ -- and os.path.getsize(swapfile) == size: -- logger.info("Swap already enabled") -+ if self.check_existing_swap_file(swapfile, swaplist, size): - return - - if os.path.isfile(swapfile) and os.path.getsize(swapfile) != size: -diff --git a/tests/distro/test_resourceDisk.py b/tests/distro/test_resourceDisk.py -index 4c185ee..3259836 100644 ---- a/tests/distro/test_resourceDisk.py -+++ b/tests/distro/test_resourceDisk.py -@@ -19,6 +19,7 @@ - # http://msdn.microsoft.com/en-us/library/cc227259%28PROT.13%29.aspx - - import sys -+import stat - from azurelinuxagent.common.utils import shellutil - from azurelinuxagent.daemon.resourcedisk import get_resourcedisk_handler - from tests.tools import * -@@ -38,6 +39,10 @@ class TestResourceDisk(AgentTestCase): - # assert - assert os.path.exists(test_file) - -+ # only the owner should have access -+ mode = os.stat(test_file).st_mode & (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) -+ assert mode == stat.S_IRUSR | stat.S_IWUSR -+ - # cleanup - os.remove(test_file) - -@@ -83,6 +88,32 @@ class TestResourceDisk(AgentTestCase): - assert run_patch.call_count == 1 - assert "dd if" in run_patch.call_args_list[0][0][0] - -+ def test_check_existing_swap_file(self): -+ test_file = os.path.join(self.tmp_dir, 'test_swap_file') -+ file_size = 1024 * 128 -+ if os.path.exists(test_file): -+ os.remove(test_file) -+ -+ with open(test_file, "wb") as file: -+ file.write(bytes(file_size)) -+ -+ os.chmod(test_file, stat.S_ISUID | stat.S_ISGID | stat.S_IRUSR | stat.S_IWUSR | stat.S_IRWXG | stat.S_IRWXO) # 0o6677 -+ -+ def swap_on(_): # mimic the output of "swapon -s" -+ return [ -+ "Filename Type Size Used Priority", -+ "{0} partition 16498684 0 -2".format(test_file) -+ ] -+ -+ with patch.object(shellutil, "run_get_output", side_effect=swap_on): -+ get_resourcedisk_handler().check_existing_swap_file(test_file, test_file, file_size) -+ -+ # it should remove access from group, others -+ mode = os.stat(test_file).st_mode & (stat.S_ISUID | stat.S_ISGID | stat.S_IRWXU | stat.S_IWUSR | stat.S_IRWXG | stat.S_IRWXO) # 0o6777 -+ assert mode == stat.S_ISUID | stat.S_ISGID | stat.S_IRUSR | stat.S_IWUSR # 0o6600 -+ -+ os.remove(test_file) -+ - - if __name__ == '__main__': - unittest.main() diff -Nru waagent-2.2.34/debian/patches/debian-changes waagent-2.2.45/debian/patches/debian-changes --- waagent-2.2.34/debian/patches/debian-changes 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/debian/patches/debian-changes 2020-04-27 06:48:47.000000000 +0000 @@ -0,0 +1,458 @@ +diff --git a/azurelinuxagent/agent.py b/azurelinuxagent/agent.py +index 2e3b26c..8590473 100644 +--- a/azurelinuxagent/agent.py ++++ b/azurelinuxagent/agent.py +@@ -49,10 +49,6 @@ class Agent(object): + self.conf_file_path = conf_file_path + self.osutil = get_osutil() + +- #Init stdout log +- level = logger.LogLevel.VERBOSE if verbose else logger.LogLevel.INFO +- logger.add_logger_appender(logger.AppenderType.STDOUT, level) +- + #Init config + conf_file_path = self.conf_file_path \ + if self.conf_file_path is not None \ +@@ -140,6 +136,11 @@ class Agent(object): + update_handler = get_update_handler() + update_handler.run(debug) + ++ def resourcedisk(self): ++ from azurelinuxagent.daemon.resourcedisk import get_resourcedisk_handler ++ resourcedisk_handler = get_resourcedisk_handler() ++ resourcedisk_handler.run() ++ + def show_configuration(self): + configuration = conf.get_configuration() + for k in sorted(configuration.keys()): +@@ -154,6 +155,11 @@ def main(args=[]): + if len(args) <= 0: + args = sys.argv[1:] + command, force, verbose, debug, conf_file_path = parse_args(args) ++ ++ #Init stdout log ++ level = logger.LogLevel.VERBOSE if verbose else logger.LogLevel.INFO ++ logger.add_logger_appender(logger.AppenderType.STDOUT, level) ++ + if command == "version": + version() + elif command == "help": +@@ -175,6 +181,8 @@ def main(args=[]): + agent.daemon() + elif command == "run-exthandlers": + agent.run_exthandlers(debug) ++ elif command == "resourcedisk": ++ agent.resourcedisk() + elif command == "show-configuration": + agent.show_configuration() + except Exception: +@@ -205,6 +213,8 @@ def parse_args(sys_args): + cmd = "deprovision+user" + elif re.match("^([-/]*)deprovision", a): + cmd = "deprovision" ++ elif re.match("^([-/]*)provision", a): ++ cmd = "provision" + elif re.match("^([-/]*)daemon", a): + cmd = "daemon" + elif re.match("^([-/]*)start", a): +@@ -213,6 +223,8 @@ def parse_args(sys_args): + cmd = "register-service" + elif re.match("^([-/]*)run-exthandlers", a): + cmd = "run-exthandlers" ++ elif re.match("^([-/]*)resourcedisk", a): ++ cmd = "resourcedisk" + elif re.match("^([-/]*)version", a): + cmd = "version" + elif re.match("^([-/]*)verbose", a): +@@ -251,8 +263,8 @@ def usage(): + s = "\n" + s += ("usage: {0} [-verbose] [-force] [-help] " + "-configuration-path:" +- "-deprovision[+user]|-register-service|-version|-daemon|-start|" +- "-run-exthandlers|-show-configuration]" ++ "-provision|-deprovision[+user]|-register-service|-version|-daemon|-start|" ++ "-run-exthandlers||-resourcedisk-show-configuration]" + "").format(sys.argv[0]) + s += "\n" + return s +diff --git a/azurelinuxagent/common/osutil/debian.py b/azurelinuxagent/common/osutil/debian.py +index 6e573ef..382a775 100644 +--- a/azurelinuxagent/common/osutil/debian.py ++++ b/azurelinuxagent/common/osutil/debian.py +@@ -43,10 +43,10 @@ class DebianOSBaseUtil(DefaultOSUtil): + return shellutil.run("systemctl --job-mode=ignore-dependencies try-reload-or-restart ssh", chk_err=False) + + def stop_agent_service(self): +- return shellutil.run("service azurelinuxagent stop", chk_err=False) ++ return shellutil.run("systemctl --job-mode=ignore-dependencies stop walinuxagent", chk_err=False) + + def start_agent_service(self): +- return shellutil.run("service azurelinuxagent start", chk_err=False) ++ return shellutil.run("systemctl --job-mode=ignore-dependencies start walinuxagent", chk_err=False) + + def start_network(self): + pass +diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py +index 7ccc534..27a9f8b 100644 +--- a/azurelinuxagent/common/osutil/default.py ++++ b/azurelinuxagent/common/osutil/default.py +@@ -423,9 +423,9 @@ class DefaultOSUtil(object): + return + + if expiration is not None: +- cmd = "useradd -m {0} -e {1}".format(username, expiration) ++ cmd = "useradd -m {0} -s /bin/bash -e {1}".format(username, expiration) + else: +- cmd = "useradd -m {0}".format(username) ++ cmd = "useradd -m {0} -s /bin/bash".format(username) + + if comment is not None: + cmd += " -c {0}".format(comment) +diff --git a/azurelinuxagent/common/osutil/factory.py b/azurelinuxagent/common/osutil/factory.py +index 56515ed..90cf495 100644 +--- a/azurelinuxagent/common/osutil/factory.py ++++ b/azurelinuxagent/common/osutil/factory.py +@@ -22,7 +22,7 @@ from .default import DefaultOSUtil + from .arch import ArchUtil + from .clearlinux import ClearLinuxUtil + from .coreos import CoreOSUtil +-from .debian import DebianOSBaseUtil, DebianOSModernUtil ++from .debian import DebianOSModernUtil + from .freebsd import FreeBSDOSUtil + from .openbsd import OpenBSDOSUtil + from .redhat import RedhatOSUtil, Redhat6xOSUtil +@@ -90,10 +90,7 @@ def _get_osutil(distro_name, distro_code_name, distro_version, distro_full_name) + return SUSEOSUtil() + + if distro_name == "debian": +- if "sid" in distro_version or Version(distro_version) > Version("7"): +- return DebianOSModernUtil() +- else: +- return DebianOSBaseUtil() ++ return DebianOSModernUtil() + + if distro_name == "redhat" \ + or distro_name == "centos" \ +diff --git a/azurelinuxagent/common/utils/shellutil.py b/azurelinuxagent/common/utils/shellutil.py +index 1260f9d..2ecf76f 100644 +--- a/azurelinuxagent/common/utils/shellutil.py ++++ b/azurelinuxagent/common/utils/shellutil.py +@@ -87,7 +87,6 @@ def run_get_output(cmd, chk_err=True, log_cmd=True, expected_errors=[]): + logger.verbose(u"Command: [{0}]", cmd) + try: + output = subprocess.check_output(cmd, +- stderr=subprocess.STDOUT, + shell=True) + output = _encode_command_output(output) + except subprocess.CalledProcessError as e: +diff --git a/azurelinuxagent/daemon/resourcedisk/default.py b/azurelinuxagent/daemon/resourcedisk/default.py +index 5ed14b0..73c764f 100644 +--- a/azurelinuxagent/daemon/resourcedisk/default.py ++++ b/azurelinuxagent/daemon/resourcedisk/default.py +@@ -18,6 +18,7 @@ + import os + import re + import stat ++import subprocess + import sys + import threading + from time import sleep +@@ -31,6 +32,7 @@ import azurelinuxagent.common.utils.shellutil as shellutil + from azurelinuxagent.common.exception import ResourceDiskError + from azurelinuxagent.common.osutil import get_osutil + from azurelinuxagent.common.version import AGENT_NAME ++from azurelinuxagent.pa.provision.cloudinit import cloud_init_is_enabled + + DATALOSS_WARNING_FILE_NAME = "DATALOSS_WARNING_README.txt" + DATA_LOSS_WARNING = """\ +@@ -55,6 +57,10 @@ class ResourceDiskHandler(object): + disk_thread.start() + + def run(self): ++ if cloud_init_is_enabled(): ++ logger.info('Using cloud-init for provisioning') ++ return ++ + mount_point = None + if conf.get_resourcedisk_format(): + mount_point = self.activate_resource_disk() +@@ -88,9 +94,8 @@ class ResourceDiskHandler(object): + logger.error("Failed to enable swap {0}", e) + + def reread_partition_table(self, device): +- if shellutil.run("sfdisk -R {0}".format(device), chk_err=False): +- shellutil.run("blockdev --rereadpt {0}".format(device), +- chk_err=False) ++ shellutil.run("blockdev --rereadpt {0}".format(device), ++ chk_err=False) + + def mount_resource_disk(self, mount_point): + device = self.osutil.device_for_ide_port(1) +@@ -117,7 +122,7 @@ class ResourceDiskHandler(object): + raise ResourceDiskError(msg=msg, inner=ose) + + logger.info("Examining partition table") +- ret = shellutil.run_get_output("parted {0} print".format(device)) ++ ret = shellutil.run_get_output("blkid -o value -s PTTYPE {0}".format(device)) + if ret[0]: + raise ResourceDiskError("Could not determine partition info for " + "{0}: {1}".format(device, ret[1])) +@@ -128,8 +133,9 @@ class ResourceDiskHandler(object): + mkfs_string = "mkfs.{0} -{2} {1}".format( + self.fs, partition, force_option) + +- if "gpt" in ret[1]: ++ if ret[1].strip() == "gpt": + logger.info("GPT detected, finding partitions") ++ ret = shellutil.run_get_output("parted {0} print".format(device)) + parts = [x for x in ret[1].split("\n") if + re.match(r"^\s*[0-9]+", x)] + logger.info("Found {0} GPT partition(s).", len(parts)) +@@ -147,21 +153,13 @@ class ResourceDiskHandler(object): + shellutil.run(mkfs_string) + else: + logger.info("GPT not detected, determining filesystem") +- ret = self.change_partition_type( +- suppress_message=True, +- option_str="{0} 1 -n".format(device)) +- ptype = ret[1].strip() +- if ptype == "7" and self.fs != "ntfs": ++ ret = shellutil.run_get_output("blkid -o value -s TYPE {0}".format(partition)) ++ if ret[1].strip() == 'ntfs' and self.fs != 'ntfs': + logger.info("The partition is formatted with ntfs, updating " + "partition type to 83") +- self.change_partition_type( +- suppress_message=False, +- option_str="{0} 1 83".format(device)) +- self.reread_partition_table(device) ++ subprocess.call(['sfdisk', '-c', '-f', device, '1', '83'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + logger.info("Format partition [{0}]", mkfs_string) + shellutil.run(mkfs_string) +- else: +- logger.info("The partition type is {0}", ptype) + + mount_options = conf.get_resourcedisk_mountoptions() + mount_string = self.get_mount_string(mount_options, +@@ -216,39 +214,6 @@ class ResourceDiskHandler(object): + self.fs) + return mount_point + +- def change_partition_type(self, suppress_message, option_str): +- """ +- use sfdisk to change partition type. +- First try with --part-type; if fails, fall back to -c +- """ +- +- command_to_use = '--part-type' +- input = "sfdisk {0} {1} {2}".format( +- command_to_use, '-f' if suppress_message else '', option_str) +- err_code, output = shellutil.run_get_output( +- input, chk_err=False, log_cmd=True) +- +- # fall back to -c +- if err_code != 0: +- logger.info( +- "sfdisk with --part-type failed [{0}], retrying with -c", +- err_code) +- command_to_use = '-c' +- input = "sfdisk {0} {1} {2}".format( +- command_to_use, '-f' if suppress_message else '', option_str) +- err_code, output = shellutil.run_get_output(input, log_cmd=True) +- +- if err_code == 0: +- logger.info('{0} succeeded', +- input) +- else: +- logger.error('{0} failed [{1}: {2}]', +- input, +- err_code, +- output) +- +- return err_code, output +- + @staticmethod + def get_mount_string(mount_options, partition, mount_point): + if mount_options is not None: +diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py +index c882bc1..073587b 100644 +--- a/azurelinuxagent/ga/update.py ++++ b/azurelinuxagent/ga/update.py +@@ -147,6 +147,9 @@ class UpdateHandler(object): + if child_args is not None: + agent_cmd = "{0} {1}".format(agent_cmd, child_args) + ++ env = os.environ.copy() ++ env['PYTHONDONTWRITEBYTECODE'] = '1' ++ + try: + + # Launch the correct Python version for python-based agents +@@ -162,7 +165,7 @@ class UpdateHandler(object): + cwd=agent_dir, + stdout=sys.stdout, + stderr=sys.stderr, +- env=os.environ) ++ env=env) + + logger.verbose(u"Agent {0} launched with command '{1}'", agent_name, agent_cmd) + +diff --git a/config/debian/waagent.conf b/config/debian/waagent.conf +index 28e496e..e605009 100644 +--- a/config/debian/waagent.conf ++++ b/config/debian/waagent.conf +@@ -106,7 +106,7 @@ OS.SshDir=/etc/ssh + # OS.EnableRDMA=y + + # Enable or disable goal state processing auto-update, default is enabled +-# AutoUpdate.Enabled=y ++AutoUpdate.Enabled=n + + # Determine the update family, this should not be changed + # AutoUpdate.GAFamily=Prod +diff --git a/setup.py b/setup.py +index ee0d839..889a494 100755 +--- a/setup.py ++++ b/setup.py +@@ -37,11 +37,6 @@ def set_files(data_files, dest=None, src=None): + data_files.append((dest, src)) + + +-def set_bin_files(data_files, dest="/usr/sbin", +- src=["bin/waagent", "bin/waagent2.0"]): +- data_files.append((dest, src)) +- +- + def set_conf_files(data_files, dest="/etc", src=["config/waagent.conf"]): + data_files.append((dest, src)) + +@@ -83,7 +78,6 @@ def get_data_files(name, version, fullname): + data_files = [] + + if name == 'redhat' or name == 'centos': +- set_bin_files(data_files) + set_conf_files(data_files) + set_logrotate_files(data_files) + set_udev_files(data_files) +@@ -96,13 +90,11 @@ def get_data_files(name, version, fullname): + # TODO this is a mitigation to systemctl bug on 7.1 + set_sysv_files(data_files) + elif name == 'arch': +- set_bin_files(data_files, dest="/usr/bin") + set_conf_files(data_files, src=["config/arch/waagent.conf"]) + set_udev_files(data_files) + set_systemd_files(data_files, dest='/usr/lib/systemd/system', + src=["init/arch/waagent.service"]) + elif name == 'coreos': +- set_bin_files(data_files, dest="/usr/share/oem/bin") + set_conf_files(data_files, dest="/usr/share/oem", + src=["config/coreos/waagent.conf"]) + set_logrotate_files(data_files) +@@ -110,13 +102,11 @@ def get_data_files(name, version, fullname): + set_files(data_files, dest="/usr/share/oem", + src=["init/coreos/cloud-config.yml"]) + elif "Clear Linux" in fullname: +- set_bin_files(data_files, dest="/usr/bin") + set_conf_files(data_files, dest="/usr/share/defaults/waagent", + src=["config/clearlinux/waagent.conf"]) + set_systemd_files(data_files, dest='/usr/lib/systemd/system', + src=["init/clearlinux/waagent.service"]) + elif name == 'ubuntu': +- set_bin_files(data_files) + set_conf_files(data_files, src=["config/ubuntu/waagent.conf"]) + set_logrotate_files(data_files) + set_udev_files(data_files) +@@ -134,7 +124,6 @@ def get_data_files(name, version, fullname): + set_systemd_files(data_files, + src=["init/ubuntu/walinuxagent.service"]) + elif name == 'suse' or name == 'opensuse': +- set_bin_files(data_files) + set_conf_files(data_files, src=["config/suse/waagent.conf"]) + set_logrotate_files(data_files) + set_udev_files(data_files) +@@ -148,15 +137,12 @@ def get_data_files(name, version, fullname): + # sles 12+ and openSUSE 13.2+ use systemd + set_systemd_files(data_files, dest='/usr/lib/systemd/system') + elif name == 'freebsd': +- set_bin_files(data_files, dest="/usr/local/sbin") + set_conf_files(data_files, src=["config/freebsd/waagent.conf"]) + set_freebsd_rc_files(data_files) + elif name == 'openbsd': +- set_bin_files(data_files, dest="/usr/local/sbin") + set_conf_files(data_files, src=["config/openbsd/waagent.conf"]) + set_openbsd_rc_files(data_files) + elif name == 'debian': +- set_bin_files(data_files) + set_conf_files(data_files, src=["config/debian/waagent.conf"]) + set_logrotate_files(data_files) + set_udev_files(data_files, dest="/lib/udev/rules.d") +@@ -178,7 +164,6 @@ def get_data_files(name, version, fullname): + set_sysv_files(data_files, dest='/etc/init.d', src=["init/openwrt/waagent"]) + else: + # Use default setting +- set_bin_files(data_files) + set_conf_files(data_files) + set_logrotate_files(data_files) + set_udev_files(data_files) +@@ -258,5 +243,8 @@ setuptools.setup( + install_requires=requires, + cmdclass={ + 'install': install +- } ++ }, ++ entry_points = { ++ 'console_scripts': ['waagent=azurelinuxagent.agent:main'], ++ }, + ) +diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py +index c435dcc..552de30 100644 +--- a/tests/ga/test_update.py ++++ b/tests/ga/test_update.py +@@ -3,6 +3,8 @@ + + from __future__ import print_function + ++import unittest ++ + from azurelinuxagent.common.event import * + from azurelinuxagent.common.protocol.hostplugin import * + from azurelinuxagent.common.protocol.metadata import * +@@ -1111,6 +1113,7 @@ class TestUpdate(UpdateTestCase): + self._test_run_latest(mock_time=mock_time) + self.assertEqual(1, mock_time.sleep_interval) + ++ @unittest.expectedFailure + def test_run_latest_defaults_to_current(self): + self.assertEqual(None, self.update_handler.get_latest_agent()) + +diff --git a/tests/protocol/test_wire.py b/tests/protocol/test_wire.py +index b7a324a..a0fa32f 100644 +--- a/tests/protocol/test_wire.py ++++ b/tests/protocol/test_wire.py +@@ -205,6 +205,7 @@ class TestWireProtocol(AgentTestCase): + self.assertEqual(patch_http.call_args_list[1][0][1], host_uri) + + @skip_if_predicate_true(running_under_travis, "Travis unit tests should not have external dependencies") ++ @skip_if_predicate_true(lambda: os.environ.get('https_proxy') is not None, "Skip if proxy is defined") + def test_download_ext_handler_pkg_stream(self, *args): + ext_uri = 'https://dcrdata.blob.core.windows.net/files/packer.zip' + tmp = tempfile.mkdtemp() +diff --git a/tests/utils/test_rest_util.py b/tests/utils/test_rest_util.py +index f097f10..49307c8 100644 +--- a/tests/utils/test_rest_util.py ++++ b/tests/utils/test_rest_util.py +@@ -174,6 +174,7 @@ class TestHttpOperations(AgentTestCase): + for x in urls_tuples: + self.assertEquals(restutil.redact_sas_tokens_in_urls(x[0]), x[1]) + ++ @skip_if_predicate_true(lambda: os.environ.get('https_proxy') is not None, "Skip if proxy is defined") + @patch('azurelinuxagent.common.conf.get_httpproxy_port') + @patch('azurelinuxagent.common.conf.get_httpproxy_host') + def test_get_http_proxy_none_is_default(self, mock_host, mock_port): +@@ -194,6 +195,7 @@ class TestHttpOperations(AgentTestCase): + self.assertEqual(1, mock_host.call_count) + self.assertEqual(1, mock_port.call_count) + ++ @skip_if_predicate_true(lambda: os.environ.get('https_proxy') is not None, "Skip if proxy is defined") + @patch('azurelinuxagent.common.conf.get_httpproxy_port') + @patch('azurelinuxagent.common.conf.get_httpproxy_host') + def test_get_http_proxy_configuration_requires_host(self, mock_host, mock_port): diff -Nru waagent-2.2.34/debian/patches/disable-auto-update.patch waagent-2.2.45/debian/patches/disable-auto-update.patch --- waagent-2.2.34/debian/patches/disable-auto-update.patch 2019-04-16 08:05:24.000000000 +0000 +++ waagent-2.2.45/debian/patches/disable-auto-update.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -From: Bastian Blank -Date: Tue, 29 May 2018 14:24:59 +0200 -Subject: Default auto update - ---- - config/debian/waagent.conf | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/config/debian/waagent.conf b/config/debian/waagent.conf -index 7d5c491..2b9926d 100644 ---- a/config/debian/waagent.conf -+++ b/config/debian/waagent.conf -@@ -103,7 +103,7 @@ OS.SshDir=/etc/ssh - # OS.EnableRDMA=y - - # Enable or disable goal state processing auto-update, default is enabled --# AutoUpdate.Enabled=y -+AutoUpdate.Enabled=n - - # Determine the update family, this should not be changed - # AutoUpdate.GAFamily=Prod diff -Nru waagent-2.2.34/debian/patches/disable-bytecode-exthandler.patch waagent-2.2.45/debian/patches/disable-bytecode-exthandler.patch --- waagent-2.2.34/debian/patches/disable-bytecode-exthandler.patch 2019-04-16 08:05:24.000000000 +0000 +++ waagent-2.2.45/debian/patches/disable-bytecode-exthandler.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,31 +0,0 @@ -From: Bastian Blank -Date: Fri, 7 Jul 2017 17:13:52 +0200 -Subject: Disable bytecode save in exthandler - ---- - azurelinuxagent/ga/update.py | 5 ++++- - 1 file changed, 4 insertions(+), 1 deletion(-) - -diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py -index d3c39c1..935451a 100644 ---- a/azurelinuxagent/ga/update.py -+++ b/azurelinuxagent/ga/update.py -@@ -146,6 +146,9 @@ class UpdateHandler(object): - if child_args is not None: - agent_cmd = "{0} {1}".format(agent_cmd, child_args) - -+ env = os.environ.copy() -+ env['PYTHONDONTWRITEBYTECODE'] = '1' -+ - try: - - # Launch the correct Python version for python-based agents -@@ -161,7 +164,7 @@ class UpdateHandler(object): - cwd=agent_dir, - stdout=sys.stdout, - stderr=sys.stderr, -- env=os.environ) -+ env=env) - - logger.verbose(u"Agent {0} launched with command '{1}'", agent_name, agent_cmd) - diff -Nru waagent-2.2.34/debian/patches/entry-points.patch waagent-2.2.45/debian/patches/entry-points.patch --- waagent-2.2.34/debian/patches/entry-points.patch 2019-04-16 08:05:24.000000000 +0000 +++ waagent-2.2.45/debian/patches/entry-points.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,102 +0,0 @@ -From: Bastian Blank -Date: Wed, 18 Jan 2017 13:09:05 +0100 -Subject: Use entry_points for scripts - ---- - setup.py | 20 ++++---------------- - 1 file changed, 4 insertions(+), 16 deletions(-) - -diff --git a/setup.py b/setup.py -index c24180d..0312629 100755 ---- a/setup.py -+++ b/setup.py -@@ -36,11 +36,6 @@ def set_files(data_files, dest=None, src=None): - data_files.append((dest, src)) - - --def set_bin_files(data_files, dest="/usr/sbin", -- src=["bin/waagent", "bin/waagent2.0"]): -- data_files.append((dest, src)) -- -- - def set_conf_files(data_files, dest="/etc", src=["config/waagent.conf"]): - data_files.append((dest, src)) - -@@ -80,7 +75,6 @@ def get_data_files(name, version, fullname): - data_files = [] - - if name == 'redhat' or name == 'centos': -- set_bin_files(data_files) - set_conf_files(data_files) - set_logrotate_files(data_files) - set_udev_files(data_files) -@@ -94,13 +88,11 @@ def get_data_files(name, version, fullname): - set_sysv_files(data_files) - - elif name == 'arch': -- set_bin_files(data_files, dest="/usr/bin") - set_conf_files(data_files, src=["config/arch/waagent.conf"]) - set_udev_files(data_files) - set_systemd_files(data_files, dest='/usr/lib/systemd/system', - src=["init/arch/waagent.service"]) - elif name == 'coreos': -- set_bin_files(data_files, dest="/usr/share/oem/bin") - set_conf_files(data_files, dest="/usr/share/oem", - src=["config/coreos/waagent.conf"]) - set_logrotate_files(data_files) -@@ -108,13 +100,11 @@ def get_data_files(name, version, fullname): - set_files(data_files, dest="/usr/share/oem", - src=["init/coreos/cloud-config.yml"]) - elif "Clear Linux" in fullname: -- set_bin_files(data_files, dest="/usr/bin") - set_conf_files(data_files, dest="/usr/share/defaults/waagent", - src=["config/clearlinux/waagent.conf"]) - set_systemd_files(data_files, dest='/usr/lib/systemd/system', - src=["init/clearlinux/waagent.service"]) - elif name == 'ubuntu': -- set_bin_files(data_files) - set_conf_files(data_files, src=["config/ubuntu/waagent.conf"]) - set_logrotate_files(data_files) - set_udev_files(data_files) -@@ -132,7 +122,6 @@ def get_data_files(name, version, fullname): - set_systemd_files(data_files, - src=["init/ubuntu/walinuxagent.service"]) - elif name == 'suse' or name == 'opensuse': -- set_bin_files(data_files) - set_conf_files(data_files, src=["config/suse/waagent.conf"]) - set_logrotate_files(data_files) - set_udev_files(data_files) -@@ -146,15 +135,12 @@ def get_data_files(name, version, fullname): - # sles 12+ and openSUSE 13.2+ use systemd - set_systemd_files(data_files, dest='/usr/lib/systemd/system') - elif name == 'freebsd': -- set_bin_files(data_files, dest="/usr/local/sbin") - set_conf_files(data_files, src=["config/freebsd/waagent.conf"]) - set_freebsd_rc_files(data_files) - elif name == 'openbsd': -- set_bin_files(data_files, dest="/usr/local/sbin") - set_conf_files(data_files, src=["config/openbsd/waagent.conf"]) - set_openbsd_rc_files(data_files) - elif name == 'debian': -- set_bin_files(data_files) - set_conf_files(data_files, src=["config/debian/waagent.conf"]) - set_logrotate_files(data_files) - set_udev_files(data_files, dest="/lib/udev/rules.d") -@@ -169,7 +155,6 @@ def get_data_files(name, version, fullname): - set_sysv_files(data_files) - else: - # Use default setting -- set_bin_files(data_files) - set_conf_files(data_files) - set_logrotate_files(data_files) - set_udev_files(data_files) -@@ -234,5 +219,8 @@ setuptools.setup( - install_requires=requires, - cmdclass={ - 'install': install -- } -+ }, -+ entry_points = { -+ 'console_scripts': ['waagent=azurelinuxagent.agent:main'], -+ }, - ) diff -Nru waagent-2.2.34/debian/patches/ignore-tests.patch waagent-2.2.45/debian/patches/ignore-tests.patch --- waagent-2.2.34/debian/patches/ignore-tests.patch 2019-04-16 08:05:24.000000000 +0000 +++ waagent-2.2.45/debian/patches/ignore-tests.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,63 +0,0 @@ -From: Bastian Blank -Date: Thu, 10 Jan 2019 09:39:05 +0100 -Subject: Ignore some broken tests - ---- - tests/ga/test_update.py | 3 +++ - tests/protocol/test_wire.py | 1 + - tests/utils/test_rest_util.py | 2 ++ - 3 files changed, 6 insertions(+) - -diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py -index 2443005..1f2fe20 100644 ---- a/tests/ga/test_update.py -+++ b/tests/ga/test_update.py -@@ -3,6 +3,8 @@ - - from __future__ import print_function - -+import unittest -+ - from azurelinuxagent.common.event import * - from azurelinuxagent.common.protocol.hostplugin import * - from azurelinuxagent.common.protocol.metadata import * -@@ -1112,6 +1114,7 @@ class TestUpdate(UpdateTestCase): - self._test_run_latest(mock_time=mock_time) - self.assertNotEqual(1, mock_time.sleep_interval) - -+ @unittest.expectedFailure - def test_run_latest_defaults_to_current(self): - self.assertEqual(None, self.update_handler.get_latest_agent()) - -diff --git a/tests/protocol/test_wire.py b/tests/protocol/test_wire.py -index 45b1dca..7b9b236 100644 ---- a/tests/protocol/test_wire.py -+++ b/tests/protocol/test_wire.py -@@ -178,6 +178,7 @@ class TestWireProtocol(AgentTestCase): - self.assertEqual(patch_http.call_args_list[1][0][1], host_uri) - - @skip_if_predicate_true(running_under_travis, "Travis unit tests should not have external dependencies") -+ @skip_if_predicate_true(lambda: os.environ.get('https_proxy') is not None, "Skip if proxy is defined") - def test_download_ext_handler_pkg_stream(self, *args): - ext_uri = 'https://dcrdata.blob.core.windows.net/files/packer.zip' - tmp = tempfile.mkdtemp() -diff --git a/tests/utils/test_rest_util.py b/tests/utils/test_rest_util.py -index 6f8518e..cb1b13c 100644 ---- a/tests/utils/test_rest_util.py -+++ b/tests/utils/test_rest_util.py -@@ -178,6 +178,7 @@ class TestHttpOperations(AgentTestCase): - for x in urls_tuples: - self.assertEquals(restutil.redact_sas_tokens_in_urls(x[0]), x[1]) - -+ @skip_if_predicate_true(lambda: os.environ.get('https_proxy') is not None, "Skip if proxy is defined") - @patch('azurelinuxagent.common.conf.get_httpproxy_port') - @patch('azurelinuxagent.common.conf.get_httpproxy_host') - def test_get_http_proxy_none_is_default(self, mock_host, mock_port): -@@ -198,6 +199,7 @@ class TestHttpOperations(AgentTestCase): - self.assertEqual(1, mock_host.call_count) - self.assertEqual(1, mock_port.call_count) - -+ @skip_if_predicate_true(lambda: os.environ.get('https_proxy') is not None, "Skip if proxy is defined") - @patch('azurelinuxagent.common.conf.get_httpproxy_port') - @patch('azurelinuxagent.common.conf.get_httpproxy_host') - def test_get_http_proxy_configuration_requires_host(self, mock_host, mock_port): diff -Nru waagent-2.2.34/debian/patches/osutil-debian.patch waagent-2.2.45/debian/patches/osutil-debian.patch --- waagent-2.2.34/debian/patches/osutil-debian.patch 2019-04-16 08:05:24.000000000 +0000 +++ waagent-2.2.45/debian/patches/osutil-debian.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,25 +0,0 @@ -From: Bastian Blank -Date: Wed, 24 Aug 2016 16:25:56 +0200 -Subject: Support Debian in osutil - ---- - azurelinuxagent/common/osutil/debian.py | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - -diff --git a/azurelinuxagent/common/osutil/debian.py b/azurelinuxagent/common/osutil/debian.py -index 9178814..fc0a631 100644 ---- a/azurelinuxagent/common/osutil/debian.py -+++ b/azurelinuxagent/common/osutil/debian.py -@@ -42,10 +42,10 @@ class DebianOSUtil(DefaultOSUtil): - return shellutil.run("systemctl --job-mode=ignore-dependencies try-reload-or-restart ssh", chk_err=False) - - def stop_agent_service(self): -- return shellutil.run("service azurelinuxagent stop", chk_err=False) -+ return shellutil.run("systemctl --job-mode=ignore-dependencies stop walinuxagent", chk_err=False) - - def start_agent_service(self): -- return shellutil.run("service azurelinuxagent start", chk_err=False) -+ return shellutil.run("systemctl --job-mode=ignore-dependencies start walinuxagent", chk_err=False) - - def start_network(self): - pass diff -Nru waagent-2.2.34/debian/patches/resourcedisk-filesystem.patch waagent-2.2.45/debian/patches/resourcedisk-filesystem.patch --- waagent-2.2.34/debian/patches/resourcedisk-filesystem.patch 2019-04-16 08:05:25.000000000 +0000 +++ waagent-2.2.45/debian/patches/resourcedisk-filesystem.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,155 +0,0 @@ -From: Bastian Blank -Date: Wed, 4 Jan 2017 18:07:20 +0100 -Subject: Fix filesystem detection - -Use blkid to detect filesystem information. ---- - azurelinuxagent/common/utils/shellutil.py | 1 - - azurelinuxagent/daemon/resourcedisk/default.py | 49 +++++--------------------- - tests/distro/test_resourceDisk.py | 22 ------------ - 3 files changed, 9 insertions(+), 63 deletions(-) - -diff --git a/azurelinuxagent/common/utils/shellutil.py b/azurelinuxagent/common/utils/shellutil.py -index 8b25b81..8cd4ca7 100644 ---- a/azurelinuxagent/common/utils/shellutil.py -+++ b/azurelinuxagent/common/utils/shellutil.py -@@ -84,7 +84,6 @@ def run_get_output(cmd, chk_err=True, log_cmd=True): - logger.verbose(u"Command: [{0}]", cmd) - try: - output = subprocess.check_output(cmd, -- stderr=subprocess.STDOUT, - shell=True) - output = ustr(output, - encoding='utf-8', -diff --git a/azurelinuxagent/daemon/resourcedisk/default.py b/azurelinuxagent/daemon/resourcedisk/default.py -index 0f0925d..ce1309c 100644 ---- a/azurelinuxagent/daemon/resourcedisk/default.py -+++ b/azurelinuxagent/daemon/resourcedisk/default.py -@@ -17,6 +17,7 @@ - - import os - import re -+import subprocess - import sys - import threading - from time import sleep -@@ -87,9 +88,8 @@ class ResourceDiskHandler(object): - logger.error("Failed to enable swap {0}", e) - - def reread_partition_table(self, device): -- if shellutil.run("sfdisk -R {0}".format(device), chk_err=False): -- shellutil.run("blockdev --rereadpt {0}".format(device), -- chk_err=False) -+ shellutil.run("blockdev --rereadpt {0}".format(device), -+ chk_err=False) - - def mount_resource_disk(self, mount_point): - device = self.osutil.device_for_ide_port(1) -@@ -116,7 +116,7 @@ class ResourceDiskHandler(object): - raise ResourceDiskError(msg=msg, inner=ose) - - logger.info("Examining partition table") -- ret = shellutil.run_get_output("parted {0} print".format(device)) -+ ret = shellutil.run_get_output("blkid -o value -s PTTYPE {0}".format(device)) - if ret[0]: - raise ResourceDiskError("Could not determine partition info for " - "{0}: {1}".format(device, ret[1])) -@@ -126,8 +126,9 @@ class ResourceDiskHandler(object): - force_option = 'f' - mkfs_string = "mkfs.{0} -{2} {1}".format(self.fs, partition, force_option) - -- if "gpt" in ret[1]: -+ if ret[1].strip() == "gpt": - logger.info("GPT detected, finding partitions") -+ ret = shellutil.run_get_output("parted {0} print".format(device)) - parts = [x for x in ret[1].split("\n") if - re.match("^\s*[0-9]+", x)] - logger.info("Found {0} GPT partition(s).", len(parts)) -@@ -144,17 +145,13 @@ class ResourceDiskHandler(object): - shellutil.run(mkfs_string) - else: - logger.info("GPT not detected, determining filesystem") -- ret = self.change_partition_type(suppress_message=True, option_str="{0} 1 -n".format(device)) -- ptype = ret[1].strip() -- if ptype == "7" and self.fs != "ntfs": -+ ret = shellutil.run_get_output("blkid -o value -s TYPE {0}".format(partition)) -+ if ret[1].strip() == 'ntfs' and self.fs != 'ntfs': - logger.info("The partition is formatted with ntfs, updating " - "partition type to 83") -- self.change_partition_type(suppress_message=False, option_str="{0} 1 83".format(device)) -- self.reread_partition_table(device) -+ subprocess.call(['sfdisk', '-c', '-f', device, '1', '83'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - logger.info("Format partition [{0}]", mkfs_string) - shellutil.run(mkfs_string) -- else: -- logger.info("The partition type is {0}", ptype) - - mount_options = conf.get_resourcedisk_mountoptions() - mount_string = self.get_mount_string(mount_options, -@@ -208,34 +205,6 @@ class ResourceDiskHandler(object): - self.fs) - return mount_point - -- def change_partition_type(self, suppress_message, option_str): -- """ -- use sfdisk to change partition type. -- First try with --part-type; if fails, fall back to -c -- """ -- -- command_to_use = '--part-type' -- input = "sfdisk {0} {1} {2}".format(command_to_use, '-f' if suppress_message else '', option_str) -- err_code, output = shellutil.run_get_output(input, chk_err=False, log_cmd=True) -- -- # fall back to -c -- if err_code != 0: -- logger.info("sfdisk with --part-type failed [{0}], retrying with -c", err_code) -- command_to_use = '-c' -- input = "sfdisk {0} {1} {2}".format(command_to_use, '-f' if suppress_message else '', option_str) -- err_code, output = shellutil.run_get_output(input, log_cmd=True) -- -- if err_code == 0: -- logger.info('{0} succeeded', -- input) -- else: -- logger.error('{0} failed [{1}: {2}]', -- input, -- err_code, -- output) -- -- return err_code, output -- - @staticmethod - def get_mount_string(mount_options, partition, mount_point): - if mount_options is not None: -diff --git a/tests/distro/test_resourceDisk.py b/tests/distro/test_resourceDisk.py -index d2ce6e1..4c185ee 100644 ---- a/tests/distro/test_resourceDisk.py -+++ b/tests/distro/test_resourceDisk.py -@@ -84,27 +84,5 @@ class TestResourceDisk(AgentTestCase): - assert "dd if" in run_patch.call_args_list[0][0][0] - - -- def test_change_partition_type(self): -- resource_handler = get_resourcedisk_handler() -- # test when sfdisk --part-type does not exist -- with patch.object(shellutil, "run_get_output", -- side_effect=[[1, ''], [0, '']]) as run_patch: -- resource_handler.change_partition_type(suppress_message=True, option_str='') -- -- # assert -- assert run_patch.call_count == 2 -- assert "sfdisk --part-type" in run_patch.call_args_list[0][0][0] -- assert "sfdisk -c" in run_patch.call_args_list[1][0][0] -- -- # test when sfdisk --part-type exists -- with patch.object(shellutil, "run_get_output", -- side_effect=[[0, '']]) as run_patch: -- resource_handler.change_partition_type(suppress_message=True, option_str='') -- -- # assert -- assert run_patch.call_count == 1 -- assert "sfdisk --part-type" in run_patch.call_args_list[0][0][0] -- -- - if __name__ == '__main__': - unittest.main() diff -Nru waagent-2.2.34/debian/patches/series waagent-2.2.45/debian/patches/series --- waagent-2.2.34/debian/patches/series 2019-04-16 08:05:25.000000000 +0000 +++ waagent-2.2.45/debian/patches/series 2020-04-27 06:48:47.000000000 +0000 @@ -1,10 +1 @@ -osutil-debian.patch -user-shell.patch -agent-command-provision.patch -agent-command-resourcedisk.patch -resourcedisk-filesystem.patch -disable-bytecode-exthandler.patch -entry-points.patch -disable-auto-update.patch -ignore-tests.patch -cve-2019-0804.patch +debian-changes diff -Nru waagent-2.2.34/debian/patches/user-shell.patch waagent-2.2.45/debian/patches/user-shell.patch --- waagent-2.2.34/debian/patches/user-shell.patch 2019-04-16 08:05:24.000000000 +0000 +++ waagent-2.2.45/debian/patches/user-shell.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,24 +0,0 @@ -From: Bastian Blank -Date: Wed, 4 Jan 2017 16:07:33 +0100 -Subject: Set shell of created user to /bin/bash - ---- - azurelinuxagent/common/osutil/default.py | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - -diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py -index f33e5ad..8489479 100644 ---- a/azurelinuxagent/common/osutil/default.py -+++ b/azurelinuxagent/common/osutil/default.py -@@ -403,9 +403,9 @@ class DefaultOSUtil(object): - return - - if expiration is not None: -- cmd = "useradd -m {0} -e {1}".format(username, expiration) -+ cmd = "useradd -m {0} -s /bin/bash -e {1}".format(username, expiration) - else: -- cmd = "useradd -m {0}".format(username) -+ cmd = "useradd -m {0} -s /bin/bash".format(username) - - if comment is not None: - cmd += " -c {0}".format(comment) diff -Nru waagent-2.2.34/debian/rules waagent-2.2.45/debian/rules --- waagent-2.2.34/debian/rules 2019-04-16 08:05:24.000000000 +0000 +++ waagent-2.2.45/debian/rules 2020-04-27 06:48:47.000000000 +0000 @@ -2,11 +2,31 @@ # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 +include /usr/share/dpkg/default.mk + export PYBUILD_INSTALL_ARGS=--install-scripts /usr/sbin %: dh $@ --with python3,systemd --buildsystem pybuild +git-source: TAR_ORIG=$(DEB_SOURCE)_$(DEB_VERSION_UPSTREAM).orig.tar.xz +git-source: TAR_DEBIAN=$(DEB_SOURCE)_$(DEB_VERSION_UPSTREAM_REVISION).debian.tar.xz +git-source: CHANGES=$(DEB_SOURCE)_$(DEB_VERSION_UPSTREAM_REVISION)_source.changes +git-source: + pristine-lfs checkout -o .. $(TAR_ORIG) + mkdir -p debian/patches debian/source + git diff v$(DEB_VERSION_UPSTREAM) -- . ':!debian' > debian/patches/debian-changes + echo debian-changes > debian/patches/series + echo '3.0 (quilt)' > debian/source/format + tar -caf ../$(TAR_DEBIAN) debian + dpkg-source --build --format='3.0 (custom)' --target-format='3.0 (quilt)' . $(TAR_ORIG) $(TAR_DEBIAN) + dpkg-genchanges -S > ../$(CHANGES) + +git-orig: TAR_ORIG=$(DEB_SOURCE)_$(DEB_VERSION_UPSTREAM).orig.tar.xz +git-orig: + git archive --format tar --prefix $(DEB_SOURCE)-$(DEB_VERSION_UPSTREAM)/ v$(DEB_VERSION_UPSTREAM) | xz > ../$(TAR_ORIG) + pristine-lfs commit ../$(TAR_ORIG) + override_dh_auto_install: dh_auto_install rm -rf debian/waagent/usr/lib/python3*/dist-packages/tests/ diff -Nru waagent-2.2.34/init/openwrt/waagent waagent-2.2.45/init/openwrt/waagent --- waagent-2.2.34/init/openwrt/waagent 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/init/openwrt/waagent 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,60 @@ +#!/bin/sh /etc/rc.common +# Init file for AzureLinuxAgent. +# +# Copyright 2018 Microsoft Corporation +# Copyright 2018 Sonus Networks, Inc. (d.b.a. Ribbon Communications Operating Company) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +# description: AzureLinuxAgent +# + +START=60 +STOP=80 + + +RETVAL=0 +FriendlyName="AzureLinuxAgent" +WAZD_BIN=/usr/sbin/waagent +WAZD_CONF=/etc/waagent.conf +WAZD_PIDFILE=/var/run/waagent.pid + +test -x "$WAZD_BIN" || { echo "$WAZD_BIN not installed"; exit 5; } +test -e "$WAZD_CONF" || { echo "$WAZD_CONF not found"; exit 6; } + +start() +{ + echo -n "Starting $FriendlyName: " + $WAZD_BIN -start + RETVAL=$? + echo + return $RETVAL +} + +stop() +{ + echo -n "Stopping $FriendlyName: " + if [ -f "$WAZD_PIDFILE" ] + then + kill -9 `cat ${WAZD_PIDFILE}` + rm ${WAZD_PIDFILE} + RETVAL=$? + echo + return $RETVAL + else + echo "$FriendlyName already stopped." + fi +} + diff -Nru waagent-2.2.34/requirements.txt waagent-2.2.45/requirements.txt --- waagent-2.2.34/requirements.txt 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/requirements.txt 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,2 @@ +distro; python_version >= '3.8' +pyasn1 \ No newline at end of file diff -Nru waagent-2.2.34/setup.py waagent-2.2.45/setup.py --- waagent-2.2.34/setup.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/setup.py 2019-11-07 00:36:56.000000000 +0000 @@ -25,7 +25,8 @@ from azurelinuxagent.common.osutil import get_osutil import setuptools from setuptools import find_packages -from setuptools.command.install import install as _install +from setuptools.command.install import install as _install +import subprocess import sys root_dir = os.path.dirname(os.path.abspath(__file__)) @@ -59,11 +60,13 @@ data_files.append((dest, src)) -def set_freebsd_rc_files(data_files, dest="/etc/rc.d/", src=["init/freebsd/waagent"]): +def set_freebsd_rc_files(data_files, dest="/etc/rc.d/", + src=["init/freebsd/waagent"]): data_files.append((dest, src)) -def set_openbsd_rc_files(data_files, dest="/etc/rc.d/", src=["init/openbsd/waagent"]): +def set_openbsd_rc_files(data_files, dest="/etc/rc.d/", + src=["init/openbsd/waagent"]): data_files.append((dest, src)) @@ -92,7 +95,6 @@ if version.startswith("7.1"): # TODO this is a mitigation to systemctl bug on 7.1 set_sysv_files(data_files) - elif name == 'arch': set_bin_files(data_files, dest="/usr/bin") set_conf_files(data_files, src=["config/arch/waagent.conf"]) @@ -138,7 +140,7 @@ set_udev_files(data_files) if fullname == 'SUSE Linux Enterprise Server' and \ version.startswith('11') or \ - fullname == 'openSUSE' and version.startswith( + fullname == 'openSUSE' and version.startswith( '13.1'): set_sysv_files(data_files, dest='/etc/init.d', src=["init/suse/waagent"]) @@ -158,6 +160,8 @@ set_conf_files(data_files, src=["config/debian/waagent.conf"]) set_logrotate_files(data_files) set_udev_files(data_files, dest="/lib/udev/rules.d") + if debian_has_systemd(): + set_systemd_files(data_files) elif name == 'iosxe': set_bin_files(data_files) set_conf_files(data_files, src=["config/iosxe/waagent.conf"]) @@ -167,6 +171,11 @@ if version.startswith("7.1"): # TODO this is a mitigation to systemctl bug on 7.1 set_sysv_files(data_files) + elif name == 'openwrt': + set_bin_files(data_files) + set_conf_files(data_files) + set_logrotate_files(data_files) + set_sysv_files(data_files, dest='/etc/init.d', src=["init/openwrt/waagent"]) else: # Use default setting set_bin_files(data_files) @@ -177,6 +186,14 @@ return data_files +def debian_has_systemd(): + try: + return subprocess.check_output( + ['cat', '/proc/1/comm']).strip() == 'systemd' + except subprocess.CalledProcessError: + return False + + class install(_install): user_options = _install.user_options + [ ('lnx-distro=', None, 'target Linux distribution'), @@ -212,6 +229,7 @@ osutil.stop_agent_service() osutil.start_agent_service() + # Note to packagers and users from source. # In version 3.5 of Python distribution information handling in the platform # module was deprecated. Depending on the Linux distribution the @@ -221,6 +239,11 @@ if float(sys.version[:3]) >= 3.7: requires = ['distro'] +modules = [] + +if "bdist_egg" in sys.argv: + modules.append("__main__") + setuptools.setup( name=AGENT_NAME, version=AGENT_VERSION, @@ -231,6 +254,7 @@ url='https://github.com/Azure/WALinuxAgent', license='Apache License Version 2.0', packages=find_packages(exclude=["tests*"]), + py_modules=modules, install_requires=requires, cmdclass={ 'install': install diff -Nru waagent-2.2.34/test-requirements.txt waagent-2.2.45/test-requirements.txt --- waagent-2.2.34/test-requirements.txt 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/test-requirements.txt 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,5 @@ +codecov +coverage +flake8; python_version >= '2.7' +mock +nose \ No newline at end of file diff -Nru waagent-2.2.34/tests/common/dhcp/test_dhcp.py waagent-2.2.45/tests/common/dhcp/test_dhcp.py --- waagent-2.2.34/tests/common/dhcp/test_dhcp.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/common/dhcp/test_dhcp.py 2019-11-07 00:36:56.000000000 +0000 @@ -22,6 +22,13 @@ class TestDHCP(AgentTestCase): + + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + def test_wireserver_route_exists(self): # setup dhcp_handler = dhcp.get_dhcp_handler() diff -Nru waagent-2.2.34/tests/common/osutil/test_alpine.py waagent-2.2.45/tests/common/osutil/test_alpine.py --- waagent-2.2.34/tests/common/osutil/test_alpine.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/osutil/test_alpine.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,34 @@ +# Copyright 2019 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +from azurelinuxagent.common.osutil.alpine import AlpineOSUtil +from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids +from tests.tools import * + + +class TestAlpineOSUtil(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + + def test_get_dhcp_pid_should_return_a_list_of_pids(self): + osutil_get_dhcp_pid_should_return_a_list_of_pids(self, AlpineOSUtil()) + + +if __name__ == '__main__': + unittest.main() diff -Nru waagent-2.2.34/tests/common/osutil/test_arch.py waagent-2.2.45/tests/common/osutil/test_arch.py --- waagent-2.2.34/tests/common/osutil/test_arch.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/osutil/test_arch.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,34 @@ +# Copyright 2019 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +from azurelinuxagent.common.osutil.arch import ArchUtil +from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids +from tests.tools import * + + +class TestArchUtil(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + + def test_get_dhcp_pid_should_return_a_list_of_pids(self): + osutil_get_dhcp_pid_should_return_a_list_of_pids(self, ArchUtil()) + + +if __name__ == '__main__': + unittest.main() diff -Nru waagent-2.2.34/tests/common/osutil/test_bigip.py waagent-2.2.45/tests/common/osutil/test_bigip.py --- waagent-2.2.34/tests/common/osutil/test_bigip.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/common/osutil/test_bigip.py 2019-11-07 00:36:56.000000000 +0000 @@ -15,15 +15,15 @@ # Requires Python 2.6+ and Openssl 1.0+ # -import os import socket -import time import azurelinuxagent.common.osutil.bigip as osutil import azurelinuxagent.common.osutil.default as default import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.exception import OSUtilError +from azurelinuxagent.common.osutil.bigip import BigIpOSUtil +from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids from tests.tools import * @@ -69,19 +69,6 @@ self.assertEqual(args[0].call_count, 1) -class TestBigIpOSUtil_get_dhcp_pid(AgentTestCase): - - @patch.object(shellutil, "run_get_output", return_value=(0, 8623)) - def test_success(self, *args): - result = osutil.BigIpOSUtil.get_dhcp_pid(osutil.BigIpOSUtil()) - self.assertEqual(result, 8623) - - @patch.object(shellutil, "run_get_output", return_value=(1, 'foo')) - def test_failure(self, *args): - result = osutil.BigIpOSUtil.get_dhcp_pid(osutil.BigIpOSUtil()) - self.assertEqual(result, None) - - class TestBigIpOSUtil_useradd(AgentTestCase): @patch.object(osutil.BigIpOSUtil, 'get_userentry', return_value=None) @@ -319,5 +306,16 @@ self.assertEqual(args[2].call_count, 0) +class TestBigIpOSUtil(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + + def test_get_dhcp_pid_should_return_a_list_of_pids(self): + osutil_get_dhcp_pid_should_return_a_list_of_pids(self, BigIpOSUtil()) + + if __name__ == '__main__': unittest.main() \ No newline at end of file diff -Nru waagent-2.2.34/tests/common/osutil/test_clearlinux.py waagent-2.2.45/tests/common/osutil/test_clearlinux.py --- waagent-2.2.34/tests/common/osutil/test_clearlinux.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/osutil/test_clearlinux.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,34 @@ +# Copyright 2019 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +from azurelinuxagent.common.osutil.clearlinux import ClearLinuxUtil +from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids +from tests.tools import * + + +class TestClearLinuxUtil(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + + def test_get_dhcp_pid_should_return_a_list_of_pids(self): + osutil_get_dhcp_pid_should_return_a_list_of_pids(self, ClearLinuxUtil()) + + +if __name__ == '__main__': + unittest.main() diff -Nru waagent-2.2.34/tests/common/osutil/test_coreos.py waagent-2.2.45/tests/common/osutil/test_coreos.py --- waagent-2.2.34/tests/common/osutil/test_coreos.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/osutil/test_coreos.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,34 @@ +# Copyright 2019 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +from azurelinuxagent.common.osutil.coreos import CoreOSUtil +from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids +from tests.tools import * + + +class TestAlpineOSUtil(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + + def test_get_dhcp_pid_should_return_a_list_of_pids(self): + osutil_get_dhcp_pid_should_return_a_list_of_pids(self, CoreOSUtil()) + + +if __name__ == '__main__': + unittest.main() diff -Nru waagent-2.2.34/tests/common/osutil/test_default.py waagent-2.2.45/tests/common/osutil/test_default.py --- waagent-2.2.34/tests/common/osutil/test_default.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/common/osutil/test_default.py 2019-11-07 00:36:56.000000000 +0000 @@ -19,6 +19,7 @@ import glob import mock import traceback +import re import azurelinuxagent.common.osutil.default as osutil import azurelinuxagent.common.utils.shellutil as shellutil @@ -31,15 +32,19 @@ actual_get_proc_net_route = 'azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_net_route' + def fake_is_loopback(_, iface): return iface.startswith('lo') -def running_under_travis(): - return 'TRAVIS' in os.environ and os.environ['TRAVIS'] == 'true' +class TestOSUtil(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) -class TestOSUtil(AgentTestCase): def test_restart(self): # setup retries = 3 @@ -506,6 +511,9 @@ self.assertEqual( "D0DF4C54-4ECB-4A4B-9954-5BDF3ED5C3B8", util._correct_instance_id("544CDFD0-CB4E-4B4A-9954-5BDF3ED5C3B8")) + self.assertEqual( + "d0df4c54-4ecb-4a4b-9954-5bdf3ed5c3b8", + util._correct_instance_id("544cdfd0-cb4e-4b4a-9954-5bdf3ed5c3b8")) @patch('os.path.isfile', return_value=True) @patch('azurelinuxagent.common.utils.fileutil.read_file', @@ -562,6 +570,10 @@ def test_is_current_instance_id_from_file(self, mock_read, mock_isfile): util = osutil.DefaultOSUtil() + mock_read.return_value = "11111111-2222-3333-4444-556677889900" + self.assertFalse(util.is_current_instance_id( + "B9F3C233-9913-9F42-8EB3-BA656DF32502")) + mock_read.return_value = "B9F3C233-9913-9F42-8EB3-BA656DF32502" self.assertTrue(util.is_current_instance_id( "B9F3C233-9913-9F42-8EB3-BA656DF32502")) @@ -570,6 +582,14 @@ self.assertTrue(util.is_current_instance_id( "B9F3C233-9913-9F42-8EB3-BA656DF32502")) + mock_read.return_value = "b9f3c233-9913-9f42-8eb3-ba656df32502" + self.assertTrue(util.is_current_instance_id( + "B9F3C233-9913-9F42-8EB3-BA656DF32502")) + + mock_read.return_value = "33c2f3b9-1399-429f-8eb3-ba656df32502" + self.assertTrue(util.is_current_instance_id( + "B9F3C233-9913-9F42-8EB3-BA656DF32502")) + @patch('os.path.isfile', return_value=False) @patch('azurelinuxagent.common.utils.shellutil.run_get_output') def test_is_current_instance_id_from_dmidecode(self, mock_shell, mock_isfile): @@ -892,6 +912,74 @@ another_state[name].add_ipv4("xyzzy") self.assertNotEqual(state, another_state) + def test_get_dhcp_pid_should_return_a_list_of_pids(self): + osutil_get_dhcp_pid_should_return_a_list_of_pids(self, osutil.DefaultOSUtil()) + + def test_get_dhcp_pid_should_return_an_empty_list_when_the_dhcp_client_is_not_running(self): + original_run_command = shellutil.run_command + + def mock_run_command(cmd): + return original_run_command(["pidof", "non-existing-process"]) + + with patch("azurelinuxagent.common.utils.shellutil.run_command", side_effect=mock_run_command): + pid_list = osutil.DefaultOSUtil().get_dhcp_pid() + + self.assertTrue(len(pid_list) == 0, "the return value is not an empty list: {0}".format(pid_list)) + + @patch('os.walk', return_value=[('host3/target3:0:1/3:0:1:0/block', ['sdb'], [])]) + @patch('azurelinuxagent.common.utils.fileutil.read_file', return_value='{00000000-0001-8899-0000-000000000000}') + @patch('os.listdir', return_value=['00000000-0001-8899-0000-000000000000']) + @patch('os.path.exists', return_value=True) + def test_device_for_ide_port_gen1_success( + self, + os_path_exists, + os_listdir, + fileutil_read_file, + os_walk): + dev = osutil.DefaultOSUtil().device_for_ide_port(1) + self.assertEqual(dev, 'sdb', 'The returned device should be the resource disk') + + @patch('os.walk', return_value=[('host0/target0:0:0/0:0:0:1/block', ['sdb'], [])]) + @patch('azurelinuxagent.common.utils.fileutil.read_file', return_value='{f8b3781a-1e82-4818-a1c3-63d806ec15bb}') + @patch('os.listdir', return_value=['f8b3781a-1e82-4818-a1c3-63d806ec15bb']) + @patch('os.path.exists', return_value=True) + def test_device_for_ide_port_gen2_success( + self, + os_path_exists, + os_listdir, + fileutil_read_file, + os_walk): + dev = osutil.DefaultOSUtil().device_for_ide_port(1) + self.assertEqual(dev, 'sdb', 'The returned device should be the resource disk') + + @patch('os.listdir', return_value=['00000000-0000-0000-0000-000000000000']) + @patch('os.path.exists', return_value=True) + def test_device_for_ide_port_none( + self, + os_path_exists, + os_listdir): + dev = osutil.DefaultOSUtil().device_for_ide_port(1) + self.assertIsNone(dev, 'None should be returned if no resource disk found') + +def osutil_get_dhcp_pid_should_return_a_list_of_pids(test_instance, osutil_instance): + """ + This is a very basic test for osutil.get_dhcp_pid. It is simply meant to exercise the implementation of that method + in case there are any basic errors, such as a typos, etc. The test does not verify that the implementation returns + the PID for the actual dhcp client; in fact, it uses a mock that invokes pidof to return the PID of an arbitrary + process (the pidof process itself). Most implementations of get_dhcp_pid use pidof with the appropriate name for + the dhcp client. + The test is defined as a global function to make it easily accessible from the test suites for each distro. + """ + original_run_command = shellutil.run_command + + def mock_run_command(cmd): + return original_run_command(["pidof", "pidof"]) + + with patch("azurelinuxagent.common.utils.shellutil.run_command", side_effect=mock_run_command): + pid = osutil_instance.get_dhcp_pid() + + test_instance.assertTrue(len(pid) != 0, "get_dhcp_pid did not return a PID") + if __name__ == '__main__': unittest.main() diff -Nru waagent-2.2.34/tests/common/osutil/test_default_osutil.py waagent-2.2.45/tests/common/osutil/test_default_osutil.py --- waagent-2.2.34/tests/common/osutil/test_default_osutil.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/osutil/test_default_osutil.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,182 @@ +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.4+ and Openssl 1.0+ +# + +from azurelinuxagent.common.osutil.default import DefaultOSUtil, shellutil +from tests.tools import * + + +class DefaultOsUtilTestCase(AgentTestCase): + + def setUp(self): + AgentTestCase.setUp(self) + self.cgroups_file_system_root = os.path.join(self.tmp_dir, "cgroups") + self.mock_base_cgroups = patch("azurelinuxagent.common.osutil.default.BASE_CGROUPS", self.cgroups_file_system_root) + self.mock_base_cgroups.start() + + def tearDown(self): + self.mock_base_cgroups.stop() + + @staticmethod + def _get_mount_commands(mock): + mount_commands = '' + for call_args in mock.call_args_list: + args, kwargs = call_args + mount_commands += ';' + args[0] + return mount_commands + + def test_mount_cgroups_should_mount_the_cpu_and_memory_controllers(self): + # the mount command requires root privileges; make it a no op and check only for file existence + original_run_get_output = shellutil.run_get_output + + def mock_run_get_output(cmd, *args, **kwargs): + if cmd.startswith('mount '): + return 0, None + return original_run_get_output(cmd, *args, **kwargs) + + with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output) as patch_run_get_output: + DefaultOSUtil().mount_cgroups() + + # the directories for the controllers should have been created + for controller in ['cpu', 'memory', 'cpuacct', 'cpu,cpuacct']: + directory = os.path.join(self.cgroups_file_system_root, controller) + self.assertTrue(os.path.exists(directory), "A directory for controller {0} was not created".format(controller)) + + # the cgroup filesystem and the cpu and memory controllers should have been mounted + mount_commands = DefaultOsUtilTestCase._get_mount_commands(patch_run_get_output) + + self.assertRegex(mount_commands, ';mount.* cgroup_root ', 'The cgroups file system was not mounted') + self.assertRegex(mount_commands, ';mount.* cpu,cpuacct ', 'The cpu controller was not mounted') + self.assertRegex(mount_commands, ';mount.* memory ', 'The memory controller was not mounted') + + def test_mount_cgroups_should_not_mount_the_cgroups_file_system_when_it_already_exists(self): + os.mkdir(self.cgroups_file_system_root) + + original_run_get_output = shellutil.run_get_output + + def mock_run_get_output(cmd, *args, **kwargs): + if cmd.startswith('mount '): + return 0, None + return original_run_get_output(cmd, *args, **kwargs) + + with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output) as patch_run_get_output: + DefaultOSUtil().mount_cgroups() + + mount_commands = DefaultOsUtilTestCase._get_mount_commands(patch_run_get_output) + + self.assertNotIn('cgroup_root', mount_commands, 'The cgroups file system should not have been mounted') + self.assertRegex(mount_commands, ';mount.* cpu,cpuacct ', 'The cpu controller was not mounted') + self.assertRegex(mount_commands, ';mount.* memory ', 'The memory controller was not mounted') + + def test_mount_cgroups_should_not_mount_cgroup_controllers_when_they_already_exist(self): + os.mkdir(self.cgroups_file_system_root) + os.mkdir(os.path.join(self.cgroups_file_system_root, 'cpu,cpuacct')) + os.mkdir(os.path.join(self.cgroups_file_system_root, 'memory')) + + original_run_get_output = shellutil.run_get_output + + def mock_run_get_output(cmd, *args, **kwargs): + if cmd.startswith('mount '): + return 0, None + return original_run_get_output(cmd, *args, **kwargs) + + with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output) as patch_run_get_output: + DefaultOSUtil().mount_cgroups() + + mount_commands = DefaultOsUtilTestCase._get_mount_commands(patch_run_get_output) + + self.assertNotIn('cgroup_root', mount_commands, 'The cgroups file system should not have been mounted') + self.assertNotIn('cpu,cpuacct', mount_commands, 'The cpu controller should not have been mounted') + self.assertNotIn('memory', mount_commands, 'The memory controller should not have been mounted') + + def test_mount_cgroups_should_handle_errors_when_mounting_an_individual_controller(self): + original_run_get_output = shellutil.run_get_output + + def mock_run_get_output(cmd, *args, **kwargs): + if cmd.startswith('mount '): + if 'memory' in cmd: + raise Exception('A test exception mounting the memory controller') + return 0, None + return original_run_get_output(cmd, *args, **kwargs) + + with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output) as patch_run_get_output: + with patch("azurelinuxagent.common.cgroupconfigurator.logger.warn") as mock_logger_warn: + DefaultOSUtil().mount_cgroups() + + # the cgroup filesystem and the cpu controller should still have been mounted + mount_commands = DefaultOsUtilTestCase._get_mount_commands(patch_run_get_output) + + self.assertRegex(mount_commands, ';mount.* cgroup_root ', 'The cgroups file system was not mounted') + self.assertRegex(mount_commands, ';mount.* cpu,cpuacct ', 'The cpu controller was not mounted') + + # A warning should have been logged for the memory controller + args, kwargs = mock_logger_warn.call_args + self.assertIn('A test exception mounting the memory controller', args) + + def test_mount_cgroups_should_raise_when_the_cgroups_filesystem_fails_to_mount(self): + original_run_get_output = shellutil.run_get_output + + def mock_run_get_output(cmd, *args, **kwargs): + if cmd.startswith('mount '): + if 'cgroup_root' in cmd: + raise Exception('A test exception mounting the cgroups file system') + return 0, None + return original_run_get_output(cmd, *args, **kwargs) + + with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output) as patch_run_get_output: + with self.assertRaises(Exception) as context_manager: + DefaultOSUtil().mount_cgroups() + + self.assertRegex(str(context_manager.exception), 'A test exception mounting the cgroups file system') + + mount_commands = DefaultOsUtilTestCase._get_mount_commands(patch_run_get_output) + self.assertNotIn('memory', mount_commands, 'The memory controller should not have been mounted') + self.assertNotIn('cpu', mount_commands, 'The cpu controller should not have been mounted') + + def test_mount_cgroups_should_raise_when_all_controllers_fail_to_mount(self): + original_run_get_output = shellutil.run_get_output + + def mock_run_get_output(cmd, *args, **kwargs): + if cmd.startswith('mount '): + if 'memory' in cmd or 'cpu,cpuacct' in cmd: + raise Exception('A test exception mounting a cgroup controller') + return 0, None + return original_run_get_output(cmd, *args, **kwargs) + + with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output): + with self.assertRaises(Exception) as context_manager: + DefaultOSUtil().mount_cgroups() + + self.assertRegex(str(context_manager.exception), 'A test exception mounting a cgroup controller') + + def test_mount_cgroups_should_not_create_symbolic_links_when_the_cpu_controller_fails_to_mount(self): + original_run_get_output = shellutil.run_get_output + + def mock_run_get_output(cmd, *args, **kwargs): + if cmd.startswith('mount '): + if 'cpu,cpuacct' in cmd: + raise Exception('A test exception mounting the cpu controller') + return 0, None + return original_run_get_output(cmd, *args, **kwargs) + + with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output): + with patch("azurelinuxagent.common.osutil.default.os.symlink") as patch_symlink: + DefaultOSUtil().mount_cgroups() + + self.assertEquals(patch_symlink.call_count, 0, 'A symbolic link should not have been created') + + def test_default_service_name(self): + self.assertEquals(DefaultOSUtil().get_service_name(), "waagent") diff -Nru waagent-2.2.34/tests/common/osutil/test_factory.py waagent-2.2.45/tests/common/osutil/test_factory.py --- waagent-2.2.34/tests/common/osutil/test_factory.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/osutil/test_factory.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,273 @@ +# Copyright 2019 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.4+ and Openssl 1.0+ +# + +from azurelinuxagent.common.osutil.factory import _get_osutil +from azurelinuxagent.common.osutil.default import DefaultOSUtil +from azurelinuxagent.common.osutil.arch import ArchUtil +from azurelinuxagent.common.osutil.clearlinux import ClearLinuxUtil +from azurelinuxagent.common.osutil.coreos import CoreOSUtil +from azurelinuxagent.common.osutil.debian import DebianOSBaseUtil, DebianOSModernUtil +from azurelinuxagent.common.osutil.freebsd import FreeBSDOSUtil +from azurelinuxagent.common.osutil.openbsd import OpenBSDOSUtil +from azurelinuxagent.common.osutil.redhat import RedhatOSUtil, Redhat6xOSUtil +from azurelinuxagent.common.osutil.suse import SUSEOSUtil, SUSE11OSUtil +from azurelinuxagent.common.osutil.ubuntu import UbuntuOSUtil, Ubuntu12OSUtil, Ubuntu14OSUtil, \ + UbuntuSnappyOSUtil, Ubuntu16OSUtil, Ubuntu18OSUtil +from azurelinuxagent.common.osutil.alpine import AlpineOSUtil +from azurelinuxagent.common.osutil.bigip import BigIpOSUtil +from azurelinuxagent.common.osutil.gaia import GaiaOSUtil +from azurelinuxagent.common.osutil.iosxe import IosxeOSUtil +from azurelinuxagent.common.osutil.openwrt import OpenWRTOSUtil +from tests.tools import * + + +class TestOsUtilFactory(AgentTestCase): + + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + + @patch("azurelinuxagent.common.logger.warn") + def test_get_osutil_it_should_return_default(self, patch_logger): + ret = _get_osutil(distro_name="", + distro_code_name="", + distro_version="", + distro_full_name="") + self.assertTrue(type(ret) == DefaultOSUtil) + self.assertEquals(patch_logger.call_count, 1) + self.assertEquals(ret.get_service_name(), "waagent") + + def test_get_osutil_it_should_return_ubuntu(self): + ret = _get_osutil(distro_name="ubuntu", + distro_code_name="", + distro_version="10.04", + distro_full_name="") + self.assertTrue(type(ret) == UbuntuOSUtil) + self.assertEquals(ret.get_service_name(), "walinuxagent") + + ret = _get_osutil(distro_name="ubuntu", + distro_code_name="", + distro_version="12.04", + distro_full_name="") + self.assertTrue(type(ret) == Ubuntu12OSUtil) + self.assertEquals(ret.get_service_name(), "walinuxagent") + + ret = _get_osutil(distro_name="ubuntu", + distro_code_name="trusty", + distro_version="14.04", + distro_full_name="") + self.assertTrue(type(ret) == Ubuntu14OSUtil) + self.assertEquals(ret.get_service_name(), "walinuxagent") + + ret = _get_osutil(distro_name="ubuntu", + distro_code_name="xenial", + distro_version="16.04", + distro_full_name="") + self.assertTrue(type(ret) == Ubuntu16OSUtil) + self.assertEquals(ret.get_service_name(), "walinuxagent") + + ret = _get_osutil(distro_name="ubuntu", + distro_code_name="", + distro_version="18.04", + distro_full_name="") + self.assertTrue(type(ret) == Ubuntu18OSUtil) + self.assertEquals(ret.get_service_name(), "walinuxagent") + + ret = _get_osutil(distro_name="ubuntu", + distro_code_name="", + distro_version="10.04", + distro_full_name="Snappy Ubuntu Core") + self.assertTrue(type(ret) == UbuntuSnappyOSUtil) + self.assertEquals(ret.get_service_name(), "walinuxagent") + + def test_get_osutil_it_should_return_arch(self): + ret = _get_osutil(distro_name="arch", + distro_code_name="", + distro_version="", + distro_full_name="") + self.assertTrue(type(ret) == ArchUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + def test_get_osutil_it_should_return_clear_linux(self): + ret = _get_osutil(distro_name="clear linux", + distro_code_name="", + distro_version="", + distro_full_name="Clear Linux") + self.assertTrue(type(ret) == ClearLinuxUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + def test_get_osutil_it_should_return_alpine(self): + ret = _get_osutil(distro_name="alpine", + distro_code_name="", + distro_version="", + distro_full_name="") + self.assertTrue(type(ret) == AlpineOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + def test_get_osutil_it_should_return_kali(self): + ret = _get_osutil(distro_name="kali", + distro_code_name="", + distro_version="", + distro_full_name="") + self.assertTrue(type(ret) == DebianOSBaseUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + def test_get_osutil_it_should_return_coreos(self): + ret = _get_osutil(distro_name="coreos", + distro_code_name="", + distro_version="", + distro_full_name="") + self.assertTrue(type(ret) == CoreOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + def test_get_osutil_it_should_return_suse(self): + ret = _get_osutil(distro_name="suse", + distro_code_name="", + distro_version="10", + distro_full_name="") + self.assertTrue(type(ret) == SUSEOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + ret = _get_osutil(distro_name="suse", + distro_code_name="", + distro_full_name="SUSE Linux Enterprise Server", + distro_version="11") + self.assertTrue(type(ret) == SUSE11OSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + ret = _get_osutil(distro_name="suse", + distro_code_name="", + distro_full_name="openSUSE", + distro_version="12") + self.assertTrue(type(ret) == SUSE11OSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + def test_get_osutil_it_should_return_debian(self): + ret = _get_osutil(distro_name="debian", + distro_code_name="", + distro_full_name="", + distro_version="7") + self.assertTrue(type(ret) == DebianOSBaseUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + ret = _get_osutil(distro_name="debian", + distro_code_name="", + distro_full_name="", + distro_version="8") + self.assertTrue(type(ret) == DebianOSModernUtil) + self.assertEquals(ret.get_service_name(), "walinuxagent") + + def test_get_osutil_it_should_return_redhat(self): + ret = _get_osutil(distro_name="redhat", + distro_code_name="", + distro_full_name="", + distro_version="6") + self.assertTrue(type(ret) == Redhat6xOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + ret = _get_osutil(distro_name="centos", + distro_code_name="", + distro_full_name="", + distro_version="6") + self.assertTrue(type(ret) == Redhat6xOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + ret = _get_osutil(distro_name="oracle", + distro_code_name="", + distro_full_name="", + distro_version="6") + self.assertTrue(type(ret) == Redhat6xOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + ret = _get_osutil(distro_name="redhat", + distro_code_name="", + distro_full_name="", + distro_version="7") + self.assertTrue(type(ret) == RedhatOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + ret = _get_osutil(distro_name="centos", + distro_code_name="", + distro_full_name="", + distro_version="7") + self.assertTrue(type(ret) == RedhatOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + ret = _get_osutil(distro_name="oracle", + distro_code_name="", + distro_full_name="", + distro_version="7") + self.assertTrue(type(ret) == RedhatOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + def test_get_osutil_it_should_return_euleros(self): + ret = _get_osutil(distro_name="euleros", + distro_code_name="", + distro_version="", + distro_full_name="") + self.assertTrue(type(ret) == RedhatOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + def test_get_osutil_it_should_return_freebsd(self): + ret = _get_osutil(distro_name="freebsd", + distro_code_name="", + distro_version="", + distro_full_name="") + self.assertTrue(type(ret) == FreeBSDOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + def test_get_osutil_it_should_return_openbsd(self): + ret = _get_osutil(distro_name="openbsd", + distro_code_name="", + distro_version="", + distro_full_name="") + self.assertTrue(type(ret) == OpenBSDOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + def test_get_osutil_it_should_return_bigip(self): + ret = _get_osutil(distro_name="bigip", + distro_code_name="", + distro_version="", + distro_full_name="") + self.assertTrue(type(ret) == BigIpOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + def test_get_osutil_it_should_return_gaia(self): + ret = _get_osutil(distro_name="gaia", + distro_code_name="", + distro_version="", + distro_full_name="") + self.assertTrue(type(ret) == GaiaOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + def test_get_osutil_it_should_return_iosxe(self): + ret = _get_osutil(distro_name="iosxe", + distro_code_name="", + distro_version="", + distro_full_name="") + self.assertTrue(type(ret) == IosxeOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") + + def test_get_osutil_it_should_return_openwrt(self): + ret = _get_osutil(distro_name="openwrt", + distro_code_name="", + distro_version="", + distro_full_name="") + self.assertTrue(type(ret) == OpenWRTOSUtil) + self.assertEquals(ret.get_service_name(), "waagent") diff -Nru waagent-2.2.34/tests/common/osutil/test_freebsd.py waagent-2.2.45/tests/common/osutil/test_freebsd.py --- waagent-2.2.34/tests/common/osutil/test_freebsd.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/osutil/test_freebsd.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,124 @@ +# Copyright 2019 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +import mock + +from azurelinuxagent.common.osutil.freebsd import FreeBSDOSUtil +import azurelinuxagent.common.utils.shellutil as shellutil +from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids +from tests.tools import * + +class TestFreeBSDOSUtil(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + + def test_get_dhcp_pid_should_return_a_list_of_pids(self): + osutil_get_dhcp_pid_should_return_a_list_of_pids(self, FreeBSDOSUtil()) + + def test_empty_proc_net_route(self): + route_table = "" + + with patch.object(shellutil, 'run_command', return_value=route_table): + # Header line only + self.assertEqual(len(FreeBSDOSUtil().read_route_table()), 1) + + def test_no_routes(self): + route_table = """Routing tables + +Internet: +Destination Gateway Flags Netif Expire +""" + + with patch.object(shellutil, 'run_command', return_value=route_table): + raw_route_list = FreeBSDOSUtil().read_route_table() + + self.assertEqual(len(FreeBSDOSUtil().get_list_of_routes(raw_route_list)), 0) + + def test_bogus_proc_net_route(self): + route_table = """Routing tables + +Internet: +Destination Gateway Flags Netif Expire +1.1.1 0.0.0 +""" + + with patch.object(shellutil, 'run_command', return_value=route_table): + raw_route_list = FreeBSDOSUtil().read_route_table() + + self.assertEqual(len(FreeBSDOSUtil().get_list_of_routes(raw_route_list)), 0) + + def test_valid_routes(self): + route_table = """Routing tables + +Internet: +Destination Gateway Flags Netif Expire +0.0.0.0 10.145.187.193 UGS em0 +10.145.187.192/26 0.0.0.0 US em0 +168.63.129.16 10.145.187.193 UH em0 +169.254.169.254 10.145.187.193 UHS em0 +192.168.43.0 0.0.0.0 US vtbd0 +""" + + with patch.object(shellutil, 'run_command', return_value=route_table): + raw_route_list = FreeBSDOSUtil().read_route_table() + + self.assertEqual(len(raw_route_list), 6) + + route_list = FreeBSDOSUtil().get_list_of_routes(raw_route_list) + + self.assertEqual(len(route_list), 5) + self.assertEqual(route_list[0].gateway_quad(), '10.145.187.193') + self.assertEqual(route_list[1].gateway_quad(), '0.0.0.0') + self.assertEqual(route_list[1].mask_quad(), '255.255.255.192') + self.assertEqual(route_list[2].destination_quad(), '168.63.129.16') + self.assertEqual(route_list[1].flags, 1) + self.assertEqual(route_list[2].flags, 33) + self.assertEqual(route_list[3].flags, 5) + self.assertEqual((route_list[3].metric - route_list[4].metric), 1) + self.assertEqual(route_list[0].interface, 'em0') + self.assertEqual(route_list[4].interface, 'vtbd0') + + def test_get_first_if(self): + """ + Validate that the agent can find the first active non-loopback + interface. + This test case used to run live, but not all developers have an eth* + interface. It is perfectly valid to have a br*, but this test does not + account for that. + """ + freebsdosutil = FreeBSDOSUtil() + + with patch.object(freebsdosutil, '_get_net_info', return_value=('em0', '10.0.0.1', 'e5:f0:38:aa:da:52')): + ifname, ipaddr = freebsdosutil.get_first_if() + + self.assertEqual(ifname, 'em0') + self.assertEqual(ipaddr, '10.0.0.1') + + def test_no_primary_does_not_throw(self): + freebsdosutil = FreeBSDOSUtil() + + with patch.object(freebsdosutil, '_get_net_info', return_value=('em0', '10.0.0.1', 'e5:f0:38:aa:da:52')): + try: + freebsdosutil.get_first_if()[0] + except Exception as e: + print(traceback.format_exc()) + exception = True + +if __name__ == '__main__': + unittest.main() diff -Nru waagent-2.2.34/tests/common/osutil/test_nsbsd.py waagent-2.2.45/tests/common/osutil/test_nsbsd.py --- waagent-2.2.34/tests/common/osutil/test_nsbsd.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/osutil/test_nsbsd.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,87 @@ +# Copyright 2019 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +from azurelinuxagent.common.utils.fileutil import read_file +from azurelinuxagent.common.osutil.nsbsd import NSBSDOSUtil +from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids +from tests.tools import * +from os import path + + +class TestNSBSDOSUtil(AgentTestCase): + dhclient_pid_file = "/var/run/dhclient.pid" + + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + + def test_get_dhcp_pid_should_return_a_list_of_pids(self): + with patch.object(NSBSDOSUtil, "resolver"): # instantiating NSBSDOSUtil requires a resolver + original_isfile = path.isfile + + def mock_isfile(path): + return True if path == self.dhclient_pid_file else original_isfile(path) + + original_read_file = read_file + + def mock_read_file(file, *args, **kwargs): + return "123" if file == self.dhclient_pid_file else original_read_file(file, *args, **kwargs) + + with patch("os.path.isfile", mock_isfile): + with patch("azurelinuxagent.common.osutil.nsbsd.fileutil.read_file", mock_read_file): + pid_list = NSBSDOSUtil().get_dhcp_pid() + + self.assertEquals(pid_list, [123]) + + def test_get_dhcp_pid_should_return_an_empty_list_when_the_dhcp_client_is_not_running(self): + with patch.object(NSBSDOSUtil, "resolver"): # instantiating NSBSDOSUtil requires a resolver + # + # PID file does not exist + # + original_isfile = path.isfile + + def mock_isfile(path): + return False if path == self.dhclient_pid_file else original_isfile(path) + + with patch("os.path.isfile", mock_isfile): + pid_list = NSBSDOSUtil().get_dhcp_pid() + + self.assertEquals(pid_list, []) + + # + # PID file is empty + # + original_isfile = path.isfile + + def mock_isfile(path): + return True if path == self.dhclient_pid_file else original_isfile(path) + + original_read_file = read_file + + def mock_read_file(file, *args, **kwargs): + return "" if file == self.dhclient_pid_file else original_read_file(file, *args, **kwargs) + + with patch("os.path.isfile", mock_isfile): + with patch("azurelinuxagent.common.osutil.nsbsd.fileutil.read_file", mock_read_file): + pid_list = NSBSDOSUtil().get_dhcp_pid() + + self.assertEquals(pid_list, []) + + +if __name__ == '__main__': + unittest.main() diff -Nru waagent-2.2.34/tests/common/osutil/test_openbsd.py waagent-2.2.45/tests/common/osutil/test_openbsd.py --- waagent-2.2.34/tests/common/osutil/test_openbsd.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/osutil/test_openbsd.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,34 @@ +# Copyright 2019 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +from azurelinuxagent.common.osutil.openbsd import OpenBSDOSUtil +from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids +from tests.tools import * + + +class TestAlpineOSUtil(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + + def test_get_dhcp_pid_should_return_a_list_of_pids(self): + osutil_get_dhcp_pid_should_return_a_list_of_pids(self, OpenBSDOSUtil()) + + +if __name__ == '__main__': + unittest.main() diff -Nru waagent-2.2.34/tests/common/osutil/test_openwrt.py waagent-2.2.45/tests/common/osutil/test_openwrt.py --- waagent-2.2.34/tests/common/osutil/test_openwrt.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/osutil/test_openwrt.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,34 @@ +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +from azurelinuxagent.common.osutil.openwrt import OpenWRTOSUtil +from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids +from tests.tools import * + + +class TestOpenWRTOSUtil(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + + def test_get_dhcp_pid_should_return_a_list_of_pids(self): + osutil_get_dhcp_pid_should_return_a_list_of_pids(self, OpenWRTOSUtil()) + + +if __name__ == '__main__': + unittest.main() diff -Nru waagent-2.2.34/tests/common/osutil/test_redhat.py waagent-2.2.45/tests/common/osutil/test_redhat.py --- waagent-2.2.34/tests/common/osutil/test_redhat.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/osutil/test_redhat.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,34 @@ +# Copyright 2019 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +from azurelinuxagent.common.osutil.redhat import Redhat6xOSUtil +from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids +from tests.tools import * + + +class TestRedhat6xOSUtil(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + + def test_get_dhcp_pid_should_return_a_list_of_pids(self): + osutil_get_dhcp_pid_should_return_a_list_of_pids(self, Redhat6xOSUtil()) + + +if __name__ == '__main__': + unittest.main() diff -Nru waagent-2.2.34/tests/common/osutil/test_suse.py waagent-2.2.45/tests/common/osutil/test_suse.py --- waagent-2.2.34/tests/common/osutil/test_suse.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/osutil/test_suse.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,34 @@ +# Copyright 2019 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +from azurelinuxagent.common.osutil.suse import SUSE11OSUtil +from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids +from tests.tools import * + + +class TestSUSE11OSUtil(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + + def test_get_dhcp_pid_should_return_a_list_of_pids(self): + osutil_get_dhcp_pid_should_return_a_list_of_pids(self, SUSE11OSUtil()) + + +if __name__ == '__main__': + unittest.main() diff -Nru waagent-2.2.34/tests/common/osutil/test_ubuntu.py waagent-2.2.45/tests/common/osutil/test_ubuntu.py --- waagent-2.2.34/tests/common/osutil/test_ubuntu.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/osutil/test_ubuntu.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,45 @@ +# Copyright 2019 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +from azurelinuxagent.common.osutil.ubuntu import Ubuntu12OSUtil, Ubuntu18OSUtil +from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids +from tests.tools import * + + +class TestUbuntu12OSUtil(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + + def test_get_dhcp_pid_should_return_a_list_of_pids(self): + osutil_get_dhcp_pid_should_return_a_list_of_pids(self, Ubuntu12OSUtil()) + + +class TestUbuntu18OSUtil(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + + def test_get_dhcp_pid_should_return_a_list_of_pids(self): + osutil_get_dhcp_pid_should_return_a_list_of_pids(self, Ubuntu18OSUtil()) + + +if __name__ == '__main__': + unittest.main() diff -Nru waagent-2.2.34/tests/common/test_cgroupapi.py waagent-2.2.45/tests/common/test_cgroupapi.py --- waagent-2.2.34/tests/common/test_cgroupapi.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/test_cgroupapi.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,642 @@ +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.4+ and Openssl 1.0+ +# + +from __future__ import print_function + +import subprocess +from azurelinuxagent.common.cgroupapi import CGroupsApi, FileSystemCgroupsApi, SystemdCgroupsApi, CGROUPS_FILE_SYSTEM_ROOT, VM_AGENT_CGROUP_NAME +from azurelinuxagent.common.exception import CGroupsException, ExtensionError, ExtensionErrorCodes +from azurelinuxagent.common.future import ustr +from azurelinuxagent.common.utils import shellutil +from nose.plugins.attrib import attr +from tests.utils.cgroups_tools import CGroupsTools +from tests.tools import * + + +class _MockedFileSystemTestCase(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + self.cgroups_file_system_root = os.path.join(self.tmp_dir, "cgroup") + os.mkdir(self.cgroups_file_system_root) + os.mkdir(os.path.join(self.cgroups_file_system_root, "cpu")) + os.mkdir(os.path.join(self.cgroups_file_system_root, "memory")) + + self.mock_cgroups_file_system_root = patch("azurelinuxagent.common.cgroupapi.CGROUPS_FILE_SYSTEM_ROOT", self.cgroups_file_system_root) + self.mock_cgroups_file_system_root.start() + + def tearDown(self): + self.mock_cgroups_file_system_root.stop() + AgentTestCase.tearDown(self) + + +class CGroupsApiTestCase(_MockedFileSystemTestCase): + def test_create_should_return_a_SystemdCgroupsApi_on_systemd_platforms(self): + with patch("azurelinuxagent.common.cgroupapi.CGroupsApi._is_systemd", return_value=True): + api = CGroupsApi.create() + + self.assertTrue(type(api) == SystemdCgroupsApi) + + def test_create_should_return_a_FileSystemCgroupsApi_on_non_systemd_platforms(self): + with patch("azurelinuxagent.common.cgroupapi.CGroupsApi._is_systemd", return_value=False): + api = CGroupsApi.create() + + self.assertTrue(type(api) == FileSystemCgroupsApi) + + def test_is_systemd_should_return_true_when_systemd_manages_current_process(self): + path_exists = os.path.exists + + def mock_path_exists(path): + if path == "/run/systemd/system/": + mock_path_exists.path_tested = True + return True + return path_exists(path) + + mock_path_exists.path_tested = False + + with patch("azurelinuxagent.common.cgroupapi.os.path.exists", mock_path_exists): + is_systemd = CGroupsApi._is_systemd() + + self.assertTrue(is_systemd) + + self.assertTrue(mock_path_exists.path_tested, 'The expected path was not tested; the implementation of CGroupsApi._is_systemd() may have changed.') + + def test_is_systemd_should_return_false_when_systemd_does_not_manage_current_process(self): + path_exists = os.path.exists + + def mock_path_exists(path): + if path == "/run/systemd/system/": + mock_path_exists.path_tested = True + return False + return path_exists(path) + + mock_path_exists.path_tested = False + + with patch("azurelinuxagent.common.cgroupapi.os.path.exists", mock_path_exists): + is_systemd = CGroupsApi._is_systemd() + + self.assertFalse(is_systemd) + + self.assertTrue(mock_path_exists.path_tested, 'The expected path was not tested; the implementation of CGroupsApi._is_systemd() may have changed.') + + def test_foreach_controller_should_execute_operation_on_all_mounted_controllers(self): + executed_controllers = [] + + def controller_operation(controller): + executed_controllers.append(controller) + + CGroupsApi._foreach_controller(controller_operation, 'A dummy message') + + # The setUp method mocks azurelinuxagent.common.cgroupapi.CGROUPS_FILE_SYSTEM_ROOT to have the cpu and memory controllers mounted + self.assertIn('cpu', executed_controllers, 'The operation was not executed on the cpu controller') + self.assertIn('memory', executed_controllers, 'The operation was not executed on the memory controller') + self.assertEqual(len(executed_controllers), 2, 'The operation was not executed on unexpected controllers: {0}'.format(executed_controllers)) + + def test_foreach_controller_should_handle_errors_in_individual_controllers(self): + successful_controllers = [] + + def controller_operation(controller): + if controller == 'cpu': + raise Exception('A test exception') + + successful_controllers.append(controller) + + with patch("azurelinuxagent.common.cgroupapi.logger.warn") as mock_logger_warn: + CGroupsApi._foreach_controller(controller_operation, 'A dummy message') + + self.assertIn('memory', successful_controllers, 'The operation was not executed on the memory controller') + self.assertEqual(len(successful_controllers), 1, 'The operation was not executed on unexpected controllers: {0}'.format(successful_controllers)) + + args, kwargs = mock_logger_warn.call_args + (message_format, controller, error, message) = args + self.assertEquals(message_format, 'Error in cgroup controller "{0}": {1}. {2}') + self.assertEquals(controller, 'cpu') + self.assertEquals(error, 'A test exception') + self.assertEquals(message, 'A dummy message') + + +class FileSystemCgroupsApiTestCase(_MockedFileSystemTestCase): + def test_cleanup_legacy_cgroups_should_move_daemon_pid_to_new_cgroup_and_remove_legacy_cgroups(self): + # Set up a mock /var/run/waagent.pid file + daemon_pid = "42" + daemon_pid_file = os.path.join(self.tmp_dir, "waagent.pid") + fileutil.write_file(daemon_pid_file, daemon_pid + "\n") + + # Set up old controller cgroups and add the daemon PID to them + legacy_cpu_cgroup = CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "cpu", daemon_pid) + legacy_memory_cgroup = CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "memory", daemon_pid) + + # Set up new controller cgroups and add extension handler's PID to them + new_cpu_cgroup = CGroupsTools.create_agent_cgroup(self.cgroups_file_system_root, "cpu", "999") + new_memory_cgroup = CGroupsTools.create_agent_cgroup(self.cgroups_file_system_root, "memory", "999") + + with patch("azurelinuxagent.common.cgroupapi.add_event") as mock_add_event: + with patch("azurelinuxagent.common.cgroupapi.get_agent_pid_file_path", return_value=daemon_pid_file): + FileSystemCgroupsApi().cleanup_legacy_cgroups() + + # The method should have added the daemon PID to the new controllers and deleted the old ones + new_cpu_contents = fileutil.read_file(os.path.join(new_cpu_cgroup, "cgroup.procs")) + new_memory_contents = fileutil.read_file(os.path.join(new_memory_cgroup, "cgroup.procs")) + + self.assertTrue(daemon_pid in new_cpu_contents) + self.assertTrue(daemon_pid in new_memory_contents) + + self.assertFalse(os.path.exists(legacy_cpu_cgroup)) + self.assertFalse(os.path.exists(legacy_memory_cgroup)) + + # Assert the event parameters that were sent out + self.assertEquals(len(mock_add_event.call_args_list), 2) + self.assertTrue(all(kwargs['op'] == 'CGroupsCleanUp' for _, kwargs in mock_add_event.call_args_list)) + self.assertTrue(all(kwargs['is_success'] for _, kwargs in mock_add_event.call_args_list)) + self.assertTrue(any( + re.match(r"Moved daemon's PID from legacy cgroup to /.*/cgroup/cpu/walinuxagent.service", kwargs['message']) + for _, kwargs in mock_add_event.call_args_list)) + self.assertTrue(any( + re.match(r"Moved daemon's PID from legacy cgroup to /.*/cgroup/memory/walinuxagent.service", kwargs['message']) + for _, kwargs in mock_add_event.call_args_list)) + + def test_create_agent_cgroups_should_create_cgroups_on_all_controllers(self): + agent_cgroups = FileSystemCgroupsApi().create_agent_cgroups() + + def assert_cgroup_created(controller): + cgroup_path = os.path.join(self.cgroups_file_system_root, controller, VM_AGENT_CGROUP_NAME) + self.assertTrue(any(cgroups.path == cgroup_path for cgroups in agent_cgroups)) + self.assertTrue(any(cgroups.name == VM_AGENT_CGROUP_NAME for cgroups in agent_cgroups)) + self.assertTrue(os.path.exists(cgroup_path)) + cgroup_task = int(fileutil.read_file(os.path.join(cgroup_path, "cgroup.procs"))) + current_process = os.getpid() + self.assertEqual(cgroup_task, current_process) + + assert_cgroup_created("cpu") + assert_cgroup_created("memory") + + def test_create_extension_cgroups_root_should_create_root_directory_for_extensions(self): + FileSystemCgroupsApi().create_extension_cgroups_root() + + cpu_cgroup = os.path.join(self.cgroups_file_system_root, "cpu", "walinuxagent.extensions") + self.assertTrue(os.path.exists(cpu_cgroup)) + + memory_cgroup = os.path.join(self.cgroups_file_system_root, "memory", "walinuxagent.extensions") + self.assertTrue(os.path.exists(memory_cgroup)) + + def test_create_extension_cgroups_should_create_cgroups_on_all_controllers(self): + api = FileSystemCgroupsApi() + api.create_extension_cgroups_root() + extension_cgroups = api.create_extension_cgroups("Microsoft.Compute.TestExtension-1.2.3") + + def assert_cgroup_created(controller): + cgroup_path = os.path.join(self.cgroups_file_system_root, controller, "walinuxagent.extensions", + "Microsoft.Compute.TestExtension_1.2.3") + + self.assertTrue(any(cgroups.path == cgroup_path for cgroups in extension_cgroups)) + self.assertTrue(os.path.exists(cgroup_path)) + + assert_cgroup_created("cpu") + assert_cgroup_created("memory") + + def test_remove_extension_cgroups_should_remove_all_cgroups(self): + api = FileSystemCgroupsApi() + api.create_extension_cgroups_root() + extension_cgroups = api.create_extension_cgroups("Microsoft.Compute.TestExtension-1.2.3") + + api.remove_extension_cgroups("Microsoft.Compute.TestExtension-1.2.3") + + for cgroup in extension_cgroups: + self.assertFalse(os.path.exists(cgroup.path)) + + def test_remove_extension_cgroups_should_log_a_warning_when_the_cgroup_contains_active_tasks(self): + api = FileSystemCgroupsApi() + api.create_extension_cgroups_root() + api.create_extension_cgroups("Microsoft.Compute.TestExtension-1.2.3") + + with patch("azurelinuxagent.common.cgroupapi.logger.warn") as mock_logger_warn: + with patch("azurelinuxagent.common.cgroupapi.os.rmdir", side_effect=OSError(16, "Device or resource busy")): + api.remove_extension_cgroups("Microsoft.Compute.TestExtension-1.2.3") + + args, kwargs = mock_logger_warn.call_args + message = args[0] + self.assertIn("still has active tasks", message) + + def test_get_extension_cgroups_should_return_all_cgroups(self): + api = FileSystemCgroupsApi() + api.create_extension_cgroups_root() + created = api.create_extension_cgroups("Microsoft.Compute.TestExtension-1.2.3") + + retrieved = api.get_extension_cgroups("Microsoft.Compute.TestExtension-1.2.3") + + self.assertEqual(len(retrieved), len(created)) + + for cgroup in created: + self.assertTrue(any(retrieved_cgroup.path == cgroup.path for retrieved_cgroup in retrieved)) + + @patch('time.sleep', side_effect=lambda _: mock_sleep()) + def test_start_extension_command_should_add_the_child_process_to_the_extension_cgroup(self, _): + api = FileSystemCgroupsApi() + api.create_extension_cgroups_root() + + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: + extension_cgroups, process_output = api.start_extension_command( + extension_name="Microsoft.Compute.TestExtension-1.2.3", + command="echo $$", + timeout=300, + shell=True, + cwd=self.tmp_dir, + env={}, + stdout=stdout, + stderr=stderr) + + # The expected format of the process output is [stdout]\n{PID}\n\n\n[stderr]\n" + pattern = re.compile(r"\[stdout\]\n(\d+)\n\n\n\[stderr\]\n") + m = pattern.match(process_output) + + try: + pid_from_output = int(m.group(1)) + except Exception as e: + self.fail("No PID could be extracted from the process output! Error: {0}".format(ustr(e))) + + for cgroup in extension_cgroups: + cgroups_procs_path = os.path.join(cgroup.path, "cgroup.procs") + with open(cgroups_procs_path, "r") as f: + contents = f.read() + pid_from_cgroup = int(contents) + + self.assertEquals(pid_from_output, pid_from_cgroup, + "The PID from the process output ({0}) does not match the PID found in the" + "process cgroup {1} ({2})".format(pid_from_output, cgroups_procs_path, pid_from_cgroup)) + + +@skip_if_predicate_false(is_systemd_present, "Systemd cgroups API doesn't manage cgroups on systems not using systemd.") +class SystemdCgroupsApiTestCase(AgentTestCase): + def test_get_extensions_slice_root_name_should_return_the_root_slice_for_extensions(self): + root_slice_name = SystemdCgroupsApi()._get_extensions_slice_root_name() + self.assertEqual(root_slice_name, "system-walinuxagent.extensions.slice") + + def test_get_extension_slice_name_should_return_the_slice_for_the_given_extension(self): + extension_name = "Microsoft.Azure.DummyExtension-1.0" + extension_slice_name = SystemdCgroupsApi()._get_extension_slice_name(extension_name) + self.assertEqual(extension_slice_name, "system-walinuxagent.extensions-Microsoft.Azure.DummyExtension_1.0.slice") + + @attr('requires_sudo') + def test_create_extension_cgroups_root_should_create_extensions_root_slice(self): + self.assertTrue(i_am_root(), "Test does not run when non-root") + + SystemdCgroupsApi().create_extension_cgroups_root() + + unit_name = SystemdCgroupsApi()._get_extensions_slice_root_name() + _, status = shellutil.run_get_output("systemctl status {0}".format(unit_name)) + self.assertIn("Loaded: loaded", status) + self.assertIn("Active: active", status) + + shellutil.run_get_output("systemctl stop {0}".format(unit_name)) + shellutil.run_get_output("systemctl disable {0}".format(unit_name)) + os.remove("/etc/systemd/system/{0}".format(unit_name)) + shellutil.run_get_output("systemctl daemon-reload") + + @attr('requires_sudo') + def test_create_extension_cgroups_should_create_extension_slice(self): + self.assertTrue(i_am_root(), "Test does not run when non-root") + + extension_name = "Microsoft.Azure.DummyExtension-1.0" + cgroups = SystemdCgroupsApi().create_extension_cgroups(extension_name) + cpu_cgroup, memory_cgroup = cgroups[0], cgroups[1] + self.assertEqual(cpu_cgroup.path, "/sys/fs/cgroup/cpu/system.slice/Microsoft.Azure.DummyExtension_1.0") + self.assertEqual(memory_cgroup.path, "/sys/fs/cgroup/memory/system.slice/Microsoft.Azure.DummyExtension_1.0") + + unit_name = SystemdCgroupsApi()._get_extension_slice_name(extension_name) + self.assertEqual("system-walinuxagent.extensions-Microsoft.Azure.DummyExtension_1.0.slice", unit_name) + + _, status = shellutil.run_get_output("systemctl status {0}".format(unit_name)) + self.assertIn("Loaded: loaded", status) + self.assertIn("Active: active", status) + + shellutil.run_get_output("systemctl stop {0}".format(unit_name)) + shellutil.run_get_output("systemctl disable {0}".format(unit_name)) + os.remove("/etc/systemd/system/{0}".format(unit_name)) + shellutil.run_get_output("systemctl daemon-reload") + + def assert_cgroups_created(self, extension_cgroups): + self.assertEqual(len(extension_cgroups), 2, + 'start_extension_command did not return the expected number of cgroups') + + cpu_found = memory_found = False + + for cgroup in extension_cgroups: + match = re.match( + r'^/sys/fs/cgroup/(cpu|memory)/system.slice/Microsoft.Compute.TestExtension_1\.2\.3\_([a-f0-9-]+)\.scope$', + cgroup.path) + + self.assertTrue(match is not None, "Unexpected path for cgroup: {0}".format(cgroup.path)) + + if match.group(1) == 'cpu': + cpu_found = True + if match.group(1) == 'memory': + memory_found = True + + self.assertTrue(cpu_found, 'start_extension_command did not return a cpu cgroup') + self.assertTrue(memory_found, 'start_extension_command did not return a memory cgroup') + + @patch('time.sleep', side_effect=lambda _: mock_sleep()) + def test_start_extension_command_should_create_extension_scopes(self, _): + original_popen = subprocess.Popen + + def mock_popen(*args, **kwargs): + return original_popen("date", **kwargs) + + # we mock subprocess.Popen to execute a dummy command (date), so no actual cgroups are created; their paths + # should be computed properly, though + with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", mock_popen): + extension_cgroups, process_output = SystemdCgroupsApi().start_extension_command( + extension_name="Microsoft.Compute.TestExtension-1.2.3", + command="date", + shell=False, + timeout=300, + cwd=self.tmp_dir, + env={}, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + self.assert_cgroups_created(extension_cgroups) + + @attr('requires_sudo') + @patch('time.sleep', side_effect=lambda _: mock_sleep(0.2)) + def test_start_extension_command_should_use_systemd_and_not_the_fallback_option_if_successful(self, _): + self.assertTrue(i_am_root(), "Test does not run when non-root") + + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: + with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", wraps=subprocess.Popen) \ + as patch_mock_popen: + extension_cgroups, process_output = SystemdCgroupsApi().start_extension_command( + extension_name="Microsoft.Compute.TestExtension-1.2.3", + command="date", + timeout=300, + shell=True, + cwd=self.tmp_dir, + env={}, + stdout=stdout, + stderr=stderr) + + # We should have invoked the extension command only once and succeeded + self.assertEquals(1, patch_mock_popen.call_count) + + args = patch_mock_popen.call_args[0][0] + self.assertIn("systemd-run --unit", args) + + self.assert_cgroups_created(extension_cgroups) + + @patch('time.sleep', side_effect=lambda _: mock_sleep(0.2)) + def test_start_extension_command_should_use_fallback_option_if_systemd_fails(self, _): + original_popen = subprocess.Popen + + def mock_popen(*args, **kwargs): + # Inject a syntax error to the call + systemd_command = args[0].replace('systemd-run', 'systemd-run syntax_error') + new_args = (systemd_command,) + return original_popen(new_args, **kwargs) + + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: + with patch("azurelinuxagent.common.cgroupapi.add_event") as mock_add_event: + with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", side_effect=mock_popen) \ + as patch_mock_popen: + # We expect this call to fail because of the syntax error + extension_cgroups, process_output = SystemdCgroupsApi().start_extension_command( + extension_name="Microsoft.Compute.TestExtension-1.2.3", + command="date", + timeout=300, + shell=True, + cwd=self.tmp_dir, + env={}, + stdout=stdout, + stderr=stderr) + + args, kwargs = mock_add_event.call_args + self.assertIn("Failed to run systemd-run for unit Microsoft.Compute.TestExtension_1.2.3", + kwargs['message']) + self.assertIn("Failed to find executable syntax_error: No such file or directory", + kwargs['message']) + self.assertEquals(False, kwargs['is_success']) + self.assertEquals('InvokeCommandUsingSystemd', kwargs['op']) + + # We expect two calls to Popen, first for the systemd-run call, second for the fallback option + self.assertEquals(2, patch_mock_popen.call_count) + + first_call_args = patch_mock_popen.mock_calls[0][1][0] + second_call_args = patch_mock_popen.mock_calls[1][1][0] + self.assertIn("systemd-run --unit", first_call_args) + self.assertNotIn("systemd-run --unit", second_call_args) + + # No cgroups should have been created + self.assertEquals(extension_cgroups, []) + + @patch('time.sleep', side_effect=lambda _: mock_sleep(0.001)) + def test_start_extension_command_should_use_fallback_option_if_systemd_times_out(self, _): + # Systemd has its own internal timeout which is shorter than what we define for extension operation timeout. + # When systemd times out, it will write a message to stderr and exit with exit code 1. + # In that case, we will internally recognize the failure due to the non-zero exit code, not as a timeout. + original_popen = subprocess.Popen + systemd_timeout_command = "echo 'Failed to start transient scope unit: Connection timed out' >&2 && exit 1" + + def mock_popen(*args, **kwargs): + # If trying to invoke systemd, mock what would happen if systemd timed out internally: + # write failure to stderr and exit with exit code 1. + new_args = args + if "systemd-run" in args[0]: + new_args = (systemd_timeout_command,) + + return original_popen(new_args, **kwargs) + + expected_output = "[stdout]\n{0}\n\n\n[stderr]\n" + + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: + with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", side_effect=mock_popen) \ + as patch_mock_popen: + extension_cgroups, process_output = SystemdCgroupsApi().start_extension_command( + extension_name="Microsoft.Compute.TestExtension-1.2.3", + command="echo 'success'", + timeout=300, + shell=True, + cwd=self.tmp_dir, + env={}, + stdout=stdout, + stderr=stderr) + + # We expect two calls to Popen, first for the systemd-run call, second for the fallback option + self.assertEquals(2, patch_mock_popen.call_count) + + first_call_args = patch_mock_popen.mock_calls[0][1][0] + second_call_args = patch_mock_popen.mock_calls[1][1][0] + self.assertIn("systemd-run --unit", first_call_args) + self.assertNotIn("systemd-run --unit", second_call_args) + + self.assertEquals(extension_cgroups, []) + self.assertEquals(expected_output.format("success"), process_output) + + @attr('requires_sudo') + @patch("azurelinuxagent.common.cgroupapi.add_event") + @patch('time.sleep', side_effect=lambda _: mock_sleep()) + def test_start_extension_command_should_not_use_fallback_option_if_extension_fails(self, *args): + self.assertTrue(i_am_root(), "Test does not run when non-root") + + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: + with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", wraps=subprocess.Popen) \ + as patch_mock_popen: + with self.assertRaises(ExtensionError) as context_manager: + SystemdCgroupsApi().start_extension_command( + extension_name="Microsoft.Compute.TestExtension-1.2.3", + command="ls folder_does_not_exist", + timeout=300, + shell=True, + cwd=self.tmp_dir, + env={}, + stdout=stdout, + stderr=stderr) + + # We should have invoked the extension command only once, in the systemd-run case + self.assertEquals(1, patch_mock_popen.call_count) + args = patch_mock_popen.call_args[0][0] + self.assertIn("systemd-run --unit", args) + + self.assertEquals(context_manager.exception.code, ExtensionErrorCodes.PluginUnknownFailure) + self.assertIn("Non-zero exit code", ustr(context_manager.exception)) + + @attr('requires_sudo') + @patch("azurelinuxagent.common.cgroupapi.add_event") + def test_start_extension_command_should_not_use_fallback_option_if_extension_times_out(self, *args): + self.assertTrue(i_am_root(), "Test does not run when non-root") + + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: + with patch("azurelinuxagent.common.utils.extensionprocessutil.wait_for_process_completion_or_timeout", + return_value=[True, None]): + with patch("azurelinuxagent.common.cgroupapi.SystemdCgroupsApi._is_systemd_failure", + return_value=False): + with self.assertRaises(ExtensionError) as context_manager: + SystemdCgroupsApi().start_extension_command( + extension_name="Microsoft.Compute.TestExtension-1.2.3", + command="date", + timeout=300, + shell=True, + cwd=self.tmp_dir, + env={}, + stdout=stdout, + stderr=stderr) + + self.assertEquals(context_manager.exception.code, + ExtensionErrorCodes.PluginHandlerScriptTimedout) + self.assertIn("Timeout", ustr(context_manager.exception)) + + @patch('time.sleep', side_effect=lambda _: mock_sleep()) + def test_start_extension_command_should_capture_only_the_last_subprocess_output(self, _): + original_popen = subprocess.Popen + + def mock_popen(*args, **kwargs): + # Inject a syntax error to the call + systemd_command = args[0].replace('systemd-run', 'systemd-run syntax_error') + new_args = (systemd_command,) + return original_popen(new_args, **kwargs) + + expected_output = "[stdout]\n{0}\n\n\n[stderr]\n" + + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: + with patch("azurelinuxagent.common.cgroupapi.add_event"): + with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", side_effect=mock_popen): + # We expect this call to fail because of the syntax error + extension_cgroups, process_output = SystemdCgroupsApi().start_extension_command( + extension_name="Microsoft.Compute.TestExtension-1.2.3", + command="echo 'very specific test message'", + timeout=300, + shell=True, + cwd=self.tmp_dir, + env={}, + stdout=stdout, + stderr=stderr) + + self.assertEquals(expected_output.format("very specific test message"), process_output) + self.assertEquals(extension_cgroups, []) + + @patch("azurelinuxagent.common.utils.fileutil.read_file") + def test_create_agent_cgroups_should_create_cgroups_on_all_controllers(self, patch_read_file): + mock_proc_self_cgroup = '''12:blkio:/system.slice/walinuxagent.service +11:memory:/system.slice/walinuxagent.service +10:perf_event:/ +9:hugetlb:/ +8:freezer:/ +7:net_cls,net_prio:/ +6:devices:/system.slice/walinuxagent.service +5:cpuset:/ +4:cpu,cpuacct:/system.slice/walinuxagent.service +3:pids:/system.slice/walinuxagent.service +2:rdma:/ +1:name=systemd:/system.slice/walinuxagent.service +0::/system.slice/walinuxagent.service +''' + patch_read_file.return_value = mock_proc_self_cgroup + agent_cgroups = SystemdCgroupsApi().create_agent_cgroups() + + def assert_cgroup_created(controller): + expected_cgroup_path = os.path.join(CGROUPS_FILE_SYSTEM_ROOT, controller, "system.slice", VM_AGENT_CGROUP_NAME) + + self.assertTrue(any(cgroups.path == expected_cgroup_path for cgroups in agent_cgroups)) + self.assertTrue(any(cgroups.name == VM_AGENT_CGROUP_NAME for cgroups in agent_cgroups)) + + assert_cgroup_created("cpu") + assert_cgroup_created("memory") + + +class SystemdCgroupsApiMockedFileSystemTestCase(_MockedFileSystemTestCase): + def test_cleanup_legacy_cgroups_should_remove_legacy_cgroups(self): + # Set up a mock /var/run/waagent.pid file + daemon_pid_file = os.path.join(self.tmp_dir, "waagent.pid") + fileutil.write_file(daemon_pid_file, "42\n") + + # Set up old controller cgroups, but do not add the daemon's PID to them + legacy_cpu_cgroup = CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "cpu", '') + legacy_memory_cgroup = CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "memory", '') + + with patch("azurelinuxagent.common.cgroupapi.add_event") as mock_add_event: + with patch("azurelinuxagent.common.cgroupapi.get_agent_pid_file_path", return_value=daemon_pid_file): + SystemdCgroupsApi().cleanup_legacy_cgroups() + + self.assertFalse(os.path.exists(legacy_cpu_cgroup)) + self.assertFalse(os.path.exists(legacy_memory_cgroup)) + + def test_cleanup_legacy_cgroups_should_report_an_error_when_the_daemon_pid_was_added_to_the_legacy_cgroups(self): + # Set up a mock /var/run/waagent.pid file + daemon_pid = "42" + daemon_pid_file = os.path.join(self.tmp_dir, "waagent.pid") + fileutil.write_file(daemon_pid_file, daemon_pid + "\n") + + # Set up old controller cgroups and add the daemon's PID to them + legacy_cpu_cgroup = CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "cpu", daemon_pid) + legacy_memory_cgroup = CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "memory", daemon_pid) + + with patch("azurelinuxagent.common.cgroupapi.add_event") as mock_add_event: + with patch("azurelinuxagent.common.cgroupapi.get_agent_pid_file_path", return_value=daemon_pid_file): + with self.assertRaises(CGroupsException) as context_manager: + SystemdCgroupsApi().cleanup_legacy_cgroups() + + self.assertEquals(context_manager.exception.message, "The daemon's PID ({0}) was already added to the legacy cgroup; this invalidates resource usage data.".format(daemon_pid)) + + # The method should have deleted the legacy cgroups + self.assertFalse(os.path.exists(legacy_cpu_cgroup)) + self.assertFalse(os.path.exists(legacy_memory_cgroup)) + diff -Nru waagent-2.2.34/tests/common/test_cgroupconfigurator.py waagent-2.2.45/tests/common/test_cgroupconfigurator.py --- waagent-2.2.34/tests/common/test_cgroupconfigurator.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/test_cgroupconfigurator.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,295 @@ +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.4+ and Openssl 1.0+ +# + +from __future__ import print_function + +import subprocess + +import errno +from azurelinuxagent.common.cgroup import CGroup +from azurelinuxagent.common.cgroupapi import VM_AGENT_CGROUP_NAME +from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator +from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry +from azurelinuxagent.common.exception import CGroupsException +from azurelinuxagent.common.osutil.default import DefaultOSUtil +from tests.utils.cgroups_tools import CGroupsTools +from tests.tools import * + + +class CGroupConfiguratorTestCase(AgentTestCase): + @classmethod + def setUpClass(cls): + AgentTestCase.setUpClass() + + # Use the file system implementation of CGroupsApi (FileSystemCgroupsApi) + cls.mock_is_systemd = patch("azurelinuxagent.common.cgroupapi.CGroupsApi._is_systemd", return_value=False) + cls.mock_is_systemd.start() + + # Use the default implementation of osutil + cls.mock_get_osutil = patch("azurelinuxagent.common.cgroupconfigurator.get_osutil", return_value=DefaultOSUtil()) + cls.mock_get_osutil.start() + + # Currently osutil.is_cgroups_supported() returns False on Travis runs. We need to revisit this design; in the + # meanwhile mock the method to return True + cls.mock_is_cgroups_supported = patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.is_cgroups_supported", return_value=True) + cls.mock_is_cgroups_supported.start() + + # Mounting the cgroup filesystem requires root privileges. Since these tests do not perform any actual operation on cgroups, make it a noop. + cls.mock_mount_cgroups = patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.mount_cgroups") + cls.mock_mount_cgroups.start() + + @classmethod + def tearDownClass(cls): + cls.mock_mount_cgroups.stop() + cls.mock_is_cgroups_supported.stop() + cls.mock_get_osutil.stop() + cls.mock_is_systemd.stop() + + AgentTestCase.tearDownClass() + + def setUp(self): + AgentTestCase.setUp(self) + CGroupConfigurator._instance = None # force get_instance() to create a new instance for each test + + self.cgroups_file_system_root = os.path.join(self.tmp_dir, "cgroup") + os.mkdir(self.cgroups_file_system_root) + os.mkdir(os.path.join(self.cgroups_file_system_root, "cpu")) + os.mkdir(os.path.join(self.cgroups_file_system_root, "memory")) + + self.mock_cgroups_file_system_root = patch("azurelinuxagent.common.cgroupapi.CGROUPS_FILE_SYSTEM_ROOT", self.cgroups_file_system_root) + self.mock_cgroups_file_system_root.start() + + def tearDown(self): + self.mock_cgroups_file_system_root.stop() + + def test_init_should_mount_the_cgroups_file_system(self): + with patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.mount_cgroups") as mock_mount_cgroups: + CGroupConfigurator.get_instance() + + self.assertEqual(mock_mount_cgroups.call_count, 1) + + def test_init_should_disable_cgroups_when_they_are_not_supported(self): + with patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.is_cgroups_supported", return_value=False): + self.assertFalse(CGroupConfigurator.get_instance().enabled()) + + def test_enable_and_disable_should_change_the_enabled_state_of_cgroups(self): + configurator = CGroupConfigurator.get_instance() + + self.assertTrue(configurator.enabled()) + + configurator.disable() + self.assertFalse(configurator.enabled()) + + configurator.enable() + self.assertTrue(configurator.enabled()) + + def test_enable_should_raise_CGroupsException_when_cgroups_are_not_supported(self): + with patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.is_cgroups_supported", return_value=False): + with self.assertRaises(CGroupsException) as context_manager: + CGroupConfigurator.get_instance().enable() + self.assertIn("cgroups are not supported", str(context_manager.exception)) + + def test_cgroup_operations_should_not_invoke_the_cgroup_api_when_cgroups_are_not_enabled(self): + configurator = CGroupConfigurator.get_instance() + configurator.disable() + + # List of operations to test, and the functions to mock used in order to do verifications + operations = [ + [lambda: configurator.create_agent_cgroups(track_cgroups=False), "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.create_agent_cgroups"], + [lambda: configurator.cleanup_legacy_cgroups(), "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.cleanup_legacy_cgroups"], + [lambda: configurator.create_extension_cgroups_root(), "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.create_extension_cgroups_root"], + [lambda: configurator.create_extension_cgroups("A.B.C-1.0.0"), "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.create_extension_cgroups"], + [lambda: configurator.remove_extension_cgroups("A.B.C-1.0.0"), "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.remove_extension_cgroups"] + ] + + for op in operations: + with patch(op[1]) as mock_cgroup_api_operation: + op[0]() + + self.assertEqual(mock_cgroup_api_operation.call_count, 0) + + def test_cgroup_operations_should_log_a_warning_when_the_cgroup_api_raises_an_exception(self): + configurator = CGroupConfigurator.get_instance() + + # cleanup_legacy_cgroups disables cgroups on error, so make disable() a no-op + with patch.object(configurator, "disable"): + # List of operations to test, and the functions to mock in order to raise exceptions + operations = [ + [lambda: configurator.create_agent_cgroups(track_cgroups=False), "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.create_agent_cgroups"], + [lambda: configurator.cleanup_legacy_cgroups(), "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.cleanup_legacy_cgroups"], + [lambda: configurator.create_extension_cgroups_root(), "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.create_extension_cgroups_root"], + [lambda: configurator.create_extension_cgroups("A.B.C-1.0.0"), "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.create_extension_cgroups"], + [lambda: configurator.remove_extension_cgroups("A.B.C-1.0.0"), "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.remove_extension_cgroups"] + ] + + def raise_exception(*_): + raise Exception("A TEST EXCEPTION") + + for op in operations: + with patch("azurelinuxagent.common.cgroupconfigurator.logger.warn") as mock_logger_warn: + with patch(op[1], raise_exception): + op[0]() + + self.assertEquals(mock_logger_warn.call_count, 1) + + args, kwargs = mock_logger_warn.call_args + message = args[0] + self.assertIn("A TEST EXCEPTION", message) + + def test_start_extension_command_should_forward_to_subprocess_popen_when_groups_are_not_enabled(self): + configurator = CGroupConfigurator.get_instance() + configurator.disable() + + with patch("azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.start_extension_command") as mock_fs: + with patch("azurelinuxagent.common.cgroupapi.SystemdCgroupsApi.start_extension_command") as mock_systemd: + with patch("azurelinuxagent.common.cgroupconfigurator.handle_process_completion") as mock_popen: + configurator.start_extension_command( + extension_name="Microsoft.Compute.TestExtension-1.2.3", + command="date", + timeout=300, + shell=False, + cwd=self.tmp_dir, + env={}, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + self.assertEqual(mock_popen.call_count, 1) + self.assertEqual(mock_fs.call_count, 0) + self.assertEqual(mock_systemd.call_count, 0) + + def test_start_extension_command_should_forward_to_cgroups_api_when_groups_are_enabled(self): + configurator = CGroupConfigurator.get_instance() + + with patch("azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.start_extension_command", + return_value=[[], None]) as mock_start_extension_command: + configurator.start_extension_command( + extension_name="Microsoft.Compute.TestExtension-1.2.3", + command="date", + timeout=300, + shell=False, + cwd=self.tmp_dir, + env={}, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + self.assertEqual(mock_start_extension_command.call_count, 1) + + def test_start_extension_command_should_start_tracking_the_extension_cgroups(self): + CGroupConfigurator.get_instance().start_extension_command( + extension_name="Microsoft.Compute.TestExtension-1.2.3", + command="date", + timeout=300, + shell=False, + cwd=self.tmp_dir, + env={}, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + self.assertTrue(CGroupsTelemetry.is_tracked(os.path.join( + self.cgroups_file_system_root, "cpu", "walinuxagent.extensions/Microsoft.Compute.TestExtension_1.2.3"))) + self.assertTrue(CGroupsTelemetry.is_tracked(os.path.join( + self.cgroups_file_system_root, "memory", "walinuxagent.extensions/Microsoft.Compute.TestExtension_1.2.3"))) + + def test_start_extension_command_should_raise_an_exception_when_the_command_cannot_be_started(self): + configurator = CGroupConfigurator.get_instance() + + def raise_exception(*_, **__): + raise Exception("A TEST EXCEPTION") + + with patch("azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.start_extension_command", raise_exception): + with self.assertRaises(Exception) as context_manager: + configurator.start_extension_command( + extension_name="Microsoft.Compute.TestExtension-1.2.3", + command="date", + timeout=300, + shell=False, + cwd=self.tmp_dir, + env={}, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + self.assertIn("A TEST EXCEPTION", str(context_manager.exception)) + + def test_cleanup_legacy_cgroups_should_disable_cgroups_when_it_fails_to_process_legacy_cgroups(self): + # Set up a mock /var/run/waagent.pid file + daemon_pid = "42" + daemon_pid_file = os.path.join(self.tmp_dir, "waagent.pid") + fileutil.write_file(daemon_pid_file, daemon_pid + "\n") + + # Set up old controller cgroups and add the daemon PID to them + CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "cpu", daemon_pid) + CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "memory", daemon_pid) + + # Set up new controller cgroups and add extension handler's PID to them + CGroupsTools.create_agent_cgroup(self.cgroups_file_system_root, "cpu", "999") + CGroupsTools.create_agent_cgroup(self.cgroups_file_system_root, "memory", "999") + + def mock_append_file(filepath, contents, **kwargs): + if re.match(r'/.*/cpu/.*/cgroup.procs', filepath): + raise OSError(errno.ENOSPC, os.strerror(errno.ENOSPC)) + fileutil.append_file(filepath, controller, **kwargs) + + # Start tracking a couple of dummy cgroups + CGroupsTelemetry.track_cgroup(CGroup("dummy", "/sys/fs/cgroup/memory/system.slice/dummy.service", "cpu")) + CGroupsTelemetry.track_cgroup(CGroup("dummy", "/sys/fs/cgroup/memory/system.slice/dummy.service", "memory")) + + cgroup_configurator = CGroupConfigurator.get_instance() + + with patch("azurelinuxagent.common.cgroupconfigurator.add_event") as mock_add_event: + with patch("azurelinuxagent.common.cgroupapi.get_agent_pid_file_path", return_value=daemon_pid_file): + with patch("azurelinuxagent.common.cgroupapi.fileutil.append_file", side_effect=mock_append_file): + cgroup_configurator.cleanup_legacy_cgroups() + + self.assertEquals(len(mock_add_event.call_args_list), 1) + _, kwargs = mock_add_event.call_args_list[0] + self.assertEquals(kwargs['op'], 'CGroupsCleanUp') + self.assertFalse(kwargs['is_success']) + self.assertEquals(kwargs['message'], 'Failed to process legacy cgroups. Collection of resource usage data will be disabled. [Errno 28] No space left on device') + + self.assertFalse(cgroup_configurator.enabled()) + self.assertEquals(len(CGroupsTelemetry._tracked), 0) + + @patch("azurelinuxagent.common.cgroupapi.CGroupsApi._is_systemd", return_value=True) + def test_cleanup_legacy_cgroups_should_disable_cgroups_when_the_daemon_was_added_to_the_legacy_cgroup_on_systemd(self, _): + # Set up a mock /var/run/waagent.pid file + daemon_pid = "42" + daemon_pid_file = os.path.join(self.tmp_dir, "waagent.pid") + fileutil.write_file(daemon_pid_file, daemon_pid + "\n") + + # Set up old controller cgroups and add the daemon PID to them + CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "cpu", daemon_pid) + CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "memory", daemon_pid) + + # Start tracking a couple of dummy cgroups + CGroupsTelemetry.track_cgroup(CGroup("dummy", "/sys/fs/cgroup/memory/system.slice/dummy.service", "cpu")) + CGroupsTelemetry.track_cgroup(CGroup("dummy", "/sys/fs/cgroup/memory/system.slice/dummy.service", "memory")) + + cgroup_configurator = CGroupConfigurator.get_instance() + + with patch("azurelinuxagent.common.cgroupconfigurator.add_event") as mock_add_event: + with patch("azurelinuxagent.common.cgroupapi.get_agent_pid_file_path", return_value=daemon_pid_file): + cgroup_configurator.cleanup_legacy_cgroups() + + self.assertEquals(len(mock_add_event.call_args_list), 1) + _, kwargs = mock_add_event.call_args_list[0] + self.assertEquals(kwargs['op'], 'CGroupsCleanUp') + self.assertFalse(kwargs['is_success']) + self.assertEquals( + kwargs['message'], + "Failed to process legacy cgroups. Collection of resource usage data will be disabled. The daemon's PID ({0}) was already added to the legacy cgroup; this invalidates resource usage data.".format(daemon_pid)) + + self.assertFalse(cgroup_configurator.enabled()) + self.assertEquals(len(CGroupsTelemetry._tracked), 0) diff -Nru waagent-2.2.34/tests/common/test_cgroups.py waagent-2.2.45/tests/common/test_cgroups.py --- waagent-2.2.34/tests/common/test_cgroups.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/common/test_cgroups.py 2019-11-07 00:36:56.000000000 +0000 @@ -17,14 +17,11 @@ from __future__ import print_function -from azurelinuxagent.common.cgroups import CGroupsTelemetry, CGroups, CGroupsLimits, \ - CGroupsException, CGroupsLimits, BASE_CGROUPS, Cpu, Memory, DEFAULT_MEM_LIMIT_MIN_MB -from azurelinuxagent.common.version import AGENT_NAME -from tests.tools import * - -import os import random -import time + +from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCgroup, CGroup +from azurelinuxagent.common.exception import CGroupsException +from tests.tools import * def consume_cpu_time(): @@ -34,300 +31,128 @@ return waste -def make_self_cgroups(): - """ - Build a CGroups object for the cgroup to which this process already belongs - - :return: CGroups containing this process - :rtype: CGroups - """ - - def path_maker(hierarchy, __): - suffix = CGroups.get_my_cgroup_path(CGroups.get_hierarchy_id('cpu')) - return os.path.join(BASE_CGROUPS, hierarchy, suffix) - - return CGroups("inplace", path_maker) - - -def make_root_cgroups(): - """ - Build a CGroups object for the topmost cgroup - - :return: CGroups for most-encompassing cgroup - :rtype: CGroups - """ - - def path_maker(hierarchy, _): - return os.path.join(BASE_CGROUPS, hierarchy) - - return CGroups("root", path_maker) - - -def i_am_root(): - return os.geteuid() == 0 - - -@skip_if_predicate_false(CGroups.enabled, "CGroups not supported in this environment") -class TestCGroups(AgentTestCase): - @classmethod - def setUpClass(cls): - CGroups.setup(True) - super(AgentTestCase, cls).setUpClass() - - def test_cgroup_utilities(self): - """ - Test utilities for querying cgroup metadata - """ - cpu_id = CGroups.get_hierarchy_id('cpu') - self.assertGreater(int(cpu_id), 0) - memory_id = CGroups.get_hierarchy_id('memory') - self.assertGreater(int(memory_id), 0) - self.assertNotEqual(cpu_id, memory_id) - - def test_telemetry_inplace(self): - """ - Test raw measures and basic statistics for the cgroup in which this process is currently running. - """ - cg = make_self_cgroups() - self.assertIn('cpu', cg.cgroups) - self.assertIn('memory', cg.cgroups) - ct = CGroupsTelemetry("test", cg) - cpu = Cpu(ct) - self.assertGreater(cpu.current_system_cpu, 0) - - consume_cpu_time() # Eat some CPU - cpu.update() - - self.assertGreater(cpu.current_cpu_total, cpu.previous_cpu_total) - self.assertGreater(cpu.current_system_cpu, cpu.previous_system_cpu) - - percent_used = cpu.get_cpu_percent() - self.assertGreater(percent_used, 0) - - def test_telemetry_in_place_leaf_cgroup(self): - """ - Ensure this leaf (i.e. not root of cgroup tree) cgroup has distinct metrics from the root cgroup. - """ - # Does nothing on systems where the default cgroup for a randomly-created process (like this test invocation) - # is the root cgroup. - cg = make_self_cgroups() - root = make_root_cgroups() - if cg.cgroups['cpu'] != root.cgroups['cpu']: - ct = CGroupsTelemetry("test", cg) - cpu = Cpu(ct) - self.assertLess(cpu.current_cpu_total, cpu.current_system_cpu) - - consume_cpu_time() # Eat some CPU - time.sleep(1) # Generate some idle time - cpu.update() - self.assertLess(cpu.current_cpu_total, cpu.current_system_cpu) - - def exercise_telemetry_instantiation(self, test_cgroup): - test_extension_name = test_cgroup.name - CGroupsTelemetry.track_cgroup(test_cgroup) - self.assertIn('cpu', test_cgroup.cgroups) - self.assertIn('memory', test_cgroup.cgroups) - self.assertTrue(CGroupsTelemetry.is_tracked(test_extension_name)) - consume_cpu_time() - time.sleep(1) - metrics, limits = CGroupsTelemetry.collect_all_tracked() - my_metrics = metrics[test_extension_name] - self.assertEqual(len(my_metrics), 2) - for item in my_metrics: - metric_family, metric_name, metric_value = item - if metric_family == "Process": - self.assertEqual(metric_name, "% Processor Time") - self.assertGreater(metric_value, 0.0) - elif metric_family == "Memory": - self.assertEqual(metric_name, "Total Memory Usage") - self.assertGreater(metric_value, 100000) - else: - self.fail("Unknown metric {0}/{1} value {2}".format(metric_family, metric_name, metric_value)) - - my_limits = limits[test_extension_name] - self.assertIsInstance(my_limits, CGroupsLimits, msg="is not the correct instance") - self.assertGreater(my_limits.cpu_limit, 0.0) - self.assertGreater(my_limits.memory_limit, 0.0) - - @skip_if_predicate_false(i_am_root, "Test does not run when non-root") - def test_telemetry_instantiation_as_superuser(self): - """ - Tracking a new cgroup for an extension; collect all metrics. - """ - # Record initial state - initial_cgroup = make_self_cgroups() - - # Put the process into a different cgroup, consume some resources, ensure we see them end-to-end - test_cgroup = CGroups.for_extension("agent_unittest") - test_cgroup.add(os.getpid()) - self.assertNotEqual(initial_cgroup.cgroups['cpu'], test_cgroup.cgroups['cpu']) - self.assertNotEqual(initial_cgroup.cgroups['memory'], test_cgroup.cgroups['memory']) - - self.exercise_telemetry_instantiation(test_cgroup) - - # Restore initial state - CGroupsTelemetry.stop_tracking("agent_unittest") - initial_cgroup.add(os.getpid()) - - @skip_if_predicate_true(i_am_root, "Test does not run when root") - def test_telemetry_instantiation_as_normal_user(self): - """ - Tracking an existing cgroup for an extension; collect all metrics. - """ - self.exercise_telemetry_instantiation(make_self_cgroups()) - - @skip_if_predicate_true(i_am_root, "Test does not run when root") - @patch("azurelinuxagent.common.conf.get_cgroups_enforce_limits") - @patch("azurelinuxagent.common.cgroups.CGroups.set_cpu_limit") - @patch("azurelinuxagent.common.cgroups.CGroups.set_memory_limit") - def test_telemetry_instantiation_as_normal_user_with_limits(self, mock_get_cgroups_enforce_limits, - mock_set_cpu_limit, - mock_set_memory_limit): - """ - Tracking an existing cgroup for an extension; collect all metrics. - """ - mock_get_cgroups_enforce_limits.return_value = True - - cg = make_self_cgroups() - cg.set_limits() - self.exercise_telemetry_instantiation(cg) - - def test_cpu_telemetry(self): - """ - Test Cpu telemetry class - """ - cg = make_self_cgroups() - self.assertIn('cpu', cg.cgroups) - ct = CGroupsTelemetry('test', cg) - self.assertIs(cg, ct.cgroup) - cpu = Cpu(ct) - self.assertIs(cg, cpu.cgt.cgroup) - ticks_before = cpu.current_cpu_total - consume_cpu_time() - time.sleep(1) - cpu.update() - ticks_after = cpu.current_cpu_total - self.assertGreater(ticks_after, ticks_before) - p2 = cpu.get_cpu_percent() - self.assertGreater(p2, 0) - # when running under PyCharm, this is often > 100 - # on a multi-core machine - self.assertLess(p2, 200) - - def test_memory_telemetry(self): - """ - Test Memory telemetry class - """ - cg = make_self_cgroups() - raw_usage_file_contents = cg.get_file_contents('memory', 'memory.usage_in_bytes') - self.assertIsNotNone(raw_usage_file_contents) - self.assertGreater(len(raw_usage_file_contents), 0) - self.assertIn('memory', cg.cgroups) - ct = CGroupsTelemetry('test', cg) - self.assertIs(cg, ct.cgroup) - memory = Memory(ct) - usage_in_bytes = memory.get_memory_usage() - self.assertGreater(usage_in_bytes, 100000) - - def test_format_memory_value(self): - """ - Test formatting of memory amounts into human-readable units - """ - self.assertEqual(-1, CGroups._format_memory_value('bytes', None)) - self.assertEqual(2048, CGroups._format_memory_value('kilobytes', 2)) - self.assertEqual(0, CGroups._format_memory_value('kilobytes', 0)) - self.assertEqual(2048000, CGroups._format_memory_value('kilobytes', 2000)) - self.assertEqual(2048 * 1024, CGroups._format_memory_value('megabytes', 2)) - self.assertEqual((1024 + 512) * 1024 * 1024, CGroups._format_memory_value('gigabytes', 1.5)) - self.assertRaises(CGroupsException, CGroups._format_memory_value, 'KiloBytes', 1) - - @patch('azurelinuxagent.common.event.add_event') - @patch('azurelinuxagent.common.conf.get_cgroups_enforce_limits') - @patch('azurelinuxagent.common.cgroups.CGroups.set_memory_limit') - @patch('azurelinuxagent.common.cgroups.CGroups.set_cpu_limit') - @patch('azurelinuxagent.common.cgroups.CGroups._try_mkdir') - def assert_limits(self, _, patch_set_cpu, patch_set_memory_limit, patch_get_enforce, patch_add_event, - ext_name, - expected_cpu_limit, - limits_enforced=True, - exception_raised=False): - - should_limit = expected_cpu_limit > 0 - patch_get_enforce.return_value = limits_enforced - - if exception_raised: - patch_set_memory_limit.side_effect = CGroupsException('set_memory_limit error') - - try: - cg = CGroups.for_extension(ext_name) - cg.set_limits() - if exception_raised: - self.fail('exception expected') - except CGroupsException: - if not exception_raised: - self.fail('exception not expected') - - self.assertEqual(should_limit, patch_set_cpu.called) - self.assertEqual(should_limit, patch_set_memory_limit.called) - self.assertEqual(should_limit, patch_add_event.called) - - if should_limit: - actual_cpu_limit = patch_set_cpu.call_args[0][0] - actual_memory_limit = patch_set_memory_limit.call_args[0][0] - event_kw_args = patch_add_event.call_args[1] - - self.assertEqual(expected_cpu_limit, actual_cpu_limit) - self.assertTrue(actual_memory_limit >= DEFAULT_MEM_LIMIT_MIN_MB) - self.assertEqual(event_kw_args['op'], 'SetCGroupsLimits') - self.assertEqual(event_kw_args['is_success'], not exception_raised) - self.assertTrue('{0}%'.format(expected_cpu_limit) in event_kw_args['message']) - self.assertTrue(ext_name in event_kw_args['message']) - self.assertEqual(exception_raised, 'set_memory_limit error' in event_kw_args['message']) - - def test_limits(self): - self.assert_limits(ext_name="normal_extension", expected_cpu_limit=40) - self.assert_limits(ext_name="customscript_extension", expected_cpu_limit=-1) - self.assert_limits(ext_name=AGENT_NAME, expected_cpu_limit=10) - self.assert_limits(ext_name="normal_extension", expected_cpu_limit=-1, limits_enforced=False) - self.assert_limits(ext_name=AGENT_NAME, expected_cpu_limit=-1, limits_enforced=False) - self.assert_limits(ext_name="normal_extension", expected_cpu_limit=40, exception_raised=True) - - -class TestCGroupsLimits(AgentTestCase): - @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_total_mem", return_value=1024) - def test_no_limits_passed(self, patched_get_total_mem): - cgroup_name = "test_cgroup" - limits = CGroupsLimits(cgroup_name) - self.assertEqual(limits.cpu_limit, CGroupsLimits.get_default_cpu_limits(cgroup_name )) - self.assertEqual(limits.memory_limit, CGroupsLimits.get_default_memory_limits(cgroup_name )) - - limits = CGroupsLimits(None) - self.assertEqual(limits.cpu_limit, CGroupsLimits.get_default_cpu_limits(cgroup_name)) - self.assertEqual(limits.memory_limit, CGroupsLimits.get_default_memory_limits(cgroup_name)) - - @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_total_mem", return_value=1024) - def test_with_limits_passed(self, patched_get_total_mem): - cpu_limit = 50 - memory_limit = 300 - cgroup_name = "test_cgroup" - - threshold = {"cpu": cpu_limit} - limits = CGroupsLimits(cgroup_name, threshold=threshold) - self.assertEqual(limits.cpu_limit, cpu_limit) - self.assertEqual(limits.memory_limit, CGroupsLimits.get_default_memory_limits(cgroup_name)) - - threshold = {"memory": memory_limit} - limits = CGroupsLimits(cgroup_name, threshold=threshold) - self.assertEqual(limits.cpu_limit, CGroupsLimits.get_default_cpu_limits(cgroup_name)) - self.assertEqual(limits.memory_limit, memory_limit) - - threshold = {"cpu": cpu_limit, "memory": memory_limit} - limits = CGroupsLimits(cgroup_name, threshold=threshold) - self.assertEqual(limits.cpu_limit, cpu_limit) - self.assertEqual(limits.memory_limit, memory_limit) - - # Incorrect key - threshold = {"cpux": cpu_limit} - limits = CGroupsLimits(cgroup_name, threshold=threshold) - self.assertEqual(limits.cpu_limit, CGroupsLimits.get_default_cpu_limits(cgroup_name)) - self.assertEqual(limits.memory_limit, CGroupsLimits.get_default_memory_limits(cgroup_name)) +class TestCGroup(AgentTestCase): + + def setUp(self): + AgentTestCase.setUp(self) + + def tearDown(self): + AgentTestCase.tearDown(self) + + with open(os.path.join(data_dir, "cgroups", "cpu_mount", "tasks"), mode="wb") as tasks: + tasks.truncate(0) + with open(os.path.join(data_dir, "cgroups", "memory_mount", "tasks"), mode="wb") as tasks: + tasks.truncate(0) + + def test_correct_creation(self): + test_cgroup = CGroup.create("dummy_path", "cpu", "test_extension") + self.assertIsInstance(test_cgroup, CpuCgroup) + self.assertEqual(test_cgroup.controller, "cpu") + self.assertEqual(test_cgroup.path, "dummy_path") + self.assertEqual(test_cgroup.name, "test_extension") + + test_cgroup = CGroup.create("dummy_path", "memory", "test_extension") + self.assertIsInstance(test_cgroup, MemoryCgroup) + self.assertEqual(test_cgroup.controller, "memory") + self.assertEqual(test_cgroup.path, "dummy_path") + self.assertEqual(test_cgroup.name, "test_extension") + + def test_is_active(self): + test_cgroup = CGroup.create(os.path.join(data_dir, "cgroups", "cpu_mount"), "cpu", "test_extension") + self.assertEqual(False, test_cgroup.is_active()) + + with open(os.path.join(data_dir, "cgroups", "cpu_mount", "tasks"), mode="wb") as tasks: + tasks.write(str(1000).encode()) + + self.assertEqual(True, test_cgroup.is_active()) + + test_cgroup = CGroup.create(os.path.join(data_dir, "cgroups", "memory_mount"), "memory", "test_extension") + self.assertEqual(False, test_cgroup.is_active()) + + with open(os.path.join(data_dir, "cgroups", "memory_mount", "tasks"), mode="wb") as tasks: + tasks.write(str(1000).encode()) + + self.assertEqual(True, test_cgroup.is_active()) + + @patch("azurelinuxagent.common.logger.periodic_warn") + def test_is_active_file_not_present(self, patch_periodic_warn): + test_cgroup = CGroup.create(os.path.join(data_dir, "cgroups", "not_cpu_mount"), "cpu", "test_extension") + self.assertEqual(False, test_cgroup.is_active()) + + test_cgroup = CGroup.create(os.path.join(data_dir, "cgroups", "not_memory_mount"), "memory", "test_extension") + self.assertEqual(False, test_cgroup.is_active()) + + self.assertEqual(0, patch_periodic_warn.call_count) + + @patch("azurelinuxagent.common.logger.periodic_warn") + def test_is_active_incorrect_file(self, patch_periodic_warn): + test_cgroup = CGroup.create(os.path.join(data_dir, "cgroups", "cpu_mount", "tasks"), "cpu", "test_extension") + self.assertEqual(False, test_cgroup.is_active()) + self.assertEqual(1, patch_periodic_warn.call_count) + + test_cgroup = CGroup.create(os.path.join(data_dir, "cgroups", "memory_mount", "tasks"), "memory", "test_extension") + self.assertEqual(False, test_cgroup.is_active()) + self.assertEqual(2, patch_periodic_warn.call_count) + + +class TestCpuCgroup(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_stat") + def test_cpu_cgroup_create(self, patch_get_proc_stat): + patch_get_proc_stat.return_value = fileutil.read_file(os.path.join(data_dir, "cgroups", "dummy_proc_stat")) + test_cpu_cg = CpuCgroup("test_extension", "dummy_path") + + self.assertEqual(398488, test_cpu_cg._current_system_cpu) + self.assertEqual(0, test_cpu_cg._current_cpu_total) + self.assertEqual(0, test_cpu_cg._previous_cpu_total) + self.assertEqual(0, test_cpu_cg._previous_system_cpu) + + self.assertEqual("cpu", test_cpu_cg.controller) + + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_processor_cores", return_value=1) + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_stat") + def test_get_cpu_usage(self, patch_get_proc_stat, *args): + patch_get_proc_stat.return_value = fileutil.read_file(os.path.join(data_dir, "cgroups", "dummy_proc_stat")) + test_cpu_cg = CpuCgroup("test_extension", os.path.join(data_dir, "cgroups", "cpu_mount")) + + # Mocking CPU consumption + patch_get_proc_stat.return_value = fileutil.read_file(os.path.join(data_dir, "cgroups", + "dummy_proc_stat_updated")) + + cpu_usage = test_cpu_cg.get_cpu_usage() + + self.assertEqual(5.114, cpu_usage) + + def test_get_current_cpu_total_exception_handling(self): + test_cpu_cg = CpuCgroup("test_extension", "dummy_path") + self.assertRaises(IOError, test_cpu_cg._get_current_cpu_total) + + # Trying to raise ERRNO 20. + test_cpu_cg = CpuCgroup("test_extension", os.path.join(data_dir, "cgroups", "cpu_mount", "cpuacct.stat")) + self.assertRaises(CGroupsException, test_cpu_cg._get_current_cpu_total) + + +class TestMemoryCgroup(AgentTestCase): + def test_memory_cgroup_create(self): + test_mem_cg = MemoryCgroup("test_extension", os.path.join(data_dir, "cgroups", "memory_mount")) + self.assertEqual("memory", test_mem_cg.controller) + + def test_get_metrics(self): + test_mem_cg = MemoryCgroup("test_extension", os.path.join(data_dir, "cgroups", "memory_mount")) + + memory_usage = test_mem_cg.get_memory_usage() + self.assertEqual(100000, memory_usage) + + max_memory_usage = test_mem_cg.get_max_memory_usage() + self.assertEqual(1000000, max_memory_usage) + + def test_get_metrics_when_files_not_present(self): + test_mem_cg = MemoryCgroup("test_extension", os.path.join(data_dir, "cgroups")) + + memory_usage = test_mem_cg.get_memory_usage() + self.assertEqual(0, memory_usage) + + max_memory_usage = test_mem_cg.get_max_memory_usage() + self.assertEqual(0, max_memory_usage) \ No newline at end of file diff -Nru waagent-2.2.34/tests/common/test_cgroupstelemetry.py waagent-2.2.45/tests/common/test_cgroupstelemetry.py --- waagent-2.2.34/tests/common/test_cgroupstelemetry.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/test_cgroupstelemetry.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,699 @@ +# Copyright 2019 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.4+ and Openssl 1.0+ +# +import errno +import os +import random +import time + +from mock import patch + +from azurelinuxagent.common.cgroup import CGroup +from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator +from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry, Metric +from azurelinuxagent.common.osutil.default import BASE_CGROUPS +from azurelinuxagent.common.protocol.restapi import ExtHandlerProperties, ExtHandler +from azurelinuxagent.ga.exthandlers import ExtHandlerInstance +from nose.plugins.attrib import attr +from tests.tools import AgentTestCase, skip_if_predicate_false, skip_if_predicate_true, \ + are_cgroups_enabled, is_trusty_in_travis, i_am_root + + +def median(lst): + data = sorted(lst) + l_len = len(data) + if l_len < 1: + return None + if l_len % 2 == 0: + return (data[int((l_len - 1) / 2)] + data[int((l_len + 1) / 2)]) / 2.0 + else: + return data[int((l_len - 1) / 2)] + + +def generate_metric_list(lst): + return [float(sum(lst)) / float(len(lst)), + min(lst), + max(lst), + median(lst), + len(lst)] + + +def consume_cpu_time(): + waste = 0 + for x in range(1, 200000): + waste += random.random() + return waste + + +def consume_memory(): + waste = [] + for x in range(1, 3): + waste.append([random.random()] * 10000) + time.sleep(0.1) + waste *= 0 + return waste + + +def make_new_cgroup(name="test-cgroup"): + return CGroupConfigurator.get_instance().create_extension_cgroups(name) + + +class TestCGroupsTelemetry(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + CGroupsTelemetry.reset() + + def tearDown(self): + AgentTestCase.tearDown(self) + CGroupsTelemetry.reset() + + def _assert_cgroup_metrics_equal(self, cpu_usage, memory_usage, max_memory_usage): + for _, cgroup_metric in CGroupsTelemetry._cgroup_metrics.items(): + self.assertListEqual(cgroup_metric.get_memory_usage()._data, memory_usage) + self.assertListEqual(cgroup_metric.get_max_memory_usage()._data, max_memory_usage) + self.assertListEqual(cgroup_metric.get_cpu_usage()._data, cpu_usage) + + @patch("azurelinuxagent.common.cgroup.CpuCgroup._get_current_cpu_total") + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_total_cpu_ticks_since_boot") + def test_telemetry_polling_with_active_cgroups(self, *args): + num_extensions = 5 + for i in range(num_extensions): + dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) + + dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory", + "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) + + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") as patch_get_memory_max_usage: + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") as patch_get_memory_usage: + with patch("azurelinuxagent.common.cgroup.CpuCgroup._get_cpu_percent") as patch_get_cpu_percent: + with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: + patch_is_active.return_value = True + + current_cpu = 30 + current_memory = 209715200 + current_max_memory = 471859200 + + patch_get_cpu_percent.return_value = current_cpu + patch_get_memory_usage.return_value = current_memory # example 200 MB + patch_get_memory_max_usage.return_value = current_max_memory # example 450 MB + + poll_count = 1 + + for data_count in range(poll_count, 10): + CGroupsTelemetry.poll_all_tracked() + self.assertEqual(len(CGroupsTelemetry._cgroup_metrics), num_extensions) + self._assert_cgroup_metrics_equal( + cpu_usage=[current_cpu] * data_count, + memory_usage=[current_memory] * data_count, + max_memory_usage=[current_max_memory] * data_count) + + CGroupsTelemetry.report_all_tracked() + + self.assertEqual(CGroupsTelemetry._cgroup_metrics.__len__(), num_extensions) + self._assert_cgroup_metrics_equal([], [], []) + + @patch("azurelinuxagent.common.cgroup.CpuCgroup._get_current_cpu_total") + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_total_cpu_ticks_since_boot") + def test_telemetry_polling_with_inactive_cgroups(self, *args): + num_extensions = 5 + for i in range(num_extensions): + dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) + + dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory", + "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) + + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") as patch_get_memory_max_usage: + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") as patch_get_memory_usage: + with patch("azurelinuxagent.common.cgroup.CpuCgroup._get_cpu_percent") as patch_get_cpu_percent: + with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: + patch_is_active.return_value = False + + no_extensions_expected = 0 + data_count = 1 + current_cpu = 30 + current_memory = 209715200 + current_max_memory = 471859200 + + patch_get_cpu_percent.return_value = current_cpu + patch_get_memory_usage.return_value = current_memory # example 200 MB + patch_get_memory_max_usage.return_value = current_max_memory # example 450 MB + + for i in range(num_extensions): + self.assertTrue(CGroupsTelemetry.is_tracked("dummy_cpu_path_{0}".format(i))) + self.assertTrue(CGroupsTelemetry.is_tracked("dummy_memory_path_{0}".format(i))) + + CGroupsTelemetry.poll_all_tracked() + + for i in range(num_extensions): + self.assertFalse(CGroupsTelemetry.is_tracked("dummy_cpu_path_{0}".format(i))) + self.assertFalse(CGroupsTelemetry.is_tracked("dummy_memory_path_{0}".format(i))) + + self.assertEqual(CGroupsTelemetry._cgroup_metrics.__len__(), num_extensions) + self._assert_cgroup_metrics_equal( + cpu_usage=[current_cpu] * data_count, + memory_usage=[current_memory] * data_count, + max_memory_usage=[current_max_memory] * data_count) + + CGroupsTelemetry.report_all_tracked() + + self.assertEqual(CGroupsTelemetry._cgroup_metrics.__len__(), no_extensions_expected) + self._assert_cgroup_metrics_equal([], [], []) + + @patch("azurelinuxagent.common.cgroup.CpuCgroup._get_current_cpu_total") + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_total_cpu_ticks_since_boot") + def test_telemetry_polling_with_changing_cgroups_state(self, *args): + num_extensions = 5 + for i in range(num_extensions): + dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) + + dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory", + "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) + + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") as patch_get_memory_max_usage: + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") as patch_get_memory_usage: + with patch("azurelinuxagent.common.cgroup.CpuCgroup._get_cpu_percent") as patch_get_cpu_percent: + with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: + patch_is_active.return_value = True + + no_extensions_expected = 0 + expected_data_count = 2 + + current_cpu = 30 + current_memory = 209715200 + current_max_memory = 471859200 + + patch_get_cpu_percent.return_value = current_cpu + patch_get_memory_usage.return_value = current_memory # example 200 MB + patch_get_memory_max_usage.return_value = current_max_memory # example 450 MB + + for i in range(num_extensions): + self.assertTrue(CGroupsTelemetry.is_tracked("dummy_cpu_path_{0}".format(i))) + self.assertTrue(CGroupsTelemetry.is_tracked("dummy_memory_path_{0}".format(i))) + + CGroupsTelemetry.poll_all_tracked() + + for i in range(num_extensions): + self.assertTrue(CGroupsTelemetry.is_tracked("dummy_cpu_path_{0}".format(i))) + self.assertTrue(CGroupsTelemetry.is_tracked("dummy_memory_path_{0}".format(i))) + + self.assertEqual(CGroupsTelemetry._cgroup_metrics.__len__(), num_extensions) + + patch_is_active.return_value = False + CGroupsTelemetry.poll_all_tracked() + + for i in range(num_extensions): + self.assertFalse(CGroupsTelemetry.is_tracked("dummy_cpu_path_{0}".format(i))) + self.assertFalse(CGroupsTelemetry.is_tracked("dummy_memory_path_{0}".format(i))) + + self.assertEqual(CGroupsTelemetry._cgroup_metrics.__len__(), num_extensions) + self._assert_cgroup_metrics_equal( + cpu_usage=[current_cpu] * expected_data_count, + memory_usage=[current_memory] * expected_data_count, + max_memory_usage=[current_max_memory] * expected_data_count) + + CGroupsTelemetry.report_all_tracked() + + self.assertEqual(CGroupsTelemetry._cgroup_metrics.__len__(), no_extensions_expected) + self._assert_cgroup_metrics_equal([], [], []) + + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_stat") + @patch("azurelinuxagent.common.logger.periodic_warn") + @patch("azurelinuxagent.common.utils.fileutil.read_file") + def test_telemetry_polling_to_not_generate_transient_logs_ioerror_file_not_found(self, mock_read_file, + patch_periodic_warn, *args): + num_extensions = 1 + for i in range(num_extensions): + dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) + + dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory", + "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) + + self.assertEqual(0, patch_periodic_warn.call_count) + + # Not expecting logs present for io_error with errno=errno.ENOENT + io_error_2 = IOError() + io_error_2.errno = errno.ENOENT + mock_read_file.side_effect = io_error_2 + + poll_count = 1 + for data_count in range(poll_count, 10): + CGroupsTelemetry.poll_all_tracked() + self.assertEqual(0, patch_periodic_warn.call_count) + + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_stat") + @patch("azurelinuxagent.common.logger.periodic_warn") + @patch("azurelinuxagent.common.utils.fileutil.read_file") + def test_telemetry_polling_to_generate_transient_logs_ioerror_permission_denied(self, mock_read_file, + patch_periodic_warn, *args): + num_extensions = 1 + num_controllers = 2 + is_active_check_per_controller = 2 + + for i in range(num_extensions): + dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) + + dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory", + "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) + + self.assertEqual(0, patch_periodic_warn.call_count) + + # Expecting logs to be present for different kind of errors + io_error_3 = IOError() + io_error_3.errno = errno.EPERM + mock_read_file.side_effect = io_error_3 + + poll_count = 1 + expected_count_per_call = num_controllers + is_active_check_per_controller + # each collect per controller would generate a log statement, and each cgroup would invoke a + # is active check raising an exception + + for data_count in range(poll_count, 10): + CGroupsTelemetry.poll_all_tracked() + self.assertEqual(poll_count * expected_count_per_call, patch_periodic_warn.call_count) + + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_stat") + @patch("azurelinuxagent.common.utils.fileutil.read_file") + def test_telemetry_polling_to_generate_transient_logs_index_error(self, mock_read_file, *args): + num_extensions = 1 + for i in range(num_extensions): + dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) + + dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory", + "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) + + # Generating a different kind of error (non-IOError) to check the logging. + # Trying to invoke IndexError during the getParameter call + mock_read_file.return_value = '' + + with patch("azurelinuxagent.common.logger.periodic_warn") as patch_periodic_warn: + expected_call_count = 1 # called only once at start, and then gets removed from the tracked data. + for data_count in range(1, 10): + CGroupsTelemetry.poll_all_tracked() + self.assertEqual(expected_call_count, patch_periodic_warn.call_count) + + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_total_cpu_ticks_since_boot") + @patch("azurelinuxagent.common.cgroup.CpuCgroup._get_current_cpu_total") + @patch("azurelinuxagent.common.cgroup.CpuCgroup._update_cpu_data") + def test_telemetry_calculations(self, *args): + num_polls = 10 + num_extensions = 1 + num_summarization_values = 7 + + cpu_percent_values = [random.randint(0, 100) for _ in range(num_polls)] + + # only verifying calculations and not validity of the values. + memory_usage_values = [random.randint(0, 8 * 1024 ** 3) for _ in range(num_polls)] + max_memory_usage_values = [random.randint(0, 8 * 1024 ** 3) for _ in range(num_polls)] + + for i in range(num_extensions): + dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) + + dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory", + "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) + + self.assertEqual(2 * num_extensions, len(CGroupsTelemetry._tracked)) + + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") as patch_get_memory_max_usage: + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") as patch_get_memory_usage: + with patch("azurelinuxagent.common.cgroup.CpuCgroup._get_cpu_percent") as patch_get_cpu_percent: + with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: + for i in range(num_polls): + patch_is_active.return_value = True + patch_get_cpu_percent.return_value = cpu_percent_values[i] + patch_get_memory_usage.return_value = memory_usage_values[i] # example 200 MB + patch_get_memory_max_usage.return_value = max_memory_usage_values[i] # example 450 MB + CGroupsTelemetry.poll_all_tracked() + + collected_metrics = CGroupsTelemetry.report_all_tracked() + for i in range(num_extensions): + name = "dummy_extension_{0}".format(i) + + self.assertIn(name, collected_metrics) + self.assertIn("memory", collected_metrics[name]) + self.assertIn("cur_mem", collected_metrics[name]["memory"]) + self.assertIn("max_mem", collected_metrics[name]["memory"]) + self.assertEqual(num_summarization_values, len(collected_metrics[name]["memory"]["cur_mem"])) + self.assertEqual(num_summarization_values, len(collected_metrics[name]["memory"]["max_mem"])) + + self.assertListEqual(generate_metric_list(memory_usage_values), + collected_metrics[name]["memory"]["cur_mem"][0:5]) + self.assertListEqual(generate_metric_list(max_memory_usage_values), + collected_metrics[name]["memory"]["max_mem"][0:5]) + + self.assertIn("cpu", collected_metrics[name]) + self.assertIn("cur_cpu", collected_metrics[name]["cpu"]) + self.assertEqual(num_summarization_values, len(collected_metrics[name]["cpu"]["cur_cpu"])) + self.assertListEqual(generate_metric_list(cpu_percent_values), + collected_metrics[name]["cpu"]["cur_cpu"][0:5]) + + # mocking get_proc_stat to make it run on Mac and other systems + # this test does not need to read the values of the /proc/stat file + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_stat") + def test_cgroup_tracking(self, *args): + num_extensions = 5 + num_controllers = 2 + for i in range(num_extensions): + dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) + + dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory", + "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) + + for i in range(num_extensions): + self.assertTrue(CGroupsTelemetry.is_tracked("dummy_cpu_path_{0}".format(i))) + self.assertTrue(CGroupsTelemetry.is_tracked("dummy_memory_path_{0}".format(i))) + + self.assertEqual(num_extensions * num_controllers, len(CGroupsTelemetry._tracked)) + + # mocking get_proc_stat to make it run on Mac and other systems + # this test does not need to read the values of the /proc/stat file + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_stat") + def test_cgroup_pruning(self, *args): + num_extensions = 5 + num_controllers = 2 + for i in range(num_extensions): + dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) + + dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory", + "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) + + for i in range(num_extensions): + self.assertTrue(CGroupsTelemetry.is_tracked("dummy_cpu_path_{0}".format(i))) + self.assertTrue(CGroupsTelemetry.is_tracked("dummy_memory_path_{0}".format(i))) + + self.assertEqual(num_extensions * num_controllers, len(CGroupsTelemetry._tracked)) + + CGroupsTelemetry.prune_all_tracked() + + for i in range(num_extensions): + self.assertFalse(CGroupsTelemetry.is_tracked("dummy_cpu_path_{0}".format(i))) + self.assertFalse(CGroupsTelemetry.is_tracked("dummy_memory_path_{0}".format(i))) + + self.assertEqual(0, len(CGroupsTelemetry._tracked)) + + # mocking get_proc_stat to make it run on Mac and other systems + # this test does not need to read the values of the /proc/stat file + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_stat") + def test_cgroup_is_tracked(self, *args): + num_extensions = 5 + for i in range(num_extensions): + dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) + + dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory", "dummy_extension_{0}". + format(i)) + CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) + + for i in range(num_extensions): + self.assertTrue(CGroupsTelemetry.is_tracked("dummy_cpu_path_{0}".format(i))) + self.assertTrue(CGroupsTelemetry.is_tracked("dummy_memory_path_{0}".format(i))) + + self.assertFalse(CGroupsTelemetry.is_tracked("not_present_cpu_dummy_path")) + self.assertFalse(CGroupsTelemetry.is_tracked("not_present_memory_dummy_path")) + + # mocking get_proc_stat to make it run on Mac and other systems + # this test does not need to read the values of the /proc/stat file + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_stat") + def test_process_cgroup_metric_with_incorrect_cgroups_mounted(self, *args): + num_extensions = 5 + for i in range(num_extensions): + dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) + + dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory", + "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) + + with patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage") as patch_get_cpu_usage: + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") as patch_get_memory_usage: + patch_get_cpu_usage.side_effect = Exception("File not found") + patch_get_memory_usage.side_effect = Exception("File not found") + + for data_count in range(1, 10): + CGroupsTelemetry.poll_all_tracked() + + self.assertEqual(CGroupsTelemetry._cgroup_metrics.__len__(), num_extensions) + + collected_metrics = {} + for name, cgroup_metrics in CGroupsTelemetry._cgroup_metrics.items(): + collected_metrics[name] = CGroupsTelemetry._process_cgroup_metric(cgroup_metrics) + self.assertEqual(collected_metrics[name], {}) # empty + + @patch("azurelinuxagent.common.cgroup.CpuCgroup._get_current_cpu_total") + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_total_cpu_ticks_since_boot") + def test_process_cgroup_metric_with_no_memory_cgroup_mounted(self, *args): + num_extensions = 5 + + for i in range(num_extensions): + dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) + + dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory", + "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) + + with patch("azurelinuxagent.common.cgroup.CpuCgroup._get_cpu_percent") as patch_get_cpu_percent: + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") as patch_get_memory_usage: + with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: + patch_is_active.return_value = True + patch_get_memory_usage.side_effect = Exception("File not found") + + current_cpu = 30 + patch_get_cpu_percent.return_value = current_cpu + + poll_count = 1 + + for data_count in range(poll_count, 10): + CGroupsTelemetry.poll_all_tracked() + + self.assertEqual(CGroupsTelemetry._cgroup_metrics.__len__(), num_extensions) + self._assert_cgroup_metrics_equal(cpu_usage=[current_cpu] * data_count, memory_usage=[], max_memory_usage=[]) + + CGroupsTelemetry.report_all_tracked() + + self.assertEqual(CGroupsTelemetry._cgroup_metrics.__len__(), num_extensions) + self._assert_cgroup_metrics_equal([], [], []) + + @patch("azurelinuxagent.common.cgroup.CpuCgroup._get_current_cpu_total") + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_total_cpu_ticks_since_boot") + def test_process_cgroup_metric_with_no_cpu_cgroup_mounted(self, *args): + num_extensions = 5 + for i in range(num_extensions): + dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) + + dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory", + "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) + + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") as patch_get_memory_max_usage: + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") as patch_get_memory_usage: + with patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage") as patch_get_cpu_usage: + with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: + patch_is_active.return_value = True + + patch_get_cpu_usage.side_effect = Exception("File not found") + + current_memory = 209715200 + current_max_memory = 471859200 + + patch_get_memory_usage.return_value = current_memory # example 200 MB + patch_get_memory_max_usage.return_value = current_max_memory # example 450 MB + + poll_count = 1 + + for data_count in range(poll_count, 10): + CGroupsTelemetry.poll_all_tracked() + self.assertEqual(len(CGroupsTelemetry._cgroup_metrics), num_extensions) + self._assert_cgroup_metrics_equal( + cpu_usage=[], + memory_usage=[current_memory] * data_count, + max_memory_usage=[current_max_memory] * data_count) + + CGroupsTelemetry.report_all_tracked() + + self.assertEqual(len(CGroupsTelemetry._cgroup_metrics), num_extensions) + self._assert_cgroup_metrics_equal([], [], []) + + @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") + @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") + @patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage") + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_total_cpu_ticks_since_boot") + def test_extension_temetry_not_sent_for_empty_perf_metrics(self, *args): + num_extensions = 5 + for i in range(num_extensions): + dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) + + dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory", + "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) + + with patch("azurelinuxagent.common.cgroupstelemetry.CGroupsTelemetry._process_cgroup_metric") as \ + patch_process_cgroup_metric: + with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: + + patch_is_active.return_value = False + patch_process_cgroup_metric.return_value = {} + poll_count = 1 + + for data_count in range(poll_count, 10): + CGroupsTelemetry.poll_all_tracked() + + collected_metrics = CGroupsTelemetry.report_all_tracked() + self.assertEqual(0, len(collected_metrics)) + + @skip_if_predicate_false(are_cgroups_enabled, "Does not run when Cgroups are not enabled") + @skip_if_predicate_true(is_trusty_in_travis, "Does not run on Trusty in Travis") + @attr('requires_sudo') + def test_telemetry_with_tracked_cgroup(self): + self.assertTrue(i_am_root(), "Test does not run when non-root") + + # This test has some timing issues when systemd is managing cgroups, so we force the file system API + # by creating a new instance of the CGroupConfigurator + with patch("azurelinuxagent.common.cgroupapi.CGroupsApi._is_systemd", return_value=False): + cgroup_configurator_instance = CGroupConfigurator._instance + CGroupConfigurator._instance = None + + try: + max_num_polls = 30 + time_to_wait = 3 + extn_name = "foobar-1.0.0" + num_summarization_values = 7 + + cgs = make_new_cgroup(extn_name) + self.assertEqual(len(cgs), 2) + + ext_handler_properties = ExtHandlerProperties() + ext_handler_properties.version = "1.0.0" + self.ext_handler = ExtHandler(name='foobar') + self.ext_handler.properties = ext_handler_properties + self.ext_handler_instance = ExtHandlerInstance(ext_handler=self.ext_handler, protocol=None) + + command = self.create_script("keep_cpu_busy_and_consume_memory_for_5_seconds", ''' +nohup python -c "import time + +for i in range(5): + x = [1, 2, 3, 4, 5] * (i * 1000) + time.sleep({0}) + x *= 0 + print('Test loop')" & +'''.format(time_to_wait)) + + self.log_dir = os.path.join(self.tmp_dir, "log") + + with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.get_base_dir", lambda *_: self.tmp_dir) as \ + patch_get_base_dir: + with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.get_log_dir", lambda *_: self.log_dir) as \ + patch_get_log_dir: + self.ext_handler_instance.launch_command(command) + + # + # If the test is made to run using the systemd API, then the paths of the cgroups need to be checked differently: + # + # self.assertEquals(len(CGroupsTelemetry._tracked), 2) + # cpu = os.path.join(BASE_CGROUPS, "cpu", "system.slice", r"foobar_1.0.0_.*\.scope") + # self.assertTrue(any(re.match(cpu, tracked.path) for tracked in CGroupsTelemetry._tracked)) + # memory = os.path.join(BASE_CGROUPS, "memory", "system.slice", r"foobar_1.0.0_.*\.scope") + # self.assertTrue(any(re.match(memory, tracked.path) for tracked in CGroupsTelemetry._tracked)) + # + self.assertTrue(CGroupsTelemetry.is_tracked(os.path.join( + BASE_CGROUPS, "cpu", "walinuxagent.extensions", "foobar_1.0.0"))) + self.assertTrue(CGroupsTelemetry.is_tracked(os.path.join( + BASE_CGROUPS, "memory", "walinuxagent.extensions", "foobar_1.0.0"))) + + for i in range(max_num_polls): + CGroupsTelemetry.poll_all_tracked() + time.sleep(0.5) + + collected_metrics = CGroupsTelemetry.report_all_tracked() + + self.assertIn("memory", collected_metrics[extn_name]) + self.assertIn("cur_mem", collected_metrics[extn_name]["memory"]) + self.assertIn("max_mem", collected_metrics[extn_name]["memory"]) + self.assertEqual(len(collected_metrics[extn_name]["memory"]["cur_mem"]), num_summarization_values) + self.assertEqual(len(collected_metrics[extn_name]["memory"]["max_mem"]), num_summarization_values) + + self.assertIsInstance(collected_metrics[extn_name]["memory"]["cur_mem"][5], str) + self.assertIsInstance(collected_metrics[extn_name]["memory"]["cur_mem"][6], str) + self.assertIsInstance(collected_metrics[extn_name]["memory"]["max_mem"][5], str) + self.assertIsInstance(collected_metrics[extn_name]["memory"]["max_mem"][6], str) + + self.assertIn("cpu", collected_metrics[extn_name]) + self.assertIn("cur_cpu", collected_metrics[extn_name]["cpu"]) + self.assertEqual(len(collected_metrics[extn_name]["cpu"]["cur_cpu"]), num_summarization_values) + + self.assertIsInstance(collected_metrics[extn_name]["cpu"]["cur_cpu"][5], str) + self.assertIsInstance(collected_metrics[extn_name]["cpu"]["cur_cpu"][6], str) + + for i in range(5): + self.assertGreater(collected_metrics[extn_name]["memory"]["cur_mem"][i], 0) + self.assertGreater(collected_metrics[extn_name]["memory"]["max_mem"][i], 0) + self.assertGreaterEqual(collected_metrics[extn_name]["cpu"]["cur_cpu"][i], 0) + # Equal because CPU could be zero for minimum value. + finally: + CGroupConfigurator._instance = cgroup_configurator_instance + + +class TestMetric(AgentTestCase): + def test_empty_metrics(self): + test_metric = Metric() + self.assertEqual("None", test_metric.first_poll_time()) + self.assertEqual("None", test_metric.last_poll_time()) + self.assertEqual(0, test_metric.count()) + self.assertEqual(None, test_metric.median()) + self.assertEqual(None, test_metric.max()) + self.assertEqual(None, test_metric.min()) + self.assertEqual(None, test_metric.average()) + + def test_metrics(self): + num_polls = 10 + + test_values = [random.randint(0, 100) for _ in range(num_polls)] + + test_metric = Metric() + for value in test_values: + test_metric.append(value) + + self.assertListEqual(generate_metric_list(test_values), [test_metric.average(), test_metric.min(), + test_metric.max(), test_metric.median(), + test_metric.count()]) + + test_metric.clear() + self.assertEqual("None", test_metric.first_poll_time()) + self.assertEqual("None", test_metric.last_poll_time()) + self.assertEqual(0, test_metric.count()) + self.assertEqual(None, test_metric.median()) + self.assertEqual(None, test_metric.max()) + self.assertEqual(None, test_metric.min()) + self.assertEqual(None, test_metric.average()) diff -Nru waagent-2.2.34/tests/common/test_conf.py waagent-2.2.45/tests/common/test_conf.py --- waagent-2.2.34/tests/common/test_conf.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/common/test_conf.py 2019-11-07 00:36:56.000000000 +0000 @@ -28,8 +28,7 @@ # -- These values *MUST* match those from data/test_waagent.conf EXPECTED_CONFIGURATION = { "Extensions.Enabled": True, - "Provisioning.Enabled": True, - "Provisioning.UseCloudInit": True, + "Provisioning.Agent": "auto", "Provisioning.DeleteRootPassword": True, "Provisioning.RegenerateSshHostKeyPair": True, "Provisioning.SshHostKeyPairType": "rsa", @@ -106,8 +105,8 @@ def test_get_fips_enabled(self): self.assertTrue(get_fips_enabled(self.conf)) - def test_get_provision_cloudinit(self): - self.assertTrue(get_provision_cloudinit(self.conf)) + def test_get_provision_agent(self): + self.assertTrue(get_provisioning_agent(self.conf) == 'auto') def test_get_configuration(self): configuration = conf.get_configuration(self.conf) diff -Nru waagent-2.2.34/tests/common/test_event.py waagent-2.2.45/tests/common/test_event.py --- waagent-2.2.34/tests/common/test_event.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/common/test_event.py 2019-11-07 00:36:56.000000000 +0000 @@ -17,19 +17,89 @@ from __future__ import print_function +import json +import os +import threading from datetime import datetime, timedelta +from mock import patch, Mock + +from azurelinuxagent.common import event, logger from azurelinuxagent.common.event import add_event, \ WALAEventOperation, elapsed_milliseconds +from azurelinuxagent.common.exception import EventError from azurelinuxagent.common.future import ustr -from azurelinuxagent.common.version import CURRENT_VERSION +from azurelinuxagent.common.protocol.wire import GoalState +from azurelinuxagent.common.utils import fileutil +from azurelinuxagent.common.utils.extensionprocessutil import read_output +from azurelinuxagent.common.version import CURRENT_VERSION, CURRENT_AGENT +from azurelinuxagent.ga.monitor import MonitorHandler +from tests.tools import AgentTestCase, load_data, data_dir -from tests.tools import * -import azurelinuxagent.common.event as event +class TestEvent(AgentTestCase): + def test_add_event_should_read_container_id_from_process_environment(self): + tmp_file = os.path.join(self.tmp_dir, "tmp_file") + def patch_save_event(json_data): + fileutil.write_file(tmp_file, json_data) + + with patch("azurelinuxagent.common.event.EventLogger.save_event", side_effect=patch_save_event): + # No container id is set + os.environ.pop(event.CONTAINER_ID_ENV_VARIABLE, None) + event.add_event(name='dummy_name') + data = fileutil.read_file(tmp_file) + self.assertTrue('{"name": "ContainerId", "value": "UNINITIALIZED"}' in data or + '{"value": "UNINITIALIZED", "name": "ContainerId"}' in data) + + # Container id is set as an environment variable explicitly + os.environ[event.CONTAINER_ID_ENV_VARIABLE] = '424242' + event.add_event(name='dummy_name') + data = fileutil.read_file(tmp_file) + self.assertTrue('{{"name": "ContainerId", "value": "{0}"}}'.format( + os.environ[event.CONTAINER_ID_ENV_VARIABLE]) in data or + '{{"value": "{0}", "name": "ContainerId"}}'.format( + os.environ[event.CONTAINER_ID_ENV_VARIABLE]) in data) + + # Container id is set as an environment variable when parsing the goal state + xml_text = load_data("wire/goal_state.xml") + goal_state = GoalState(xml_text) + + container_id = goal_state.container_id + event.add_event(name='dummy_name') + data = fileutil.read_file(tmp_file) + self.assertTrue('{{"name": "ContainerId", "value": "{0}"}}'.format(container_id) in data or + '{{"value": "{0}", "name": "ContainerId"}}'.format(container_id), data) + + # Container id is updated as the goal state changes, both in telemetry event and in environment variables + new_container_id = "z6d5526c-5ac2-4200-b6e2-56f2b70c5ab2" + xml_text = load_data("wire/goal_state.xml") + xml_text_updated = xml_text.replace("c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2", new_container_id) + goal_state = GoalState(xml_text_updated) + + event.add_event(name='dummy_name') + data = fileutil.read_file(tmp_file) + + # Assert both the environment variable and telemetry event got updated + self.assertEquals(os.environ[event.CONTAINER_ID_ENV_VARIABLE], new_container_id) + self.assertTrue('{{"name": "ContainerId", "value": "{0}"}}'.format(new_container_id) in data or + '{{"value": "{0}", "name": "ContainerId"}}'.format(new_container_id), data) + + os.environ.pop(event.CONTAINER_ID_ENV_VARIABLE) + + def test_add_event_should_handle_event_errors(self): + with patch("azurelinuxagent.common.utils.fileutil.mkdir", side_effect=OSError): + with patch('azurelinuxagent.common.logger.periodic_error') as mock_logger_periodic_error: + add_event('test', message='test event') + + # The event shouldn't have been created + self.assertTrue(len(os.listdir(self.tmp_dir)) == 0) + + # The exception should have been caught and logged + args = mock_logger_periodic_error.call_args + exception_message = args[0][1] + self.assertIn("[EventError] Failed to create events folder", exception_message) -class TestEvent(AgentTestCase): def test_event_status_event_marked(self): es = event.__event_status__ @@ -78,7 +148,6 @@ self.assertTrue(event.should_emit_event("Foo", "1.2", "FauxOperation", True)) self.assertTrue(event.should_emit_event("Foo", "1.2", "FauxOperation", False)) - def test_should_emit_event_handles_known_operations(self): event.__event_status__ = event.EventStatus() @@ -105,6 +174,58 @@ self.assertTrue(event.should_emit_event("Foo", "1.2", op, True)) self.assertFalse(event.should_emit_event("Foo", "1.2", op, False)) + @patch('azurelinuxagent.common.event.EventLogger') + @patch('azurelinuxagent.common.logger.error') + @patch('azurelinuxagent.common.logger.warn') + @patch('azurelinuxagent.common.logger.info') + def test_should_log_errors_if_failed_operation_and_empty_event_dir(self, + mock_logger_info, + mock_logger_warn, + mock_logger_error, + mock_reporter): + mock_reporter.event_dir = None + add_event("dummy name", + version=CURRENT_VERSION, + op=WALAEventOperation.Download, + is_success=False, + message="dummy event message", + reporter=mock_reporter) + + self.assertEquals(1, mock_logger_error.call_count) + self.assertEquals(1, mock_logger_warn.call_count) + self.assertEquals(0, mock_logger_info.call_count) + + args = mock_logger_error.call_args[0] + self.assertEquals(('dummy name', 'Download', 'dummy event message', 0), args[1:]) + + @patch('azurelinuxagent.common.event.EventLogger') + @patch('azurelinuxagent.common.logger.error') + @patch('azurelinuxagent.common.logger.warn') + @patch('azurelinuxagent.common.logger.info') + def test_should_log_errors_if_failed_operation_and_not_empty_event_dir(self, + mock_logger_info, + mock_logger_warn, + mock_logger_error, + mock_reporter): + mock_reporter.event_dir = "dummy" + + with patch("azurelinuxagent.common.event.should_emit_event", return_value=True) as mock_should_emit_event: + with patch("azurelinuxagent.common.event.mark_event_status"): + with patch("azurelinuxagent.common.event.EventLogger._add_event"): + add_event("dummy name", + version=CURRENT_VERSION, + op=WALAEventOperation.Download, + is_success=False, + message="dummy event message") + + self.assertEquals(1, mock_should_emit_event.call_count) + self.assertEquals(1, mock_logger_error.call_count) + self.assertEquals(0, mock_logger_warn.call_count) + self.assertEquals(0, mock_logger_info.call_count) + + args = mock_logger_error.call_args[0] + self.assertEquals(('dummy name', 'Download', 'dummy event message', 0), args[1:]) + @patch('azurelinuxagent.common.event.EventLogger.add_event') def test_periodic_emits_if_not_previously_sent(self, mock_event): event.__event_logger__.reset_periodic() @@ -151,26 +272,121 @@ @patch('azurelinuxagent.common.event.EventLogger.add_event') def test_periodic_forwards_args(self, mock_event): event.__event_logger__.reset_periodic() + event_time = datetime.utcnow().__str__() + event.add_periodic(logger.EVERY_DAY, "FauxEvent", op=WALAEventOperation.Log, is_success=True, duration=0, + version=str(CURRENT_VERSION), message="FauxEventMessage", evt_type="", is_internal=False, + log_event=True, force=False) + mock_event.assert_called_once_with("FauxEvent", op=WALAEventOperation.Log, is_success=True, duration=0, + version=str(CURRENT_VERSION), message="FauxEventMessage", evt_type="", + is_internal=False, log_event=True) - event.add_periodic(logger.EVERY_DAY, "FauxEvent") - mock_event.assert_called_once_with( - "FauxEvent", - duration=0, evt_type='', is_internal=False, is_success=True, - log_event=True, message='', op=WALAEventOperation.Unknown, - version=str(CURRENT_VERSION)) + @patch("azurelinuxagent.common.event.datetime") + @patch('azurelinuxagent.common.event.EventLogger.add_event') + def test_periodic_forwards_args_default_values(self, mock_event, mock_datetime): + event.__event_logger__.reset_periodic() + event.add_periodic(logger.EVERY_DAY, "FauxEvent", message="FauxEventMessage") + mock_event.assert_called_once_with("FauxEvent", op=WALAEventOperation.Unknown, is_success=True, duration=0, + version=str(CURRENT_VERSION), message="FauxEventMessage", evt_type="", + is_internal=False, log_event=True) + + @patch("azurelinuxagent.common.event.EventLogger.add_event") + def test_add_event_default_variables(self, mock_add_event): + add_event('test', message='test event') + mock_add_event.assert_called_once_with('test', duration=0, evt_type='', is_internal=False, is_success=True, + log_event=True, message='test event', op=WALAEventOperation.Unknown, + version=str(CURRENT_VERSION)) def test_save_event(self): add_event('test', message='test event') - self.assertTrue(len(os.listdir(self.tmp_dir)) == 2) + self.assertTrue(len(os.listdir(self.tmp_dir)) == 1) + + # checking the extension of the file created. + for filename in os.listdir(self.tmp_dir): + self.assertEqual(".tld", filename[-4:]) + + def test_save_event_message_with_non_ascii_characters(self): + test_data_dir = os.path.join(data_dir, "events", "collect_and_send_extension_stdout_stderror") + msg = "" + + with open(os.path.join(test_data_dir, "dummy_stdout_with_non_ascii_characters"), mode="r+b") as stdout: + with open(os.path.join(test_data_dir, "dummy_stderr_with_non_ascii_characters"), mode="r+b") as stderr: + msg = read_output(stdout, stderr) + + duration = elapsed_milliseconds(datetime.utcnow()) + log_msg = "{0}\n{1}".format("DummyCmd", "\n".join([line for line in msg.split('\n') if line != ""])) + + with patch("azurelinuxagent.common.event.datetime") as patch_datetime: + patch_datetime.utcnow = Mock(return_value=datetime.strptime("2019-01-01 01:30:00", + '%Y-%m-%d %H:%M:%S')) + with patch('os.getpid', return_value=42): + with patch("threading.Thread.getName", return_value="HelloWorldTask"): + add_event('test_extension', message=log_msg, duration=duration) + + for tld_file in os.listdir(self.tmp_dir): + event_str = MonitorHandler.collect_event(os.path.join(self.tmp_dir, tld_file)) + event_json = json.loads(event_str) + + self.assertEqual(len(event_json["parameters"]), 15) + + # Checking the contents passed above, and also validating the default values that were passed in. + for i in event_json["parameters"]: + if i["name"] == "Name": + self.assertEqual(i["value"], "test_extension") + elif i["name"] == "Message": + self.assertEqual(i["value"], log_msg) + elif i["name"] == "Version": + self.assertEqual(i["value"], str(CURRENT_VERSION)) + elif i['name'] == 'IsInternal': + self.assertEqual(i['value'], False) + elif i['name'] == 'Operation': + self.assertEqual(i['value'], 'Unknown') + elif i['name'] == 'OperationSuccess': + self.assertEqual(i['value'], True) + elif i['name'] == 'Duration': + self.assertEqual(i['value'], 0) + elif i['name'] == 'ExtensionType': + self.assertEqual(i['value'], '') + elif i['name'] == 'ContainerId': + self.assertEqual(i['value'], 'UNINITIALIZED') + elif i['name'] == 'OpcodeName': + self.assertEqual(i['value'], '2019-01-01 01:30:00') + elif i['name'] == 'EventTid': + self.assertEqual(i['value'], threading.current_thread().ident) + elif i['name'] == 'EventPid': + self.assertEqual(i['value'], 42) + elif i['name'] == 'TaskName': + self.assertEqual(i['value'], 'HelloWorldTask') + elif i['name'] == 'KeywordName': + self.assertEqual(i['value'], '') + elif i['name'] == 'GAVersion': + self.assertEqual(i['value'], str(CURRENT_AGENT)) + else: + self.assertFalse(True, "Contains a field outside the defaults expected. Field Name: {0}". + format(i['name'])) + + def test_save_event_message_with_decode_errors(self): + tmp_file = os.path.join(self.tmp_dir, "tmp_file") + fileutil.write_file(tmp_file, "This is not JSON data", encoding="utf-16") + + for tld_file in os.listdir(self.tmp_dir): + try: + MonitorHandler.collect_event(os.path.join(self.tmp_dir, tld_file)) + except Exception as e: + self.assertIsInstance(e, EventError) def test_save_event_rollover(self): - add_event('test', message='first event') - for i in range(0, 499): + # We keep 1000 events only, and the older ones are removed. + + num_of_events = 999 + add_event('test', message='first event') # this makes number of events to num_of_events + 1. + for i in range(num_of_events): add_event('test', message='test event {0}'.format(i)) + num_of_events += 1 # adding the first add_event. + events = os.listdir(self.tmp_dir) events.sort() - self.assertTrue(len(events) == 1000) + self.assertTrue(len(events) == num_of_events, "{0} is not equal to {1}".format(len(events), num_of_events)) first_event = os.path.join(self.tmp_dir, events[0]) with open(first_event) as first_fh: @@ -178,14 +394,17 @@ self.assertTrue('first event' in first_event_text) add_event('test', message='last event') + # Adding the above event displaces the first_event + events = os.listdir(self.tmp_dir) events.sort() - self.assertTrue(len(events) == 1000, "{0} events found, 1000 expected".format(len(events))) + self.assertTrue(len(events) == num_of_events, + "{0} events found, {1} expected".format(len(events), num_of_events)) first_event = os.path.join(self.tmp_dir, events[0]) with open(first_event) as first_fh: first_event_text = first_fh.read() - self.assertFalse('first event' in first_event_text) + self.assertFalse('first event' in first_event_text, "'first event' not in {0}".format(first_event_text)) self.assertTrue('test event 0' in first_event_text) last_event = os.path.join(self.tmp_dir, events[-1]) @@ -209,7 +428,7 @@ first_event = os.path.join(self.tmp_dir, events[0]) with open(first_event) as first_fh: first_event_text = first_fh.read() - self.assertTrue('test event 1002' in first_event_text) + self.assertTrue('test event 1001' in first_event_text) last_event = os.path.join(self.tmp_dir, events[-1]) with open(last_event) as last_fh: diff -Nru waagent-2.2.34/tests/common/test_logger.py waagent-2.2.45/tests/common/test_logger.py --- waagent-2.2.34/tests/common/test_logger.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/common/test_logger.py 2019-11-07 00:36:56.000000000 +0000 @@ -18,55 +18,147 @@ import json from datetime import datetime -import azurelinuxagent.common.logger as logger -from azurelinuxagent.common.event import add_log_event -from azurelinuxagent.common.version import CURRENT_AGENT, CURRENT_VERSION +from mock import patch, MagicMock -from tests.tools import * +from azurelinuxagent.common import logger +from azurelinuxagent.common.event import add_log_event +from tests.tools import AgentTestCase -_MSG = "This is our test logging message {0} {1}" +_MSG_INFO = "This is our test info logging message {0} {1}" +_MSG_WARN = "This is our test warn logging message {0} {1}" +_MSG_ERROR = "This is our test error logging message {0} {1}" +_MSG_VERBOSE = "This is our test verbose logging message {0} {1}" _DATA = ["arg1", "arg2"] class TestLogger(AgentTestCase): - @patch('azurelinuxagent.common.logger.Logger.info') - def test_periodic_emits_if_not_previously_sent(self, mock_info): + def setUp(self): + AgentTestCase.setUp(self) logger.reset_periodic() - logger.periodic(logger.EVERY_DAY, _MSG, *_DATA) - self.assertEqual(1, mock_info.call_count) + def tearDown(self): + AgentTestCase.tearDown(self) + logger.reset_periodic() + @patch('azurelinuxagent.common.logger.Logger.verbose') + @patch('azurelinuxagent.common.logger.Logger.warn') + @patch('azurelinuxagent.common.logger.Logger.error') @patch('azurelinuxagent.common.logger.Logger.info') - def test_periodic_does_not_emit_if_previously_sent(self, mock_info): - logger.reset_periodic() + def test_periodic_emits_if_not_previously_sent(self, mock_info, mock_error, mock_warn, mock_verbose): + logger.periodic_info(logger.EVERY_DAY, _MSG_INFO, logger.LogLevel.INFO, *_DATA) + self.assertEqual(1, mock_info.call_count) + + logger.periodic_error(logger.EVERY_DAY, _MSG_ERROR, logger.LogLevel.ERROR, *_DATA) + self.assertEqual(1, mock_error.call_count) + + logger.periodic_warn(logger.EVERY_DAY, _MSG_WARN, logger.LogLevel.WARNING, *_DATA) + self.assertEqual(1, mock_warn.call_count) - logger.periodic(logger.EVERY_DAY, _MSG, *_DATA) + logger.periodic_verbose(logger.EVERY_DAY, _MSG_VERBOSE, logger.LogLevel.VERBOSE, *_DATA) + self.assertEqual(1, mock_verbose.call_count) + + @patch('azurelinuxagent.common.logger.Logger.verbose') + @patch('azurelinuxagent.common.logger.Logger.warn') + @patch('azurelinuxagent.common.logger.Logger.error') + @patch('azurelinuxagent.common.logger.Logger.info') + def test_periodic_does_not_emit_if_previously_sent(self, mock_info, mock_error, mock_warn, mock_verbose): + # The count does not increase from 1 - the first time it sends the data. + logger.periodic_info(logger.EVERY_DAY, _MSG_INFO, *_DATA) + self.assertIn(hash(_MSG_INFO), logger.DEFAULT_LOGGER.periodic_messages) self.assertEqual(1, mock_info.call_count) - logger.periodic(logger.EVERY_DAY, _MSG, *_DATA) + logger.periodic_info(logger.EVERY_DAY, _MSG_INFO, *_DATA) + self.assertIn(hash(_MSG_INFO), logger.DEFAULT_LOGGER.periodic_messages) self.assertEqual(1, mock_info.call_count) + logger.periodic_warn(logger.EVERY_DAY, _MSG_WARN, *_DATA) + self.assertIn(hash(_MSG_WARN), logger.DEFAULT_LOGGER.periodic_messages) + self.assertEqual(1, mock_warn.call_count) + + logger.periodic_warn(logger.EVERY_DAY, _MSG_WARN, *_DATA) + self.assertIn(hash(_MSG_WARN), logger.DEFAULT_LOGGER.periodic_messages) + self.assertEqual(1, mock_warn.call_count) + + logger.periodic_error(logger.EVERY_DAY, _MSG_ERROR, *_DATA) + self.assertIn(hash(_MSG_ERROR), logger.DEFAULT_LOGGER.periodic_messages) + self.assertEqual(1, mock_error.call_count) + + logger.periodic_error(logger.EVERY_DAY, _MSG_ERROR, *_DATA) + self.assertIn(hash(_MSG_ERROR), logger.DEFAULT_LOGGER.periodic_messages) + self.assertEqual(1, mock_error.call_count) + + logger.periodic_verbose(logger.EVERY_DAY, _MSG_VERBOSE, *_DATA) + self.assertIn(hash(_MSG_VERBOSE), logger.DEFAULT_LOGGER.periodic_messages) + self.assertEqual(1, mock_verbose.call_count) + + logger.periodic_verbose(logger.EVERY_DAY, _MSG_VERBOSE, *_DATA) + self.assertIn(hash(_MSG_VERBOSE), logger.DEFAULT_LOGGER.periodic_messages) + self.assertEqual(1, mock_verbose.call_count) + + self.assertEqual(4, len(logger.DEFAULT_LOGGER.periodic_messages)) + + @patch('azurelinuxagent.common.logger.Logger.verbose') + @patch('azurelinuxagent.common.logger.Logger.warn') + @patch('azurelinuxagent.common.logger.Logger.error') @patch('azurelinuxagent.common.logger.Logger.info') - def test_periodic_emits_after_elapsed_delta(self, mock_info): - logger.reset_periodic() - - logger.periodic(logger.EVERY_DAY, _MSG, *_DATA) + def test_periodic_emits_after_elapsed_delta(self, mock_info, mock_error, mock_warn, mock_verbose): + logger.periodic_info(logger.EVERY_DAY, _MSG_INFO, *_DATA) self.assertEqual(1, mock_info.call_count) - logger.periodic(logger.EVERY_DAY, _MSG, *_DATA) + logger.periodic_info(logger.EVERY_DAY, _MSG_INFO, *_DATA) self.assertEqual(1, mock_info.call_count) - logger.DEFAULT_LOGGER.periodic_messages[hash(_MSG)] = \ - datetime.now() - logger.EVERY_DAY - logger.EVERY_HOUR - logger.periodic(logger.EVERY_DAY, _MSG, *_DATA) + logger.DEFAULT_LOGGER.periodic_messages[hash(_MSG_INFO)] = datetime.now() - \ + logger.EVERY_DAY - logger.EVERY_HOUR + logger.periodic_info(logger.EVERY_DAY, _MSG_INFO, *_DATA) + self.assertEqual(2, mock_info.call_count) + + logger.periodic_warn(logger.EVERY_DAY, _MSG_WARN, *_DATA) + self.assertEqual(1, mock_warn.call_count) + logger.periodic_warn(logger.EVERY_DAY, _MSG_WARN, *_DATA) + self.assertEqual(1, mock_warn.call_count) + + logger.DEFAULT_LOGGER.periodic_messages[hash(_MSG_WARN)] = datetime.now() - \ + logger.EVERY_DAY - logger.EVERY_HOUR + logger.periodic_warn(logger.EVERY_DAY, _MSG_WARN, *_DATA) + self.assertEqual(2, mock_info.call_count) + + logger.periodic_error(logger.EVERY_DAY, _MSG_ERROR, *_DATA) + self.assertEqual(1, mock_error.call_count) + logger.periodic_error(logger.EVERY_DAY, _MSG_ERROR, *_DATA) + self.assertEqual(1, mock_error.call_count) + + logger.DEFAULT_LOGGER.periodic_messages[hash(_MSG_ERROR)] = datetime.now() - \ + logger.EVERY_DAY - logger.EVERY_HOUR + logger.periodic_error(logger.EVERY_DAY, _MSG_ERROR, *_DATA) self.assertEqual(2, mock_info.call_count) + logger.periodic_verbose(logger.EVERY_DAY, _MSG_VERBOSE, *_DATA) + self.assertEqual(1, mock_verbose.call_count) + logger.periodic_verbose(logger.EVERY_DAY, _MSG_VERBOSE, *_DATA) + self.assertEqual(1, mock_verbose.call_count) + + logger.DEFAULT_LOGGER.periodic_messages[hash(_MSG_VERBOSE)] = datetime.now() - \ + logger.EVERY_DAY - logger.EVERY_HOUR + logger.periodic_verbose(logger.EVERY_DAY, _MSG_VERBOSE, *_DATA) + self.assertEqual(2, mock_info.call_count) + + @patch('azurelinuxagent.common.logger.Logger.verbose') + @patch('azurelinuxagent.common.logger.Logger.warn') + @patch('azurelinuxagent.common.logger.Logger.error') @patch('azurelinuxagent.common.logger.Logger.info') - def test_periodic_forwards_message_and_args(self, mock_info): - logger.reset_periodic() + def test_periodic_forwards_message_and_args(self, mock_info, mock_error, mock_warn, mock_verbose): + logger.periodic_info(logger.EVERY_DAY, _MSG_INFO, *_DATA) + mock_info.assert_called_once_with(_MSG_INFO, *_DATA) + + logger.periodic_error(logger.EVERY_DAY, _MSG_ERROR, *_DATA) + mock_error.assert_called_once_with(_MSG_ERROR, *_DATA) + + logger.periodic_warn(logger.EVERY_DAY, _MSG_WARN, *_DATA) + mock_warn.assert_called_once_with(_MSG_WARN, *_DATA) - logger.periodic(logger.EVERY_DAY, _MSG, *_DATA) - mock_info.assert_called_once_with(_MSG, *_DATA) + logger.periodic_verbose(logger.EVERY_DAY, _MSG_VERBOSE, *_DATA) + mock_verbose.assert_called_once_with(_MSG_VERBOSE, *_DATA) def test_telemetry_logger(self): mock = MagicMock() @@ -86,7 +178,7 @@ self.assertEqual('FFF0196F-EE4C-4EAF-9AA5-776F622DEB4F', telemetry_json['providerId']) self.assertEqual(7, telemetry_json['eventId']) - self.assertEqual(5, len(telemetry_json['parameters'])) + self.assertEqual(12, len(telemetry_json['parameters'])) for x in telemetry_json['parameters']: if x['name'] == 'EventName': self.assertEqual(x['value'], 'Log') diff -Nru waagent-2.2.34/tests/common/test_telemetryevent.py waagent-2.2.45/tests/common/test_telemetryevent.py --- waagent-2.2.34/tests/common/test_telemetryevent.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/common/test_telemetryevent.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,49 @@ +# Copyright 2019 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +from azurelinuxagent.common.telemetryevent import TelemetryEvent, TelemetryEventParam +from tests.tools import AgentTestCase + + +def get_test_event(name="DummyExtension", op="Unknown", is_success=True, duration=0, version="foo", evt_type="", is_internal=False, + message="DummyMessage", eventId=1): + event = TelemetryEvent(eventId, "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") + event.parameters.append(TelemetryEventParam('Name', name)) + event.parameters.append(TelemetryEventParam('Version', str(version))) + event.parameters.append(TelemetryEventParam('IsInternal', is_internal)) + event.parameters.append(TelemetryEventParam('Operation', op)) + event.parameters.append(TelemetryEventParam('OperationSuccess', is_success)) + event.parameters.append(TelemetryEventParam('Message', message)) + event.parameters.append(TelemetryEventParam('Duration', duration)) + event.parameters.append(TelemetryEventParam('ExtensionType', evt_type)) + return event + + +class TestTelemetryEvent(AgentTestCase): + def test_contains_works_for_TelemetryEvent(self): + test_event = get_test_event(message="Dummy Event") + + self.assertTrue('Name' in test_event) + self.assertTrue('Version' in test_event) + self.assertTrue('IsInternal' in test_event) + self.assertTrue('Operation' in test_event) + self.assertTrue('OperationSuccess' in test_event) + self.assertTrue('Message' in test_event) + self.assertTrue('Duration' in test_event) + self.assertTrue('ExtensionType' in test_event) + + self.assertFalse('GAVersion' in test_event) + self.assertFalse('ContainerId' in test_event) \ No newline at end of file diff -Nru waagent-2.2.34/tests/common/test_version.py waagent-2.2.45/tests/common/test_version.py --- waagent-2.2.34/tests/common/test_version.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/common/test_version.py 2019-11-07 00:36:56.000000000 +0000 @@ -50,6 +50,7 @@ def default_system_no_linux_distro(): return '', '', '' + def default_system_exception(): raise Exception diff -Nru waagent-2.2.34/tests/daemon/test_daemon.py waagent-2.2.45/tests/daemon/test_daemon.py --- waagent-2.2.34/tests/daemon/test_daemon.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/daemon/test_daemon.py 2019-11-07 00:36:56.000000000 +0000 @@ -85,49 +85,50 @@ daemon_handler.run() self.assertFalse(OPENSSL_FIPS_ENVIRONMENT in os.environ) + @patch('azurelinuxagent.common.conf.get_provisioning_agent', return_value='waagent') @patch('azurelinuxagent.ga.update.UpdateHandler.run_latest') @patch('azurelinuxagent.pa.provision.default.ProvisionHandler.run') - @patch('azurelinuxagent.pa.provision.get_provision_handler', return_value=ProvisionHandler()) - def test_daemon_agent_enabled(self, _, patch_run_provision, patch_run_latest): + def test_daemon_agent_enabled(self, patch_run_provision, patch_run_latest, gpa): """ Agent should run normally when no disable_agent is found """ + with patch('azurelinuxagent.pa.provision.get_provision_handler', return_value=ProvisionHandler()): + self.assertFalse(os.path.exists(conf.get_disable_agent_file_path())) + daemon_handler = get_daemon_handler() - self.assertFalse(os.path.exists(conf.get_disable_agent_file_path())) - daemon_handler = get_daemon_handler() + def stop_daemon(child_args): + daemon_handler.running = False - def stop_daemon(child_args): - daemon_handler.running = False - - patch_run_latest.side_effect = stop_daemon - daemon_handler.run() + patch_run_latest.side_effect = stop_daemon + daemon_handler.run() - self.assertEqual(1, patch_run_provision.call_count) - self.assertEqual(1, patch_run_latest.call_count) + self.assertEqual(1, patch_run_provision.call_count) + self.assertEqual(1, patch_run_latest.call_count) + @patch('azurelinuxagent.common.conf.get_provisioning_agent', return_value='waagent') @patch('azurelinuxagent.ga.update.UpdateHandler.run_latest', side_effect=AgentTestCase.fail) @patch('azurelinuxagent.pa.provision.default.ProvisionHandler.run', side_effect=ProvisionHandler.write_agent_disabled) - @patch('azurelinuxagent.pa.provision.get_provision_handler', return_value=ProvisionHandler()) - def test_daemon_agent_disabled(self, _, __, patch_run_latest): + def test_daemon_agent_disabled(self, _, patch_run_latest, gpa): """ Agent should provision, then sleep forever when disable_agent is found """ - # file is created by provisioning handler - self.assertFalse(os.path.exists(conf.get_disable_agent_file_path())) - daemon_handler = get_daemon_handler() - - # we need to assert this thread will sleep forever, so fork it - daemon = Process(target=daemon_handler.run) - daemon.start() - daemon.join(timeout=5) - - self.assertTrue(daemon.is_alive()) - daemon.terminate() - - # disable_agent was written, run_latest was not called - self.assertTrue(os.path.exists(conf.get_disable_agent_file_path())) - self.assertEqual(0, patch_run_latest.call_count) + with patch('azurelinuxagent.pa.provision.get_provision_handler', return_value=ProvisionHandler()): + # file is created by provisioning handler + self.assertFalse(os.path.exists(conf.get_disable_agent_file_path())) + daemon_handler = get_daemon_handler() + + # we need to assert this thread will sleep forever, so fork it + daemon = Process(target=daemon_handler.run) + daemon.start() + daemon.join(timeout=5) + + self.assertTrue(daemon.is_alive()) + daemon.terminate() + + # disable_agent was written, run_latest was not called + self.assertTrue(os.path.exists(conf.get_disable_agent_file_path())) + self.assertEqual(0, patch_run_latest.call_count) if __name__ == '__main__': diff -Nru waagent-2.2.34/tests/data/cgroups/cpu_mount/cpuacct.stat waagent-2.2.45/tests/data/cgroups/cpu_mount/cpuacct.stat --- waagent-2.2.34/tests/data/cgroups/cpu_mount/cpuacct.stat 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/cgroups/cpu_mount/cpuacct.stat 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,2 @@ +user 50000 +system 100000 diff -Nru waagent-2.2.34/tests/data/cgroups/dummy_proc_stat waagent-2.2.45/tests/data/cgroups/dummy_proc_stat --- waagent-2.2.34/tests/data/cgroups/dummy_proc_stat 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/cgroups/dummy_proc_stat 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,12 @@ +cpu 40362 2657 5493 349635 341 0 938 0 0 0 +cpu0 10043 1084 1319 86971 129 0 369 0 0 0 +cpu1 10069 653 1244 87708 51 0 202 0 0 0 +cpu2 10416 528 1492 87075 86 0 239 0 0 0 +cpu3 9833 391 1436 87878 73 0 126 0 0 0 +intr 1202440 15 1020 0 0 0 0 0 0 1 579 0 0 85138 0 0 0 0 0 0 0 0 0 0 35 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 572 65513 499 34 53368 177617 392 149 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +ctxt 2294069 +btime 1562092648 +processes 3715 +procs_running 1 +procs_blocked 0 +softirq 1244004 191505 366613 7 1187 62878 0 1006 328256 2205 290347 \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/cgroups/dummy_proc_stat_updated waagent-2.2.45/tests/data/cgroups/dummy_proc_stat_updated --- waagent-2.2.34/tests/data/cgroups/dummy_proc_stat_updated 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/cgroups/dummy_proc_stat_updated 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,12 @@ +cpu 286534 2705 70195 2971012 1358 0 11637 0 0 0 +cpu0 73053 1096 18020 739721 460 0 3510 0 0 0 +cpu1 70934 664 16722 745032 184 0 2552 0 0 0 +cpu2 74991 539 17715 739096 505 0 3128 0 0 0 +cpu3 67554 405 17736 747162 208 0 2446 0 0 0 +intr 16171532 15 6790 0 0 0 0 0 0 1 2254 0 0 550798 0 0 0 0 0 0 0 0 0 0 35 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 50645 129322 4209 34 458202 1721987 504 149 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +ctxt 36112837 +btime 1562092648 +processes 27949 +procs_running 2 +procs_blocked 1 +softirq 17121870 1838273 5563635 21 8079 119728 0 6931 5982692 2629 3599882 \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/cgroups/memory_mount/memory.max_usage_in_bytes waagent-2.2.45/tests/data/cgroups/memory_mount/memory.max_usage_in_bytes --- waagent-2.2.34/tests/data/cgroups/memory_mount/memory.max_usage_in_bytes 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/cgroups/memory_mount/memory.max_usage_in_bytes 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1 @@ +1000000 \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/cgroups/memory_mount/memory.usage_in_bytes waagent-2.2.45/tests/data/cgroups/memory_mount/memory.usage_in_bytes --- waagent-2.2.34/tests/data/cgroups/memory_mount/memory.usage_in_bytes 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/cgroups/memory_mount/memory.usage_in_bytes 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1 @@ +100000 \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/events/collect_and_send_events_invalid_data/1560752429123264-1.tld waagent-2.2.45/tests/data/events/collect_and_send_events_invalid_data/1560752429123264-1.tld --- waagent-2.2.34/tests/data/events/collect_and_send_events_invalid_data/1560752429123264-1.tld 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/events/collect_and_send_events_invalid_data/1560752429123264-1.tld 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,32 @@ + { + "eventId": 1, + "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", + "parameters": [ + { + "name": "Name", + "value": "Microsoft.EnterpriseCloud.Monitoring.OmsAgentForLinux" + }, + { + + "name": "Version", + "value": "1.11.5" + }, + + { + "name": "Operation", + "value": "DscPerformConsistency" + }, + { + "name": "OperationSuccess", + "value": false + }, + { + "name": "Message", + "value": "[0]....{u'PluginName': MI_STRING: u'Common', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'PatchManagement', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'Antimalware', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'SecurityBaseline', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'DockerBaseline', u'Ensure': MI_STRING: u'Absent'}....\n....{u'PluginName': MI_STRING: u'ChangeTracking', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'SoftwareChangeTracking', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'ServiceChangeTracking', u'Ensure': MI_STRING: u'Present'}....\n[-1]: error msg: "Error occurred processing (0, u'nxOMSPlugin', {u'WorkspaceID': MI_STRING: u'5a46ca0a-e748-4262-9f0a-f55a05513d9e', u'Name': MI_STRING: u'SimpleOMSPluginConfiguration', u'Plugins': MI_INSTANCEA: [{u'PluginName': 'Common', u'Ensure': 'Present'}, {u'PluginName': 'PatchManagement', u'Ensure': 'Present'}, {u'PluginName': 'Antimalware', u'Ensure': 'Present'}, {u'PluginName': 'SecurityBaseline', u'Ensure': 'Present'}, {u'PluginName': 'DockerBaseline', u'Ensure': 'Absent'}, {u'PluginName': 'ChangeTracking', u'Ensure': 'Present'}, {u'PluginName': 'SoftwareChangeTracking', u'Ensure': 'Present'}, {u'PluginName': 'ServiceChangeTracking', u'Ensure': 'Present'}]})"\n....{u'PluginName': MI_STRING: u'Common', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'PatchManagement', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'Antimalware', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'SecurityBaseline', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'DockerBaseline', u'Ensure': MI_STRING: u'Absent'}....\n....{u'PluginName': MI_STRING: u'ChangeTracking', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'SoftwareChangeTracking', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'ServiceChangeTracking', u'Ensure': MI_STRING: u'Present'}....\n[0][0][0]Exiting - closing socket\nExiting - closing socket\nExiting - closing socket\nExiting - closing socket\n\n---LOG---\n2019/06/17 06:14:49: DEBUG: Scripts/nxPackage.pyc(251):\nPackageGroup type is \n2019/06/17 06:15:31: WARNING: null(0): EventId=2 Priority=WARNING Job 174DEE2C-0E97-4EEA-9519-6F05A27715B5 : \nDisplaying messages from built-in DSC resources:\n\t WMI channel 1 \n\t ResourceID: \n\t Message : []: [] Starting consistency engine.\n2019/06/17 06:15:31: WARNING: null(0): EventId=2 Priority=WARNING Job 174DEE2C-0E97-4EEA-9519-6F05A27715B5 : \nDisplaying messages from built-in DSC resources:\n\t WMI channel 1 \n\t ResourceID: \n\t Message : []: [] Checking consistency for current configuration.\n2019/06/17 06:15:31: ERROR: Scripts/nxOMSPlugin.pyc(366):\ncopy_all_files failed for src: /opt/microsoft/omsconfig/modules/nxOMSPlugin/DSCResources/MSFT_nxOMSPluginResource/Plugins/Common/conf dest: /etc/opt/microsoft/omsagent/conf/omsagent.d/ with error \n2019/06/17 06:15:31: ERROR: Scripts/nxOMSPlugin.pyc(366):\ncopy_all_files failed for src: /opt/microsoft/omsconfig/modules/nxOMSPlugin/DSCResources/MSFT_nxOMSPluginResource/Plugins/PatchManagement/conf dest: /etc/opt/microsoft/omsagent/conf/omsagent.d/ with error \n2019/06/17 06:15:31: ERROR: Scripts/nxOMSPlugin.pyc(366):\ncopy_all_files failed for src: /opt/microsoft/omsconfig/modules/nxOMSPlugin/DSCResources/MSFT_nxOMSPluginResource/Plugins/Antimalware/conf dest: /etc/opt/microsoft/omsagent/conf/omsagent.d/ with error \n2019/06/17 06:15:31: ERROR: Scripts/nxOMSPlugin.pyc(366):\ncopy_all_files failed for src: /opt/microsoft/omsconfig/modules/nxOMSPlugin/DSCResources/MSFT_nxOMSPluginResource/Plugins/SecurityBaseline/conf dest: /etc/opt/microsoft/omsagent/conf/omsagent.d/ with error \n2019/06/17 06:15:31: ERROR: Scripts/nxOMSPlugin.pyc(366):\ncopy_all_files failed for src: /opt/microsoft/omsconfig/modules/nxOMSPlugin/DSCResources/MSFT_nxOMSPluginResource/Plugins/SoftwareChangeTracking/conf dest: /etc/opt/microsoft/omsagent/conf/omsagent.d/ with error \n2019/06/17 06:15:31: ERROR: Scripts/nxOMSPlugin.pyc(366):\ncopy_all_files failed for src: /opt/microsoft/omsconfig/modules/nxOMSPlugin/DSCResources/MSFT_nxOMSPluginResource/Plugins/ServiceChangeTracking/conf dest: /etc/opt/microsoft/omsagent/conf/omsagent.d/ with error \n2019/06/17 06:15:32: WARNING: null(0): EventId=2 Priority=WARNING Job 174DEE2C-0E97-4EEA-9519-6F05A27715B5 : \nDisplaying messages from built-in DSC resources:\n\t WMI channel 1 \n\t ResourceID: \n\t Message : []: [] Consistency check completed.\n2019/06/17 06:15:32: ERROR: null(0): EventId=1 Priority=ERROR Job 174DEE2C-0E97-4EEA-9519-6F05A27715B5 : \nDSC Engine Error : \n\t Error Message cURL failed to perform on this base url: https://scus-agentservice-prod-1.azure-automation.net/Accounts/5a46ca0a-e748-4262-9f0a-f55a05513d9e/Nodes(AgentId='6446114e-b8a9-410b-af0e-41d4d0ce83b6')/SendReport with this error message: Problem with the local SSL certificate. Make sure cURL and SSL libraries are up to date. \n\tError Code : 1 \n2019/06/17 06:15:32: ERROR: null(0): EventId=1 Priority=ERROR Job 174DEE2C-0E97-4EEA-9519-6F05A27715B5 : \nDSC Engine Error : \n\t Error Message Failed to report to all reporting servers. Last server failed with HTTP response code: 0. \n\tError Code : 1 \n" + }, + { + "name": "Duration", + "value": 300000 + } + ] + } \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/events/collect_and_send_events_invalid_data/1560752429123264.tld waagent-2.2.45/tests/data/events/collect_and_send_events_invalid_data/1560752429123264.tld --- waagent-2.2.34/tests/data/events/collect_and_send_events_invalid_data/1560752429123264.tld 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/events/collect_and_send_events_invalid_data/1560752429123264.tld 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,32 @@ + { + "eventId": 1, + "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", + "parameters": [ + { + "name": "Name", + "value": "Microsoft.EnterpriseCloud.Monitoring.OmsAgentForLinux" + }, + { + + "name": "Version", + "value": "1.11.5" + }, + + { + "name": "Operation", + "value": "DscPerformConsistency" + }, + { + "name": "OperationSuccess", + "value": false + }, + { + "name": "Message", + "value": "[0]....{u'PluginName': MI_STRING: u'Common', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'PatchManagement', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'Antimalware', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'SecurityBaseline', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'DockerBaseline', u'Ensure': MI_STRING: u'Absent'}....\n....{u'PluginName': MI_STRING: u'ChangeTracking', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'SoftwareChangeTracking', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'ServiceChangeTracking', u'Ensure': MI_STRING: u'Present'}....\n[-1]: error msg: "Error occurred processing (0, u'nxOMSPlugin', {u'WorkspaceID': MI_STRING: u'5a46ca0a-e748-4262-9f0a-f55a05513d9e', u'Name': MI_STRING: u'SimpleOMSPluginConfiguration', u'Plugins': MI_INSTANCEA: [{u'PluginName': 'Common', u'Ensure': 'Present'}, {u'PluginName': 'PatchManagement', u'Ensure': 'Present'}, {u'PluginName': 'Antimalware', u'Ensure': 'Present'}, {u'PluginName': 'SecurityBaseline', u'Ensure': 'Present'}, {u'PluginName': 'DockerBaseline', u'Ensure': 'Absent'}, {u'PluginName': 'ChangeTracking', u'Ensure': 'Present'}, {u'PluginName': 'SoftwareChangeTracking', u'Ensure': 'Present'}, {u'PluginName': 'ServiceChangeTracking', u'Ensure': 'Present'}]})"\n....{u'PluginName': MI_STRING: u'Common', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'PatchManagement', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'Antimalware', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'SecurityBaseline', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'DockerBaseline', u'Ensure': MI_STRING: u'Absent'}....\n....{u'PluginName': MI_STRING: u'ChangeTracking', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'SoftwareChangeTracking', u'Ensure': MI_STRING: u'Present'}....\n....{u'PluginName': MI_STRING: u'ServiceChangeTracking', u'Ensure': MI_STRING: u'Present'}....\n[0][0][0]Exiting - closing socket\nExiting - closing socket\nExiting - closing socket\nExiting - closing socket\n\n---LOG---\n2019/06/17 06:14:49: DEBUG: Scripts/nxPackage.pyc(251):\nPackageGroup type is \n2019/06/17 06:15:31: WARNING: null(0): EventId=2 Priority=WARNING Job 174DEE2C-0E97-4EEA-9519-6F05A27715B5 : \nDisplaying messages from built-in DSC resources:\n\t WMI channel 1 \n\t ResourceID: \n\t Message : []: [] Starting consistency engine.\n2019/06/17 06:15:31: WARNING: null(0): EventId=2 Priority=WARNING Job 174DEE2C-0E97-4EEA-9519-6F05A27715B5 : \nDisplaying messages from built-in DSC resources:\n\t WMI channel 1 \n\t ResourceID: \n\t Message : []: [] Checking consistency for current configuration.\n2019/06/17 06:15:31: ERROR: Scripts/nxOMSPlugin.pyc(366):\ncopy_all_files failed for src: /opt/microsoft/omsconfig/modules/nxOMSPlugin/DSCResources/MSFT_nxOMSPluginResource/Plugins/Common/conf dest: /etc/opt/microsoft/omsagent/conf/omsagent.d/ with error \n2019/06/17 06:15:31: ERROR: Scripts/nxOMSPlugin.pyc(366):\ncopy_all_files failed for src: /opt/microsoft/omsconfig/modules/nxOMSPlugin/DSCResources/MSFT_nxOMSPluginResource/Plugins/PatchManagement/conf dest: /etc/opt/microsoft/omsagent/conf/omsagent.d/ with error \n2019/06/17 06:15:31: ERROR: Scripts/nxOMSPlugin.pyc(366):\ncopy_all_files failed for src: /opt/microsoft/omsconfig/modules/nxOMSPlugin/DSCResources/MSFT_nxOMSPluginResource/Plugins/Antimalware/conf dest: /etc/opt/microsoft/omsagent/conf/omsagent.d/ with error \n2019/06/17 06:15:31: ERROR: Scripts/nxOMSPlugin.pyc(366):\ncopy_all_files failed for src: /opt/microsoft/omsconfig/modules/nxOMSPlugin/DSCResources/MSFT_nxOMSPluginResource/Plugins/SecurityBaseline/conf dest: /etc/opt/microsoft/omsagent/conf/omsagent.d/ with error \n2019/06/17 06:15:31: ERROR: Scripts/nxOMSPlugin.pyc(366):\ncopy_all_files failed for src: /opt/microsoft/omsconfig/modules/nxOMSPlugin/DSCResources/MSFT_nxOMSPluginResource/Plugins/SoftwareChangeTracking/conf dest: /etc/opt/microsoft/omsagent/conf/omsagent.d/ with error \n2019/06/17 06:15:31: ERROR: Scripts/nxOMSPlugin.pyc(366):\ncopy_all_files failed for src: /opt/microsoft/omsconfig/modules/nxOMSPlugin/DSCResources/MSFT_nxOMSPluginResource/Plugins/ServiceChangeTracking/conf dest: /etc/opt/microsoft/omsagent/conf/omsagent.d/ with error \n2019/06/17 06:15:32: WARNING: null(0): EventId=2 Priority=WARNING Job 174DEE2C-0E97-4EEA-9519-6F05A27715B5 : \nDisplaying messages from built-in DSC resources:\n\t WMI channel 1 \n\t ResourceID: \n\t Message : []: [] Consistency check completed.\n2019/06/17 06:15:32: ERROR: null(0): EventId=1 Priority=ERROR Job 174DEE2C-0E97-4EEA-9519-6F05A27715B5 : \nDSC Engine Error : \n\t Error Message cURL failed to perform on this base url: https://scus-agentservice-prod-1.azure-automation.net/Accounts/5a46ca0a-e748-4262-9f0a-f55a05513d9e/Nodes(AgentId='6446114e-b8a9-410b-af0e-41d4d0ce83b6')/SendReport with this error message: Problem with the local SSL certificate. Make sure cURL and SSL libraries are up to date. \n\tError Code : 1 \n2019/06/17 06:15:32: ERROR: null(0): EventId=1 Priority=ERROR Job 174DEE2C-0E97-4EEA-9519-6F05A27715B5 : \nDSC Engine Error : \n\t Error Message Failed to report to all reporting servers. Last server failed with HTTP response code: 0. \n\tError Code : 1 \n" + }, + { + "name": "Duration", + "value": 300000 + } + ] + } \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/events/collect_and_send_events_invalid_data/1560752429133818-1.tld waagent-2.2.45/tests/data/events/collect_and_send_events_invalid_data/1560752429133818-1.tld --- waagent-2.2.34/tests/data/events/collect_and_send_events_invalid_data/1560752429133818-1.tld 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/events/collect_and_send_events_invalid_data/1560752429133818-1.tld 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,32 @@ + { + "eventId": 1, + "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", + "parameters": [ + { + "name": "Name", + "value": "Microsoft.EnterpriseCloud.Monitoring.OmsAgentForLinux" + }, + { + + "name": "Version", + "value": "1.11.5" + }, + + { + "name": "Operation", + "value": "DscPerformInventory" + }, + { + "name": "OperationSuccess", + "value": false + }, + { + "name": "Message", + "value": "(0, {'__Inventory': MI_INSTANCEA: [{'Name': MI_STRING: 'gcc-8-base', 'Version': MI_STRING: '8.3.0-6ubuntu1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libgcc1', 'Version': MI_STRING: '1:8.3.0-6ubuntu1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libstdc++6', 'Version': MI_STRING: '8.3.0-6ubuntu1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libnss-systemd', 'Version': MI_STRING: '237-3ubuntu10.22', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libsystemd0', 'Version': MI_STRING: '237-3ubuntu10.22', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libpam-systemd', 'Version': MI_STRING: '237-3ubuntu10.22', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'systemd', 'Version': MI_STRING: '237-3ubuntu10.22', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'udev', 'Version': MI_STRING: '237-3ubuntu10.22', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libudev1', 'Version': MI_STRING: '237-3ubuntu10.22', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'dbus', 'Version': MI_STRING: '1.12.2-1ubuntu1.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libdbus-1-3', 'Version': MI_STRING: '1.12.2-1ubuntu1.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'systemd-sysv', 'Version': MI_STRING: '237-3ubuntu10.22', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libapt-pkg5.0', 'Version': MI_STRING: '1.6.11', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libapt-inst2.0', 'Version': MI_STRING: '1.6.11', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libdb5.3', 'Version': MI_STRING: '5.3.28-13.1ubuntu1.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'apt', 'Version': MI_STRING: '1.6.11', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'apt-utils', 'Version': MI_STRING: '1.6.11', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libpython3.6', 'Version': MI_STRING: '3.6.8-1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libssl1.1', 'Version': MI_STRING: '1.1.1-1ubuntu2.1~18.04.2', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'python3.6', 'Version': MI_STRING: '3.6.8-1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'python3.6-minimal', 'Version': MI_STRING: '3.6.8-1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libpython3.6-stdlib', 'Version': MI_STRING: '3.6.8-1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libpython3.6-minimal', 'Version': MI_STRING: '3.6.8-1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libpython2.7', 'Version': MI_STRING: '2.7.15-4ubuntu4~18.04', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'python2.7', 'Version': MI_STRING: '2.7.15-4ubuntu4~18.04', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libpython2.7-stdlib', 'Version': MI_STRING: '2.7.15-4ubuntu4~18.04', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'python2.7-minimal', 'Version': MI_STRING: '2.7.15-4ubuntu4~18.04', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libpython2.7-minimal', 'Version': MI_STRING: '2.7.15-4ubuntu4~18.04', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'update-notifier-common', 'Version': MI_STRING: '3.192.1.7', 'Architecture': MI_STRING: 'all', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libdw1', 'Version': MI_STRING: '0.170-0.4ubuntu0.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libelf1', 'Version': MI_STRING: '0.170-0.4ubuntu0.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libglib2.0-0', 'Version': MI_STRING: '2.56.4-0ubuntu0.18.04.3', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libglib2.0-data', 'Version': MI_STRING: '2.56.4-0ubuntu0.18.04.3', 'Architecture': MI_STRING: 'all', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'openssl', 'Version': MI_STRING: '1.1.1-1ubuntu2.1~18.04.2', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'xxd', 'Version': MI_STRING: '2:8.0.1453-1ubuntu1.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'vim', 'Version': MI_STRING: '2:8.0.1453-1ubuntu1.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'vim-tiny', 'Version': MI_STRING: '2:8.0.1453-1ubuntu1.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'vim-runtime', 'Version': MI_STRING: '2:8.0.1453-1ubuntu1.1', 'Architecture': MI_STRING: 'all', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'vim-common', 'Version': MI_STRING: '2:8.0.1453-1ubuntu1.1', 'Architecture': MI_STRING: 'all', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'python3-gdbm', 'Version': MI_STRING: '3.6.8-1~18.04', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libcups2', 'Version': MI_STRING: '2.2.7-1ubuntu2.6', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-modules-4.18.0-1019-azure', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-image-4.18.0-1019-azure', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-azure', 'Version': MI_STRING: '4.18.0.1019.18', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-image-azure', 'Version': MI_STRING: '4.18.0.1019.18', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-azure-headers-4.18.0-1019', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'all', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-headers-4.18.0-1019-azure', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-headers-azure', 'Version': MI_STRING: '4.18.0.1019.18', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-azure-tools-4.18.0-1019', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-tools-4.18.0-1019-azure', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-tools-azure', 'Version': MI_STRING: '4.18.0.1019.18', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-azure-cloud-tools-4.18.0-1019', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-cloud-tools-4.18.0-1019-azure', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-cloud-tools-azure', 'Version': MI_STRING: '4.18.0.1019.18', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'python3-cryptography', 'Version': MI_STRING: '2.1.4-1ubuntu1.3', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'python3-jinja2', 'Version': MI_STRING: '2.10-1ubuntu0.18.04.1', 'Architecture': MI_STRING: 'all', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}]})Exiting - closing socket\n\n---LOG---\n2019/06/17 06:16:55: DEBUG: Scripts/nxAvailableUpdates.py(62):\nCompleted checking Available Updates\n2019/06/17 06:17:56: DEBUG: Scripts/nxAvailableUpdates.pyc(51):\nStarting to check Available Updates\n2019/06/17 06:17:58: DEBUG: Scripts/nxAvailableUpdates.pyc(80):\nRetrieving update package list using cmd:LANG=en_US.UTF8 apt-get -s dist-upgrade | grep "^Inst"\n2019/06/17 06:17:59: DEBUG: Scripts/nxAvailableUpdates.pyc(96):\nNumber of packages being written to the XML: 56\n2019/06/17 06:17:59: DEBUG: Scripts/nxAvailableUpdates.pyc(62):\nCompleted checking Available Updates\n" + }, + { + "name": "Duration", + "value": 300000 + } + ] + } \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/events/collect_and_send_events_invalid_data/1560752429133818.tld waagent-2.2.45/tests/data/events/collect_and_send_events_invalid_data/1560752429133818.tld --- waagent-2.2.34/tests/data/events/collect_and_send_events_invalid_data/1560752429133818.tld 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/events/collect_and_send_events_invalid_data/1560752429133818.tld 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,32 @@ + { + "eventId": 1, + "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", + "parameters": [ + { + "name": "Name", + "value": "Microsoft.EnterpriseCloud.Monitoring.OmsAgentForLinux" + }, + { + + "name": "Version", + "value": "1.11.5" + }, + + { + "name": "Operation", + "value": "DscPerformInventory" + }, + { + "name": "OperationSuccess", + "value": false + }, + { + "name": "Message", + "value": "(0, {'__Inventory': MI_INSTANCEA: [{'Name': MI_STRING: 'gcc-8-base', 'Version': MI_STRING: '8.3.0-6ubuntu1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libgcc1', 'Version': MI_STRING: '1:8.3.0-6ubuntu1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libstdc++6', 'Version': MI_STRING: '8.3.0-6ubuntu1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libnss-systemd', 'Version': MI_STRING: '237-3ubuntu10.22', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libsystemd0', 'Version': MI_STRING: '237-3ubuntu10.22', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libpam-systemd', 'Version': MI_STRING: '237-3ubuntu10.22', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'systemd', 'Version': MI_STRING: '237-3ubuntu10.22', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'udev', 'Version': MI_STRING: '237-3ubuntu10.22', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libudev1', 'Version': MI_STRING: '237-3ubuntu10.22', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'dbus', 'Version': MI_STRING: '1.12.2-1ubuntu1.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libdbus-1-3', 'Version': MI_STRING: '1.12.2-1ubuntu1.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'systemd-sysv', 'Version': MI_STRING: '237-3ubuntu10.22', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libapt-pkg5.0', 'Version': MI_STRING: '1.6.11', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libapt-inst2.0', 'Version': MI_STRING: '1.6.11', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libdb5.3', 'Version': MI_STRING: '5.3.28-13.1ubuntu1.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'apt', 'Version': MI_STRING: '1.6.11', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'apt-utils', 'Version': MI_STRING: '1.6.11', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libpython3.6', 'Version': MI_STRING: '3.6.8-1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libssl1.1', 'Version': MI_STRING: '1.1.1-1ubuntu2.1~18.04.2', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'python3.6', 'Version': MI_STRING: '3.6.8-1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'python3.6-minimal', 'Version': MI_STRING: '3.6.8-1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libpython3.6-stdlib', 'Version': MI_STRING: '3.6.8-1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libpython3.6-minimal', 'Version': MI_STRING: '3.6.8-1~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libpython2.7', 'Version': MI_STRING: '2.7.15-4ubuntu4~18.04', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'python2.7', 'Version': MI_STRING: '2.7.15-4ubuntu4~18.04', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libpython2.7-stdlib', 'Version': MI_STRING: '2.7.15-4ubuntu4~18.04', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'python2.7-minimal', 'Version': MI_STRING: '2.7.15-4ubuntu4~18.04', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libpython2.7-minimal', 'Version': MI_STRING: '2.7.15-4ubuntu4~18.04', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'update-notifier-common', 'Version': MI_STRING: '3.192.1.7', 'Architecture': MI_STRING: 'all', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libdw1', 'Version': MI_STRING: '0.170-0.4ubuntu0.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libelf1', 'Version': MI_STRING: '0.170-0.4ubuntu0.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libglib2.0-0', 'Version': MI_STRING: '2.56.4-0ubuntu0.18.04.3', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libglib2.0-data', 'Version': MI_STRING: '2.56.4-0ubuntu0.18.04.3', 'Architecture': MI_STRING: 'all', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'openssl', 'Version': MI_STRING: '1.1.1-1ubuntu2.1~18.04.2', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'xxd', 'Version': MI_STRING: '2:8.0.1453-1ubuntu1.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'vim', 'Version': MI_STRING: '2:8.0.1453-1ubuntu1.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'vim-tiny', 'Version': MI_STRING: '2:8.0.1453-1ubuntu1.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'vim-runtime', 'Version': MI_STRING: '2:8.0.1453-1ubuntu1.1', 'Architecture': MI_STRING: 'all', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'vim-common', 'Version': MI_STRING: '2:8.0.1453-1ubuntu1.1', 'Architecture': MI_STRING: 'all', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'python3-gdbm', 'Version': MI_STRING: '3.6.8-1~18.04', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'libcups2', 'Version': MI_STRING: '2.2.7-1ubuntu2.6', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-modules-4.18.0-1019-azure', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-image-4.18.0-1019-azure', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-azure', 'Version': MI_STRING: '4.18.0.1019.18', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-image-azure', 'Version': MI_STRING: '4.18.0.1019.18', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-azure-headers-4.18.0-1019', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'all', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-headers-4.18.0-1019-azure', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-headers-azure', 'Version': MI_STRING: '4.18.0.1019.18', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-azure-tools-4.18.0-1019', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-tools-4.18.0-1019-azure', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-tools-azure', 'Version': MI_STRING: '4.18.0.1019.18', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-azure-cloud-tools-4.18.0-1019', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-cloud-tools-4.18.0-1019-azure', 'Version': MI_STRING: '4.18.0-1019.19~18.04.1', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'linux-cloud-tools-azure', 'Version': MI_STRING: '4.18.0.1019.18', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'python3-cryptography', 'Version': MI_STRING: '2.1.4-1ubuntu1.3', 'Architecture': MI_STRING: 'amd64', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates', 'BuildDate': MI_STRING: ''}, {'Name': MI_STRING: 'python3-jinja2', 'Version': MI_STRING: '2.10-1ubuntu0.18.04.1', 'Architecture': MI_STRING: 'all', 'Repository': MI_STRING: 'Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security', 'BuildDate': MI_STRING: ''}]})Exiting - closing socket\n\n---LOG---\n2019/06/17 06:16:55: DEBUG: Scripts/nxAvailableUpdates.py(62):\nCompleted checking Available Updates\n2019/06/17 06:17:56: DEBUG: Scripts/nxAvailableUpdates.pyc(51):\nStarting to check Available Updates\n2019/06/17 06:17:58: DEBUG: Scripts/nxAvailableUpdates.pyc(80):\nRetrieving update package list using cmd:LANG=en_US.UTF8 apt-get -s dist-upgrade | grep "^Inst"\n2019/06/17 06:17:59: DEBUG: Scripts/nxAvailableUpdates.pyc(96):\nNumber of packages being written to the XML: 56\n2019/06/17 06:17:59: DEBUG: Scripts/nxAvailableUpdates.pyc(62):\nCompleted checking Available Updates\n" + }, + { + "name": "Duration", + "value": 300000 + } + ] + } \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/events/collect_and_send_events_unreadable_data/IncorrectExtension.tmp waagent-2.2.45/tests/data/events/collect_and_send_events_unreadable_data/IncorrectExtension.tmp --- waagent-2.2.34/tests/data/events/collect_and_send_events_unreadable_data/IncorrectExtension.tmp 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/events/collect_and_send_events_unreadable_data/IncorrectExtension.tmp 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,32 @@ + { + "eventId": 1, + "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", + "parameters": [ + { + "name": "Name", + "value": "Microsoft.EnterpriseCloud.Monitoring.OmsAgentForLinux" + }, + { + + "name": "Version", + "value": "1.11.5" + }, + + { + "name": "Operation", + "value": "Install" + }, + { + "name": "OperationSuccess", + "value": false + }, + { + "name": "Message", + "value": "HelloWorld" + }, + { + "name": "Duration", + "value": 300000 + } + ] + } \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/events/collect_and_send_events_unreadable_data/UnreadableFile.tld waagent-2.2.45/tests/data/events/collect_and_send_events_unreadable_data/UnreadableFile.tld --- waagent-2.2.34/tests/data/events/collect_and_send_events_unreadable_data/UnreadableFile.tld 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/events/collect_and_send_events_unreadable_data/UnreadableFile.tld 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,32 @@ + { + "eventId": 1, + "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", + "parameters": [ + { + "name": "Name", + "value": "Microsoft.EnterpriseCloud.Monitoring.OmsAgentForLinux" + }, + { + + "name": "Version", + "value": "1.11.5" + }, + + { + "name": "Operation", + "value": "Install" + }, + { + "name": "OperationSuccess", + "value": false + }, + { + "name": "Message", + "value": "HelloWorld" + }, + { + "name": "Duration", + "value": 300000 + } + ] + } \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_stderr_with_non_ascii_characters waagent-2.2.45/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_stderr_with_non_ascii_characters --- waagent-2.2.34/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_stderr_with_non_ascii_characters 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_stderr_with_non_ascii_characters 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1 @@ +STDERR Worldעיות אחרותआज"" \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_stdout_with_non_ascii_characters waagent-2.2.45/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_stdout_with_non_ascii_characters --- waagent-2.2.34/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_stdout_with_non_ascii_characters 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_stdout_with_non_ascii_characters 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1 @@ +STDOUT Worldעיות אחרותआज"" \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_valid_stderr waagent-2.2.45/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_valid_stderr --- waagent-2.2.34/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_valid_stderr 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_valid_stderr 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1 @@ +The five boxing wizards jump quickly. \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_valid_stdout waagent-2.2.45/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_valid_stdout --- waagent-2.2.34/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_valid_stdout 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/events/collect_and_send_extension_stdout_stderror/dummy_valid_stdout 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1 @@ +The quick brown fox jumps over the lazy dog. \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/ext/dsc_event.json waagent-2.2.45/tests/data/ext/dsc_event.json --- waagent-2.2.34/tests/data/ext/dsc_event.json 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/ext/dsc_event.json 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,38 @@ +{ + "eventId":"1", + "parameters":[ + { + "name":"Name", + "value":"Microsoft.Azure.GuestConfiguration.DSCAgent" + }, + { + "name":"Version", + "value":"1.18.0" + }, + { + "name":"IsInternal", + "value":true + }, + { + "name":"Operation", + "value":"GuestConfigAgent.Scenario" + }, + { + "name":"OperationSuccess", + "value":true + }, + { + "name":"Message", + "value":"[2019-11-05 10:06:52.688] [PID 11487] [TID 11513] [Timer Manager] [INFO] [89f9cf47-c02d-4774-b21a-abdf2beb3cd9] Run pull refresh for timer 'dsc_refresh_timer'\n" + }, + { + "name":"Duration", + "value":0 + }, + { + "name":"ExtentionType", + "value":"" + } + ], + "providerId":"69B669B9-4AF8-4C50-BDC4-6006FA76E975" +} \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/ext/event.json waagent-2.2.45/tests/data/ext/event.json --- waagent-2.2.34/tests/data/ext/event.json 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/ext/event.json 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,38 @@ +{ + "eventId":1, + "providerId":"69B669B9-4AF8-4C50-BDC4-6006FA76E975", + "parameters":[ + { + "name":"Name", + "value":"CustomScript" + }, + { + "name":"Version", + "value":"1.4.1.0" + }, + { + "name":"IsInternal", + "value":false + }, + { + "name":"Operation", + "value":"RunScript" + }, + { + "name":"OperationSuccess", + "value":true + }, + { + "name":"Message", + "value":"(01302)Script is finished. ---stdout--- hello ---errout--- " + }, + { + "name":"Duration", + "value":0 + }, + { + "name":"ExtensionType", + "value":"" + } + ] +} \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/ext/event.xml waagent-2.2.45/tests/data/ext/event.xml --- waagent-2.2.34/tests/data/ext/event.xml 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/data/ext/event.xml 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ - \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/ext/event_from_agent.json waagent-2.2.45/tests/data/ext/event_from_agent.json --- waagent-2.2.34/tests/data/ext/event_from_agent.json 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/ext/event_from_agent.json 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1 @@ +{"eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [{"name": "Name", "value": "WALinuxAgent"}, {"name": "Version", "value": "2.2.44"}, {"name": "IsInternal", "value": false}, {"name": "Operation", "value": "ProcessGoalState"}, {"name": "OperationSuccess", "value": true}, {"name": "Message", "value": "Incarnation 12"}, {"name": "Duration", "value": 16610}, {"name": "ExtensionType", "value": ""}, {"name": "GAVersion", "value": "WALinuxAgent-2.2.44"}, {"name": "ContainerId", "value": "TEST-CONTAINER-ID-ALREADY-PRESENT-GUID"}, {"name": "OpcodeName", "value": "2019-11-02 01:42:49.188030"}, {"name": "EventTid", "value": 140240384030528}, {"name": "EventPid", "value": 108573}, {"name": "TaskName", "value": "ExtHandler"}, {"name": "KeywordName", "value": ""}]} \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/ext/event_from_extension.xml waagent-2.2.45/tests/data/ext/event_from_extension.xml --- waagent-2.2.34/tests/data/ext/event_from_extension.xml 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/ext/event_from_extension.xml 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff -Nru waagent-2.2.34/tests/data/ext/sample_ext-1.3.0/exit.sh waagent-2.2.45/tests/data/ext/sample_ext-1.3.0/exit.sh --- waagent-2.2.34/tests/data/ext/sample_ext-1.3.0/exit.sh 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/ext/sample_ext-1.3.0/exit.sh 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +exit $* \ No newline at end of file Binary files /srv/release.debian.org/tmp/nJO38969h0/waagent-2.2.34/tests/data/ext/sample_ext-1.3.0.zip and /srv/release.debian.org/tmp/rfssO7vR7q/waagent-2.2.45/tests/data/ext/sample_ext-1.3.0.zip differ Binary files /srv/release.debian.org/tmp/nJO38969h0/waagent-2.2.34/tests/data/ga/WALinuxAgent-2.2.33.zip and /srv/release.debian.org/tmp/rfssO7vR7q/waagent-2.2.45/tests/data/ga/WALinuxAgent-2.2.33.zip differ Binary files /srv/release.debian.org/tmp/nJO38969h0/waagent-2.2.34/tests/data/ga/WALinuxAgent-2.2.45.zip and /srv/release.debian.org/tmp/rfssO7vR7q/waagent-2.2.45/tests/data/ga/WALinuxAgent-2.2.45.zip differ diff -Nru waagent-2.2.34/tests/data/test_waagent.conf waagent-2.2.45/tests/data/test_waagent.conf --- waagent-2.2.34/tests/data/test_waagent.conf 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/data/test_waagent.conf 2019-11-07 00:36:56.000000000 +0000 @@ -8,14 +8,11 @@ FauxKey2=Value2 Value2 FauxKey3=delalloc,rw,noatime,nobarrier,users,mode=777 -# Enable instance creation -Provisioning.Enabled=y - # Enable extension handling Extensions.Enabled=y -# Rely on cloud-init to provision -Provisioning.UseCloudInit=y +# Specify provisioning agent. +Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=y diff -Nru waagent-2.2.34/tests/data/wire/certs_format_not_pfx.xml waagent-2.2.45/tests/data/wire/certs_format_not_pfx.xml --- waagent-2.2.34/tests/data/wire/certs_format_not_pfx.xml 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/wire/certs_format_not_pfx.xml 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,7 @@ + + + 2012-11-30 + 12 + CertificatesNonPfxPackage + NotPFXData + diff -Nru waagent-2.2.34/tests/data/wire/certs_no_format_specified.xml waagent-2.2.45/tests/data/wire/certs_no_format_specified.xml --- waagent-2.2.34/tests/data/wire/certs_no_format_specified.xml 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/wire/certs_no_format_specified.xml 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,81 @@ + + + 2012-11-30 + 12 + + MIINswYJKoZIhvcNAQcDoIINpDCCDaACAQIxggEwMIIBLAIBAoAUvyL+x6GkZXog +QNfsXRZAdD9lc7IwDQYJKoZIhvcNAQEBBQAEggEArhMPepD/RqwdPcHEVqvrdZid +72vXrOCuacRBhwlCGrNlg8oI+vbqmT6CSv6thDpet31ALUzsI4uQHq1EVfV1+pXy +NlYD1CKhBCoJxs2fSPU4rc8fv0qs5JAjnbtW7lhnrqFrXYcyBYjpURKfa9qMYBmj +NdijN+1T4E5qjxPr7zK5Dalp7Cgp9P2diH4Nax2nixotfek3MrEFBaiiegDd+7tE +ux685GWYPqB5Fn4OsDkkYOdb0OE2qzLRrnlCIiBCt8VubWH3kMEmSCxBwSJupmQ8 +sxCWk+sBPQ9gJSt2sIqfx/61F8Lpu6WzP+ZOnMLTUn2wLU/d1FN85HXmnQALzTCC +DGUGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIbEcBfddWPv+AggxAAOAt/kCXiffe +GeJG0P2K9Q18XZS6Rz7Xcz+Kp2PVgqHKRpPjjmB2ufsRO0pM4z/qkHTOdpfacB4h +gz912D9U04hC8mt0fqGNTvRNAFVFLsmo7KXc/a8vfZNrGWEnYn7y1WfP52pqA/Ei +SNFf0NVtMyqg5Gx+hZ/NpWAE5vcmRRdoYyWeg13lhlW96QUxf/W7vY/D5KpAGACI +ok79/XI4eJkbq3Dps0oO/difNcvdkE74EU/GPuL68yR0CdzzafbLxzV+B43TBRgP +jH1hCdRqaspjAaZL5LGfp1QUM8HZIKHuTze/+4dWzS1XR3/ix9q/2QFI7YCuXpuE +un3AFYXE4QX/6kcPklZwh9FqjSie3I5HtC1vczqYVjqT4oHrs8ktkZ7oAzeXaXTF +k6+JQNNa/IyJw24I1MR77q7HlHSSfhXX5cFjVCd/+SiA4HJQjJgeIuXZ+dXmSPdL +9xLbDbtppifFyNaXdlSzcsvepKy0WLF49RmbL7Bnd46ce/gdQ6Midwi2MTnUtapu +tHmu/iJtaUpwXXC0B93PHfAk7Y3SgeY4tl/gKzn9/x5SPAcHiNRtOsNBU8ZThzos +Wh41xMLZavmX8Yfm/XWtl4eU6xfhcRAbJQx7E1ymGEt7xGqyPV7hjqhoB9i3oR5N +itxHgf1+jw/cr7hob+Trd1hFqZO6ePMyWpqUg97G2ThJvWx6cv+KRtTlVA6/r/UH +gRGBArJKBlLpXO6dAHFztT3Y6DFThrus4RItcfA8rltfQcRm8d0nPb4lCa5kRbCx +iudq3djWtTIe64sfk8jsc6ahWYSovM+NmhbpxEUbZVWLVEcHAYOeMbKgXSu5sxNO +JZNeFdzZqDRRY9fGjYNS7DdNOmrMmWKH+KXuMCItpNZsZS/3W7QxAo3ugYLdUylU +Zg8H/BjUGZCGn1rEBAuQX78m0SZ1xHlgHSwJIOmxOJUDHLPHtThfbELY9ec14yi5 +so1aQwhhfhPvF+xuXBrVeTAfhFNYkf2uxcEp7+tgFAc5W0QfT9SBn5vSvIxv+dT4 +7B2Pg1l/zjdsM74g58lmRJeDoz4psAq+Uk7n3ImBhIku9qX632Q1hanjC8D4xM4W +sI/W0ADCuAbY7LmwMpAMdrGg//SJUnBftlom7C9VA3EVf8Eo+OZH9hze+gIgUq+E +iEUL5M4vOHK2ttsYrSkAt8MZzjQiTlDr1yzcg8fDIrqEAi5arjTPz0n2s0NFptNW +lRD+Xz6pCXrnRgR8YSWpxvq3EWSJbZkSEk/eOmah22sFnnBZpDqn9+UArAznXrRi +nYK9w38aMGPKM39ymG8kcbY7jmDZlRgGs2ab0Fdj1jl3CRo5IUatkOJwCEMd/tkB +eXLQ8hspJhpFnVNReX0oithVZir+j36epk9Yn8d1l+YlKmuynjunKl9fhmoq5Q6i +DFzdYpqBV+x9nVhnmPfGyrOkXvGL0X6vmXAEif/4JoOW4IZpyXjgn+VoCJUoae5J +Djl45Bcc2Phrn4HW4Gg/+pIwTFqqZZ2jFrznNdgeIxTGjBrVsyJUeO3BHI0mVLaq +jtjhTshYCI7mXOis9W3ic0RwE8rgdDXOYKHhLVw9c4094P/43utSVXE7UzbEhhLE +Ngb4H5UGrQmPTNbq40tMUMUCej3zIKuVOvamzeE0IwLhkjNrvKhCG1EUhX4uoJKu +DQ++3KVIVeYSv3+78Jfw9F3usAXxX1ICU74/La5DUNjU7DVodLDvCAy5y1jxP3Ic +If6m7aBYVjFSQAcD8PZPeIEl9W4ZnbwyBfSDd11P2a8JcZ7N99GiiH3yS1QgJnAO +g9XAgjT4Gcn7k4lHPHLULgijfiDSvt94Ga4/hse0F0akeZslVN/bygyib7x7Lzmq +JkepRianrvKHbatuxvcajt/d+dxCnr32Q1qCEc5fcgDsjvviRL2tKR0qhuYjn1zR +Vk/fRtYOmlaGBVzUXcjLRAg3gC9+Gy8KvXIDrnHxD+9Ob+DUP9fgbKqMeOzKcCK8 +NSfSQ+tQjBYD5Ku4zAPUQJoRGgx43vXzcl2Z2i3E2otpoH82Kx8S9WlVEUlTtBjQ +QIGM5aR0QUNt8z34t2KWRA8SpP54VzBmEPdwLnzna+PkrGKsKiHVn4K+HfjDp1uW +xyO8VjrolAOYosTPXMpNp2u/FoFxaAPTa/TvmKc0kQ3ED9/sGLS2twDnEccvHP+9 +zzrnzzN3T2CWuXveDpuyuAty3EoAid1nuC86WakSaAZoa8H2QoRgsrkkBCq+K/yl +4FO9wuP+ksZoVq3mEDQ9qv6H4JJEWurfkws3OqrA5gENcLmSUkZie4oqAxeOD4Hh +Zx4ckG5egQYr0PnOd2r7ZbIizv3MKT4RBrfOzrE6cvm9bJEzNWXdDyIxZ/kuoLA6 +zX7gGLdGhg7dqzKqnGtopLAsyM1b/utRtWxOTGO9K9lRxyX82oCVT9Yw0DwwA+cH +Gutg1w7JHrIAYEtY0ezHgxhqMGuuTyJMX9Vr0D+9DdMeBK7hVOeSnxkaQ0f9HvF6 +0XI/2OTIoBSCBpUXjpgsYt7m7n2rFJGJmtqgLAosCAkacHnHLwX0EnzBw3sdDU6Q +jFXUWIDd5xUsNkFDCbspLMFs22hjNI6f/GREwd23Q4ujF8pUIcxcfbs2myjbK45s +tsn/jrkxmKRgwCIeN/H7CM+4GXSkEGLWbiGCxWzWt9wW1F4M7NW9nho3D1Pi2LBL +1ByTmjfo/9u9haWrp53enDLJJbcaslfe+zvo3J70Nnzu3m3oJ3dmUxgJIstG10g3 +lhpUm1ynvx04IFkYJ3kr/QHG/xGS+yh/pMZlwcUSpjEgYFmjFHU4A1Ng4LGI4lnw +5wisay4J884xmDgGfK0sdVQyW5rExIg63yYXp2GskRdDdwvWlFUzPzGgCNXQU96A +ljZfjs2u4IiVCC3uVsNbGqCeSdAl9HC5xKuPNbw5yTxPkeRL1ouSdkBy7rvdFaFf +dMPw6sBRNW8ZFInlgOncR3+xT/rZxru87LCq+3hRN3kw3hvFldrW2QzZSksO759b +pJEP+4fxuG96Wq25fRmzHzE0bdJ+2qF3fp/hy4oRi+eVPa0vHdtkymE4OUFWftb6 ++P++JVOzZ4ZxYA8zyUoJb0YCaxL+Jp/QqiUiH8WZVmYZmswqR48sUUKr7TIvpNbY +6jEH6F7KiZCoWfKH12tUC69iRYx3UT/4Bmsgi3S4yUxfieYRMIwihtpP4i0O+OjB +/DPbb13qj8ZSfXJ+jmF2SRFfFG+2T7NJqm09JvT9UcslVd+vpUySNe9UAlpcvNGZ +2+j180ZU7YAgpwdVwdvqiJxkeVtAsIeqAvIXMFm1PDe7FJB0BiSVZdihB6cjnKBI +dv7Lc1tI2sQe7QSfk+gtionLrEnto+aXF5uVM5LMKi3gLElz7oXEIhn54OeEciB1 +cEmyX3Kb4HMRDMHyJxqJXwxm88RgC6RekoPvstu+AfX/NgSpRj5beaj9XkweJT3H +rKWhkjq4Ghsn1LoodxluMMHd61m47JyoqIP9PBKoW+Na0VUKIVHw9e9YeW0nY1Zi +5qFA/pHPAt9AbEilRay6NEm8P7TTlNo216amc8byPXanoNrqBYZQHhZ93A4yl6jy +RdpYskMivT+Sh1nhZAioKqqTZ3HiFR8hFGspAt5gJc4WLYevmxSicGa6AMyhrkvG +rvOSdjY6JY/NkxtcgeycBX5MLF7uDbhUeqittvmlcrVN6+V+2HIbCCrvtow9pcX9 +EkaaNttj5M0RzjQxogCG+S5TkhCy04YvKIkaGJFi8xO3icdlxgOrKD8lhtbf4UpR +cDuytl70JD95mSUWL53UYjeRf9OsLRJMHQOpS02japkMwCb/ngMCQuUXA8hGkBZL +Xw7RwwPuM1Lx8edMXn5C0E8UK5e0QmI/dVIl2aglXk2oBMBJbnyrbfUPm462SG6u +ke4gQKFmVy2rKICqSkh2DMr0NzeYEUjZ6KbmQcV7sKiFxQ0/ROk8eqkYYxGWUWJv +ylPF1OTLH0AIbGlFPLQO4lMPh05yznZTac4tmowADSHY9RCxad1BjBeine2pj48D +u36OnnuQIsedxt5YC+h1bs+mIvwMVsnMLidse38M/RayCDitEBvL0KeG3vWYzaAL +h0FCZGOW0ilVk8tTF5+XWtsQEp1PpclvkcBMkU3DtBUnlmPSKNfJT0iRr2T0sVW1 +h+249Wj0Bw== + + diff -Nru waagent-2.2.34/tests/data/wire/ext_conf_multiple_extensions.xml waagent-2.2.45/tests/data/wire/ext_conf_multiple_extensions.xml --- waagent-2.2.34/tests/data/wire/ext_conf_multiple_extensions.xml 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/wire/ext_conf_multiple_extensions.xml 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,76 @@ + + + + + Prod + + http://manifest_of_ga.xml + + + + Test + + http://manifest_of_ga.xml + + + + + + + + + + + + + + + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + + + + + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + + + + + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + + + + + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + + + + + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + + + + + https://yuezhatest.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo%3D + + + diff -Nru waagent-2.2.34/tests/data/wire/trans_pub waagent-2.2.45/tests/data/wire/trans_pub --- waagent-2.2.34/tests/data/wire/trans_pub 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/data/wire/trans_pub 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA09wkCR3pXk16iBIqMh5N +c5YLnHMpPK4k+3hhkxVKixTSUjprTAen6DZ8/bbOtWzBb5AnPoBVaiMgSotC6ndb +IJdlO/xFRuUeciOS9f/4n8ZoubPQbknNkikQsvYLwh9AsfYiI+Ur0s5AfTRbvhYV +wrdCpwnorDwZxVp5JdPWvtdBwYyoSNxYmSkougwm/csy58T4kx1tcNQZj4+ztmJy +7wpe8E9opWxzofaOuoFLx62NdvMvKt7NNQPPjmubJEnMI7lKTamiG5iDvfBTKQBQ +9XF3svxadLKrPW/jOs5uqfAEDKivrslH+GNMF+MU693yoUaid+K/ZWfP1exgVNmx +cQIDAQAB +-----END PUBLIC KEY----- diff -Nru waagent-2.2.34/tests/distro/test_resourceDisk.py waagent-2.2.45/tests/distro/test_resourceDisk.py --- waagent-2.2.34/tests/distro/test_resourceDisk.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/distro/test_resourceDisk.py 2019-11-07 00:36:56.000000000 +0000 @@ -18,6 +18,8 @@ # http://msdn.microsoft.com/en-us/library/cc227282%28PROT.10%29.aspx # http://msdn.microsoft.com/en-us/library/cc227259%28PROT.13%29.aspx +import os +import stat import sys from azurelinuxagent.common.utils import shellutil from azurelinuxagent.daemon.resourcedisk import get_resourcedisk_handler @@ -38,6 +40,11 @@ # assert assert os.path.exists(test_file) + # only the owner should have access + mode = os.stat(test_file).st_mode & ( + stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + assert mode == stat.S_IRUSR | stat.S_IWUSR + # cleanup os.remove(test_file) @@ -49,7 +56,7 @@ file_size = 1024 * 128 # execute - if sys.version_info >= (3,3): + if sys.version_info >= (3, 3): with patch("os.posix_fallocate", side_effect=Exception('failure')): get_resourcedisk_handler().mkfile(test_file, file_size) @@ -76,20 +83,20 @@ resource_disk_handler.mkfile(test_file, file_size) # assert - if sys.version_info >= (3,3): + if sys.version_info >= (3, 3): with patch("os.posix_fallocate") as posix_fallocate: self.assertEqual(0, posix_fallocate.call_count) assert run_patch.call_count == 1 assert "dd if" in run_patch.call_args_list[0][0][0] - def test_change_partition_type(self): resource_handler = get_resourcedisk_handler() # test when sfdisk --part-type does not exist with patch.object(shellutil, "run_get_output", side_effect=[[1, ''], [0, '']]) as run_patch: - resource_handler.change_partition_type(suppress_message=True, option_str='') + resource_handler.change_partition_type( + suppress_message=True, option_str='') # assert assert run_patch.call_count == 2 @@ -99,12 +106,42 @@ # test when sfdisk --part-type exists with patch.object(shellutil, "run_get_output", side_effect=[[0, '']]) as run_patch: - resource_handler.change_partition_type(suppress_message=True, option_str='') + resource_handler.change_partition_type( + suppress_message=True, option_str='') # assert assert run_patch.call_count == 1 assert "sfdisk --part-type" in run_patch.call_args_list[0][0][0] + def test_check_existing_swap_file(self): + test_file = os.path.join(self.tmp_dir, 'test_swap_file') + file_size = 1024 * 128 + if os.path.exists(test_file): + os.remove(test_file) + + with open(test_file, "wb") as file: + file.write(bytearray(file_size)) + + os.chmod(test_file, stat.S_ISUID | stat.S_ISGID | stat.S_IRUSR | + stat.S_IWUSR | stat.S_IRWXG | stat.S_IRWXO) # 0o6677 + + def swap_on(_): # mimic the output of "swapon -s" + return [ + "Filename Type Size Used Priority", + "{0} partition 16498684 0 -2".format(test_file) + ] + + with patch.object(shellutil, "run_get_output", side_effect=swap_on): + get_resourcedisk_handler().check_existing_swap_file( + test_file, test_file, file_size) + + # it should remove access from group, others + mode = os.stat(test_file).st_mode & (stat.S_ISUID | stat.S_ISGID | + stat.S_IRWXU | stat.S_IWUSR | stat.S_IRWXG | stat.S_IRWXO) # 0o6777 + assert mode == stat.S_ISUID | stat.S_ISGID | stat.S_IRUSR | stat.S_IWUSR # 0o6600 + + os.remove(test_file) + if __name__ == '__main__': unittest.main() diff -Nru waagent-2.2.34/tests/ga/test_env.py waagent-2.2.45/tests/ga/test_env.py --- waagent-2.2.34/tests/ga/test_env.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/ga/test_env.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,161 @@ +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +from azurelinuxagent.common.osutil.default import DefaultOSUtil, shellutil +from azurelinuxagent.ga.env import EnvHandler +from tests.tools import * + + +class TestEnvHandler(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + # save the original run_command so that mocks can reference it + self.shellutil_run_command = shellutil.run_command + + # save an instance of the original DefaultOSUtil so that mocks can reference it + self.default_osutil = DefaultOSUtil() + + # AgentTestCase.setUp mocks osutil.factory._get_osutil; we override that mock for this class with a new mock + # that always returns the default implementation. + self.mock_get_osutil = patch("azurelinuxagent.common.osutil.factory._get_osutil", return_value=DefaultOSUtil()) + self.mock_get_osutil.start() + + + def tearDown(self): + self.mock_get_osutil.stop() + AgentTestCase.tearDown(self) + + def test_get_dhcp_client_pid_should_return_a_sorted_list_of_pids(self): + with patch("azurelinuxagent.common.utils.shellutil.run_command", return_value="11 9 5 22 4 6"): + pids = EnvHandler().get_dhcp_client_pid() + self.assertEquals(pids, [4, 5, 6, 9, 11, 22]) + + def test_get_dhcp_client_pid_should_return_an_empty_list_and_log_a_warning_when_dhcp_client_is_not_running(self): + with patch("azurelinuxagent.common.osutil.default.shellutil.run_command", side_effect=lambda _: self.shellutil_run_command(["pidof", "non-existing-process"])): + with patch('azurelinuxagent.common.logger.Logger.warn') as mock_warn: + pids = EnvHandler().get_dhcp_client_pid() + + self.assertEquals(pids, []) + + self.assertEquals(mock_warn.call_count, 1) + args, kwargs = mock_warn.call_args + message = args[0] + self.assertEquals("Dhcp client is not running.", message) + + def test_get_dhcp_client_pid_should_return_and_empty_list_and_log_an_error_when_an_invalid_command_is_used(self): + with patch("azurelinuxagent.common.osutil.default.shellutil.run_command", side_effect=lambda _: self.shellutil_run_command(["non-existing-command"])): + with patch('azurelinuxagent.common.logger.Logger.error') as mock_error: + pids = EnvHandler().get_dhcp_client_pid() + + self.assertEquals(pids, []) + + self.assertEquals(mock_error.call_count, 1) + args, kwargs = mock_error.call_args + self.assertIn("Failed to get the PID of the DHCP client", args[0]) + self.assertIn("No such file or directory", args[1]) + + def test_get_dhcp_client_pid_should_not_log_consecutive_errors(self): + env_handler = EnvHandler() + + with patch('azurelinuxagent.common.logger.Logger.warn') as mock_warn: + def assert_warnings(count): + self.assertEquals(mock_warn.call_count, count) + + for call_args in mock_warn.call_args_list: + args, kwargs = call_args + self.assertEquals("Dhcp client is not running.", args[0]) + + with patch("azurelinuxagent.common.osutil.default.shellutil.run_command", side_effect=lambda _: self.shellutil_run_command(["pidof", "non-existing-process"])): + # it should log the first error + pids = env_handler.get_dhcp_client_pid() + self.assertEquals(pids, []) + assert_warnings(1) + + # it should not log subsequent errors + for i in range(0, 3): + pids = env_handler.get_dhcp_client_pid() + self.assertEquals(pids, []) + self.assertEquals(mock_warn.call_count, 1) + + with patch("azurelinuxagent.common.osutil.default.shellutil.run_command", return_value="123"): + # now it should succeed + pids = env_handler.get_dhcp_client_pid() + self.assertEquals(pids, [123]) + assert_warnings(1) + + with patch("azurelinuxagent.common.osutil.default.shellutil.run_command", side_effect=lambda _: self.shellutil_run_command(["pidof", "non-existing-process"])): + # it should log the new error + pids = env_handler.get_dhcp_client_pid() + self.assertEquals(pids, []) + assert_warnings(2) + + # it should not log subsequent errors + for i in range(0, 3): + pids = env_handler.get_dhcp_client_pid() + self.assertEquals(pids, []) + self.assertEquals(mock_warn.call_count, 2) + + def test_handle_dhclient_restart_should_reconfigure_network_routes_when_dhcp_client_restarts(self): + with patch("azurelinuxagent.common.dhcp.DhcpHandler.conf_routes") as mock_conf_routes: + env_handler = EnvHandler() + + # + # before the first call to handle_dhclient_restart, EnvHandler configures the network routes and initializes the DHCP PIDs + # + with patch.object(env_handler, "get_dhcp_client_pid", return_value=[123]): + env_handler.dhcp_handler.conf_routes() + env_handler.dhcp_id_list = env_handler.get_dhcp_client_pid() + self.assertEquals(mock_conf_routes.call_count, 1) + + # + # if the dhcp client has not been restarted then it should not reconfigure the network routes + # + def mock_check_pid_alive(pid): + if pid == 123: + return True + raise Exception("Unexpected PID: {0}".format(pid)) + + with patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.check_pid_alive", side_effect=mock_check_pid_alive): + with patch.object(env_handler, "get_dhcp_client_pid", side_effect=Exception("get_dhcp_client_pid should not have been invoked")): + env_handler.handle_dhclient_restart() + self.assertEquals(mock_conf_routes.call_count, 1) # count did not change + + # + # if the process was restarted then it should reconfigure the network routes + # + def mock_check_pid_alive(pid): + if pid == 123: + return False + raise Exception("Unexpected PID: {0}".format(pid)) + + with patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.check_pid_alive", side_effect=mock_check_pid_alive): + with patch.object(env_handler, "get_dhcp_client_pid", return_value=[456, 789]): + env_handler.handle_dhclient_restart() + self.assertEquals(mock_conf_routes.call_count, 2) # count increased + + # + # if the new dhcp client has not been restarted then it should not reconfigure the network routes + # + def mock_check_pid_alive(pid): + if pid in [456, 789]: + return True + raise Exception("Unexpected PID: {0}".format(pid)) + + with patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.check_pid_alive", side_effect=mock_check_pid_alive): + with patch.object(env_handler, "get_dhcp_client_pid", side_effect=Exception("get_dhcp_client_pid should not have been invoked")): + env_handler.handle_dhclient_restart() + self.assertEquals(mock_conf_routes.call_count, 2) # count did not change diff -Nru waagent-2.2.34/tests/ga/test_extension.py waagent-2.2.45/tests/ga/test_extension.py --- waagent-2.2.34/tests/ga/test_extension.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/ga/test_extension.py 2019-11-07 00:36:56.000000000 +0000 @@ -17,17 +17,39 @@ import os.path +from datetime import timedelta + +from azurelinuxagent.ga.monitor import get_monitor_handler +from nose.plugins.attrib import attr from tests.protocol.mockwiredata import * -from azurelinuxagent.common.protocol.restapi import Extension +from azurelinuxagent.common.protocol.restapi import Extension, ExtHandlerProperties from azurelinuxagent.ga.exthandlers import * -from azurelinuxagent.common.protocol.wire import WireProtocol +from azurelinuxagent.common.protocol.wire import WireProtocol, InVMArtifactsProfile + +# Mocking the original sleep to reduce test execution time +SLEEP = time.sleep + + +def mock_sleep(sec=0.01): + SLEEP(sec) def do_not_run_test(): return True +def raise_system_exception(): + raise Exception + + +def raise_ioerror(*args): + e = IOError() + from errno import EIO + e.errno = EIO + raise e + + class TestExtensionCleanup(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) @@ -35,19 +57,19 @@ self.lib_dir = tempfile.mkdtemp() def _install_handlers(self, start=0, count=1, - handler_state=ExtHandlerState.Installed): + handler_state=ExtHandlerState.Installed): src = os.path.join(data_dir, "ext", "sample_ext-1.3.0.zip") version = FlexibleVersion("1.3.0") version += start - version.patch - for i in range(start, start+count): + for i in range(start, start + count): eh = ExtHandler() eh.name = "sample_ext" eh.properties.version = str(version) handler = ExtHandlerInstance(eh, "unused") dst = os.path.join(self.lib_dir, - handler.get_full_name()+HANDLER_PKG_EXT) + handler.get_full_name() + HANDLER_PKG_EXT) shutil.copy(src, dst) if not handler_state is None: @@ -55,7 +77,7 @@ handler.set_handler_state(handler_state) version += 1 - + def _count_packages(self): return len(glob.glob(os.path.join(self.lib_dir, "*.zip"))) @@ -63,13 +85,13 @@ paths = os.listdir(self.lib_dir) paths = [os.path.join(self.lib_dir, p) for p in paths] return len([p for p in paths - if os.path.isdir(p) and self._is_installed(p)]) + if os.path.isdir(p) and self._is_installed(p)]) def _count_uninstalled(self): paths = os.listdir(self.lib_dir) paths = [os.path.join(self.lib_dir, p) for p in paths] return len([p for p in paths - if os.path.isdir(p) and not self._is_installed(p)]) + if os.path.isdir(p) and not self._is_installed(p)]) def _is_installed(self, path): path = os.path.join(path, 'config', 'HandlerState') @@ -147,9 +169,9 @@ def _prepare_handler_state(self): handler_state_path = os.path.join( - self.tmp_dir, - "handler_state", - self.ext_handler_i.get_full_name()) + self.tmp_dir, + "handler_state", + self.ext_handler_i.get_full_name()) os.makedirs(handler_state_path) fileutil.write_file( os.path.join(handler_state_path, "state"), @@ -161,9 +183,9 @@ def _prepare_handler_config(self): handler_config_path = os.path.join( - self.tmp_dir, - self.ext_handler_i.get_full_name(), - "config") + self.tmp_dir, + self.ext_handler_i.get_full_name(), + "config") os.makedirs(handler_config_path) return @@ -237,7 +259,7 @@ message = "A message" try: - with patch('json.dumps', return_value=None): + with patch('json.dumps', return_value=None): self.ext_handler_i.set_handler_status(status=status, code=code, message=message) except Exception as e: self.fail("set_handler_status threw an exception") @@ -268,17 +290,23 @@ class ExtensionTestCase(AgentTestCase): @classmethod def setUpClass(cls): - CGroups.disable() + cls.cgroups_enabled = CGroupConfigurator.get_instance().enabled() + CGroupConfigurator.get_instance().disable() @classmethod def tearDownClass(cls): - CGroups.enable() + if cls.cgroups_enabled: + CGroupConfigurator.get_instance().enable() + else: + CGroupConfigurator.get_instance().disable() + +@patch('time.sleep', side_effect=lambda _: mock_sleep(0.001)) @patch("azurelinuxagent.common.protocol.wire.CryptUtil") @patch("azurelinuxagent.common.utils.restutil.http_get") class TestExtension(ExtensionTestCase): - def _assert_handler_status(self, report_vm_status, expected_status, + def _assert_handler_status(self, report_vm_status, expected_status, expected_ext_count, version, expected_handler_name="OSTCExtensions.ExampleHandlerLinux"): self.assertTrue(report_vm_status.called) @@ -292,7 +320,15 @@ self.assertEquals(version, handler_status.version) self.assertEquals(expected_ext_count, len(handler_status.extensions)) return - + + def _assert_ext_pkg_file_status(self, expected_to_be_present=True, extension_version="1.0.0", + extension_handler_name="OSTCExtensions.ExampleHandlerLinux"): + zip_file_format = "{0}__{1}.zip" + if expected_to_be_present: + self.assertIn(zip_file_format.format(extension_handler_name, extension_version), os.listdir(conf.get_lib_dir())) + else: + self.assertNotIn(zip_file_format.format(extension_handler_name, extension_version), os.listdir(conf.get_lib_dir())) + def _assert_no_handler_status(self, report_vm_status): self.assertTrue(report_vm_status.called) args, kw = report_vm_status.call_args @@ -300,11 +336,11 @@ self.assertEquals(0, len(vm_status.vmAgent.extensionHandlers)) return - def _create_mock(self, test_data, mock_http_get, MockCryptUtil): + def _create_mock(self, test_data, mock_http_get, MockCryptUtil, *args): """Test enable/disable/uninstall of an extension""" handler = get_exthandlers_handler() - #Mock protocol to return test data + # Mock protocol to return test data mock_http_get.side_effect = test_data.mock_http_get MockCryptUtil.side_effect = test_data.mock_crypt_util @@ -315,40 +351,68 @@ handler.protocol_util.get_protocol = Mock(return_value=protocol) return handler, protocol - + + def _set_up_update_test_and_update_gs(self, patch_command, *args): + """ + This helper function sets up the Update test by setting up the protocol and ext_handler and asserts the + ext_handler runs fine the first time before patching a failure command for testing. + :param patch_command: The patch_command to setup for failure + :param args: Any additional args passed to the function, needed for creating a mock for handler and protocol + :return: test_data, exthandlers_handler, protocol + """ + test_data = WireProtocolData(DATA_FILE_EXT_SINGLE) + exthandlers_handler, protocol = self._create_mock(test_data, *args) + + # Ensure initial install and enable is successful + exthandlers_handler.run() + + self.assertEqual(0, patch_command.call_count) + self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.0") + self._assert_ext_status(protocol.report_ext_status, "success", 0) + + # Next incarnation, update version + test_data.goal_state = test_data.goal_state.replace("1<", "2<") + test_data.ext_conf = test_data.ext_conf.replace('version="1.0.0"', 'version="1.0.1"') + test_data.manifest = test_data.manifest.replace('1.0.0', '1.0.1') + + # Ensure the patched command fails + patch_command.return_value = "exit 1" + + return test_data, exthandlers_handler, protocol + def test_ext_handler(self, *args): test_data = WireProtocolData(DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) - #Test enable scenario. + # Test enable scenario. exthandlers_handler.run() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") self._assert_ext_status(protocol.report_ext_status, "success", 0) - #Test goal state not changed + # Test goal state not changed exthandlers_handler.run() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") - #Test goal state changed + # Test goal state changed test_data.goal_state = test_data.goal_state.replace("1<", "2<") - test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"0\"", + test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"0\"", "seqNo=\"1\"") exthandlers_handler.run() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") self._assert_ext_status(protocol.report_ext_status, "success", 1) - - #Test hotfix + + # Test hotfix test_data.goal_state = test_data.goal_state.replace("2<", "3<") test_data.ext_conf = test_data.ext_conf.replace("1.0.0", "1.1.1") - test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"1\"", + test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"1\"", "seqNo=\"2\"") exthandlers_handler.run() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.1") self._assert_ext_status(protocol.report_ext_status, "success", 2) - #Test upgrade + # Test upgrade test_data.goal_state = test_data.goal_state.replace("3<", "4<") test_data.ext_conf = test_data.ext_conf.replace("1.1.1", "1.2.0") @@ -358,27 +422,135 @@ self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.2.0") self._assert_ext_status(protocol.report_ext_status, "success", 3) - #Test disable + # Test disable test_data.goal_state = test_data.goal_state.replace("4<", "5<") test_data.ext_conf = test_data.ext_conf.replace("enabled", "disabled") exthandlers_handler.run() - self._assert_handler_status(protocol.report_vm_status, "NotReady", + self._assert_handler_status(protocol.report_vm_status, "NotReady", 1, "1.2.0") - #Test uninstall + # Test uninstall test_data.goal_state = test_data.goal_state.replace("5<", "6<") test_data.ext_conf = test_data.ext_conf.replace("disabled", "uninstall") exthandlers_handler.run() self._assert_no_handler_status(protocol.report_vm_status) - #Test uninstall again! + # Test uninstall again! test_data.goal_state = test_data.goal_state.replace("6<", "7<") exthandlers_handler.run() self._assert_no_handler_status(protocol.report_vm_status) + def test_ext_zip_file_packages_removed_in_update_case(self, *args): + test_data = WireProtocolData(DATA_FILE) + exthandlers_handler, protocol = self._create_mock(test_data, *args) + + # Test enable scenario. + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") + self._assert_ext_status(protocol.report_ext_status, "success", 0) + self._assert_ext_pkg_file_status(expected_to_be_present=True, + extension_version="1.0.0") + + # Update the package + test_data.goal_state = test_data.goal_state.replace("1<", + "2<") + test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"0\"", + "seqNo=\"1\"") + test_data.ext_conf = test_data.ext_conf.replace("1.0.0", "1.1.0") + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.0") + self._assert_ext_status(protocol.report_ext_status, "success", 1) + self._assert_ext_pkg_file_status(expected_to_be_present=False, + extension_version="1.0.0") + self._assert_ext_pkg_file_status(expected_to_be_present=True, + extension_version="1.1.0") + + # Update the package second time + test_data.goal_state = test_data.goal_state.replace("2<", + "3<") + test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"1\"", + "seqNo=\"2\"") + test_data.ext_conf = test_data.ext_conf.replace("1.1.0", "1.2.0") + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.2.0") + self._assert_ext_status(protocol.report_ext_status, "success", 2) + self._assert_ext_pkg_file_status(expected_to_be_present=False, + extension_version="1.1.0") + self._assert_ext_pkg_file_status(expected_to_be_present=True, + extension_version="1.2.0") + + def test_ext_zip_file_packages_removed_in_uninstall_case(self, *args): + test_data = WireProtocolData(DATA_FILE) + exthandlers_handler, protocol = self._create_mock(test_data, *args) + extension_version = "1.0.0" + + # Test enable scenario. + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, extension_version) + self._assert_ext_status(protocol.report_ext_status, "success", 0) + self._assert_ext_pkg_file_status(expected_to_be_present=True, + extension_version=extension_version) + + # Test uninstall + test_data.goal_state = test_data.goal_state.replace("1<", + "2<") + test_data.ext_conf = test_data.ext_conf.replace("enabled", "uninstall") + exthandlers_handler.run() + self._assert_no_handler_status(protocol.report_vm_status) + self._assert_ext_pkg_file_status(expected_to_be_present=False, + extension_version=extension_version) + + def test_ext_zip_file_packages_removed_in_update_and_uninstall_case(self, *args): + test_data = WireProtocolData(DATA_FILE) + exthandlers_handler, protocol = self._create_mock(test_data, *args) + + # Test enable scenario. + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") + self._assert_ext_status(protocol.report_ext_status, "success", 0) + self._assert_ext_pkg_file_status(expected_to_be_present=True, + extension_version="1.0.0") + + # Update the package + test_data.goal_state = test_data.goal_state.replace("1<", + "2<") + test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"0\"", + "seqNo=\"1\"") + test_data.ext_conf = test_data.ext_conf.replace("1.0.0", "1.1.0") + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.0") + self._assert_ext_status(protocol.report_ext_status, "success", 1) + self._assert_ext_pkg_file_status(expected_to_be_present=False, + extension_version="1.0.0") + self._assert_ext_pkg_file_status(expected_to_be_present=True, + extension_version="1.1.0") + + # Update the package second time + test_data.goal_state = test_data.goal_state.replace("2<", + "3<") + test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"1\"", + "seqNo=\"2\"") + test_data.ext_conf = test_data.ext_conf.replace("1.1.0", "1.2.0") + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.2.0") + self._assert_ext_status(protocol.report_ext_status, "success", 2) + self._assert_ext_pkg_file_status(expected_to_be_present=False, + extension_version="1.1.0") + self._assert_ext_pkg_file_status(expected_to_be_present=True, + extension_version="1.2.0") + + # Test uninstall + test_data.goal_state = test_data.goal_state.replace("3<", + "4<") + test_data.ext_conf = test_data.ext_conf.replace("enabled", "uninstall") + exthandlers_handler.run() + self._assert_no_handler_status(protocol.report_vm_status) + self._assert_ext_pkg_file_status(expected_to_be_present=False, + extension_version="1.2.0") + def test_ext_handler_no_settings(self, *args): test_data = WireProtocolData(DATA_FILE_EXT_NO_SETTINGS) exthandlers_handler, protocol = self._create_mock(test_data, *args) @@ -397,15 +569,15 @@ test_data = WireProtocolData(DATA_FILE_NO_EXT) exthandlers_handler, protocol = self._create_mock(test_data, *args) - #Assert no extension handler status + # Assert no extension handler status exthandlers_handler.run() self._assert_no_handler_status(protocol.report_vm_status) - + def test_ext_handler_sequencing(self, *args): test_data = WireProtocolData(DATA_FILE_EXT_SEQUENCING) exthandlers_handler, protocol = self._create_mock(test_data, *args) - #Test enable scenario. + # Test enable scenario. exthandlers_handler.run() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0", expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux") @@ -418,12 +590,12 @@ self.assertEqual(exthandlers_handler.ext_handlers.extHandlers[0].properties.extensions[0].dependencyLevel, 1) self.assertEqual(exthandlers_handler.ext_handlers.extHandlers[1].properties.extensions[0].dependencyLevel, 2) - #Test goal state not changed + # Test goal state not changed exthandlers_handler.run() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0", expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux") - #Test goal state changed + # Test goal state changed test_data.goal_state = test_data.goal_state.replace("1<", "2<") test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"0\"", @@ -441,7 +613,7 @@ self.assertEqual(exthandlers_handler.ext_handlers.extHandlers[0].properties.extensions[0].dependencyLevel, 3) self.assertEqual(exthandlers_handler.ext_handlers.extHandlers[1].properties.extensions[0].dependencyLevel, 4) - #Test disable + # Test disable # In the case of disable, the last extension to be enabled should be # the first extension disabled. The first extension enabled should be # the last one disabled. @@ -456,7 +628,7 @@ self.assertEqual(exthandlers_handler.ext_handlers.extHandlers[0].properties.extensions[0].dependencyLevel, 4) self.assertEqual(exthandlers_handler.ext_handlers.extHandlers[1].properties.extensions[0].dependencyLevel, 3) - #Test uninstall + # Test uninstall # In the case of uninstall, the last extension to be installed should be # the first extension uninstalled. The first extension installed # should be the last one uninstalled. @@ -498,23 +670,78 @@ self.assertEqual(exthandlers_handler.ext_handlers.extHandlers[0].properties.extensions[0].dependencyLevel, 0) self.assertEqual(exthandlers_handler.ext_handlers.extHandlers[0].properties.extensions[0].dependencyLevel, 0) + @patch('time.gmtime', MagicMock(return_value=time.gmtime(0))) + def test_ext_handler_reporting_status_file(self, *args): + expected_status = ''' +{{ + "agent_name": "{agent_name}", + "current_version": "{current_version}", + "goal_state_version": "{goal_state_version}", + "distro_details": "{distro_details}", + "last_successful_status_upload_time": "{last_successful_status_upload_time}", + "python_version": "{python_version}", + "extensions_status": [ + {{ + "name": "OSTCExtensions.ExampleHandlerLinux", + "version": "1.0.0", + "status": "Ready" + }}, + {{ + "name": "Microsoft.Powershell.ExampleExtension", + "version": "1.0.0", + "status": "Ready" + }}, + {{ + "name": "Microsoft.EnterpriseCloud.Monitoring.ExampleHandlerLinux", + "version": "1.0.0", + "status": "Ready" + }}, + {{ + "name": "Microsoft.CPlat.Core.ExampleExtensionLinux", + "version": "1.0.0", + "status": "Ready" + }}, + {{ + "name": "Microsoft.OSTCExtensions.Edp.ExampleExtensionLinuxInTest", + "version": "1.0.0", + "status": "Ready" + }} + ] +}}'''.format(agent_name=AGENT_NAME, + current_version=str(CURRENT_VERSION), + goal_state_version=str(GOAL_STATE_AGENT_VERSION), + distro_details="{0}:{1}".format(DISTRO_NAME, DISTRO_VERSION), + last_successful_status_upload_time=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + python_version="Python: {0}.{1}.{2}".format(PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO)) + + expected_status_json = json.loads(expected_status) + + test_data = WireProtocolData(DATA_FILE_MULTIPLE_EXT) + exthandlers_handler, protocol = self._create_mock(test_data, *args) + exthandlers_handler.run() + + status_path = os.path.join(conf.get_lib_dir(), AGENT_STATUS_FILE) + actual_status_json = json.loads(fileutil.read_file(status_path)) + + self.assertEquals(expected_status_json, actual_status_json) + def test_ext_handler_rollingupgrade(self, *args): test_data = WireProtocolData(DATA_FILE_EXT_ROLLINGUPGRADE) exthandlers_handler, protocol = self._create_mock(test_data, *args) - #Test enable scenario. + # Test enable scenario. exthandlers_handler.run() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") self._assert_ext_status(protocol.report_ext_status, "success", 0) - #Test goal state changed + # Test goal state changed test_data.goal_state = test_data.goal_state.replace("1<", "2<") exthandlers_handler.run() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") self._assert_ext_status(protocol.report_ext_status, "success", 0) - #Test minor version bump + # Test minor version bump test_data.goal_state = test_data.goal_state.replace("2<", "3<") test_data.ext_conf = test_data.ext_conf.replace("1.0.0", "1.1.0") @@ -522,7 +749,7 @@ self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.0") self._assert_ext_status(protocol.report_ext_status, "success", 0) - #Test hotfix version bump + # Test hotfix version bump test_data.goal_state = test_data.goal_state.replace("3<", "4<") test_data.ext_conf = test_data.ext_conf.replace("1.1.0", "1.1.1") @@ -530,7 +757,7 @@ self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.1") self._assert_ext_status(protocol.report_ext_status, "success", 0) - #Test disable + # Test disable test_data.goal_state = test_data.goal_state.replace("4<", "5<") test_data.ext_conf = test_data.ext_conf.replace("enabled", "disabled") @@ -538,20 +765,20 @@ self._assert_handler_status(protocol.report_vm_status, "NotReady", 1, "1.1.1") - #Test uninstall + # Test uninstall test_data.goal_state = test_data.goal_state.replace("5<", "6<") test_data.ext_conf = test_data.ext_conf.replace("disabled", "uninstall") exthandlers_handler.run() self._assert_no_handler_status(protocol.report_vm_status) - #Test uninstall again! + # Test uninstall again! test_data.goal_state = test_data.goal_state.replace("6<", "7<") exthandlers_handler.run() self._assert_no_handler_status(protocol.report_vm_status) - #Test re-install + # Test re-install test_data.goal_state = test_data.goal_state.replace("7<", "8<") test_data.ext_conf = test_data.ext_conf.replace("uninstall", "enabled") @@ -559,7 +786,7 @@ self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.1") self._assert_ext_status(protocol.report_ext_status, "success", 0) - #Test version bump post-re-install + # Test version bump post-re-install test_data.goal_state = test_data.goal_state.replace("8<", "9<") test_data.ext_conf = test_data.ext_conf.replace("1.1.1", "1.2.0") @@ -567,7 +794,7 @@ self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.2.0") self._assert_ext_status(protocol.report_ext_status, "success", 0) - #Test rollback + # Test rollback test_data.goal_state = test_data.goal_state.replace("9<", "10<") test_data.ext_conf = test_data.ext_conf.replace("1.2.0", "1.1.0") @@ -575,14 +802,16 @@ self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.0") self._assert_ext_status(protocol.report_ext_status, "success", 0) - @skip_if_predicate_true(do_not_run_test, "Incorrect test - Change in behavior in reporting events now.") @patch('azurelinuxagent.ga.exthandlers.add_event') def test_ext_handler_download_failure_transient(self, mock_add_event, *args): + original_sleep = time.sleep + test_data = WireProtocolData(DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) protocol.download_ext_handler_pkg = Mock(side_effect=ProtocolError) exthandlers_handler.run() + self.assertEquals(0, mock_add_event.call_count) @patch('azurelinuxagent.common.errorstate.ErrorState.is_triggered') @@ -613,31 +842,95 @@ self.assertTrue("ResourceGoneError" in kw['message']) self.assertEquals("ExtensionProcessing", kw['op']) - @skip_if_predicate_true(do_not_run_test, "Incorrect test - Change in behavior in reporting events now.") @patch('azurelinuxagent.common.errorstate.ErrorState.is_triggered') - @patch('azurelinuxagent.common.event.add_event') - def test_ext_handler_download_failure_permanent(self, mock_add_event, mock_error_state, *args): + @patch('azurelinuxagent.ga.exthandlers.ExtHandlerInstance.report_event') + def test_ext_handler_download_failure_permanent_ProtocolError(self, mock_add_event, mock_error_state, *args): test_data = WireProtocolData(DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) protocol.get_ext_handler_pkgs = Mock(side_effect=ProtocolError) mock_error_state.return_value = True + exthandlers_handler.run() + self.assertEquals(1, mock_add_event.call_count) args, kw = mock_add_event.call_args_list[0] self.assertEquals(False, kw['is_success']) self.assertTrue("Failed to get ext handler pkgs" in kw['message']) - self.assertTrue("Failed to get artifact" in kw['message']) - self.assertEquals("GetArtifactExtended", kw['op']) + self.assertTrue("ProtocolError" in kw['message']) + + @patch('azurelinuxagent.common.errorstate.ErrorState.is_triggered') + @patch('azurelinuxagent.common.event.add_event') + def test_ext_handler_download_failure_permanent_with_ExtensionDownloadError_and_triggered(self, mock_add_event, + mock_error_state, *args): + test_data = WireProtocolData(DATA_FILE) + exthandlers_handler, protocol = self._create_mock(test_data, *args) + protocol.get_ext_handler_pkgs = Mock(side_effect=ExtensionDownloadError) + + mock_error_state.return_value = True + + exthandlers_handler.run() + + self.assertEquals(1, mock_add_event.call_count) + args, kw = mock_add_event.call_args_list[0] + self.assertEquals(False, kw['is_success']) + self.assertTrue("Failed to get artifact for over" in kw['message']) + self.assertTrue("ExtensionDownloadError" in kw['message']) + self.assertEquals("Download", kw['op']) + + @patch('azurelinuxagent.common.errorstate.ErrorState.is_triggered') + @patch('azurelinuxagent.common.event.add_event') + def test_ext_handler_download_failure_permanent_with_ExtensionDownloadError_and_not_triggered(self, mock_add_event, + mock_error_state, + *args): + test_data = WireProtocolData(DATA_FILE) + exthandlers_handler, protocol = self._create_mock(test_data, *args) + protocol.get_ext_handler_pkgs = Mock(side_effect=ExtensionDownloadError) + + mock_error_state.return_value = False + + exthandlers_handler.run() + + self.assertEquals(0, mock_add_event.call_count) @patch('azurelinuxagent.ga.exthandlers.fileutil') def test_ext_handler_io_error(self, mock_fileutil, *args): test_data = WireProtocolData(DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) - + mock_fileutil.write_file.return_value = IOError("Mock IO Error") exthandlers_handler.run() + def test_extension_processing_allowed(self, *args): + exthandlers_handler = get_exthandlers_handler() + exthandlers_handler.protocol = Mock() + + # disable extension handling in configuration + with patch.object(conf, 'get_extensions_enabled', return_value=False): + self.assertFalse(exthandlers_handler.extension_processing_allowed()) + + # enable extension handling in configuration + with patch.object(conf, "get_extensions_enabled", return_value=True): + # disable overprovisioning in configuration + with patch.object(conf, 'get_enable_overprovisioning', return_value=False): + self.assertTrue(exthandlers_handler.extension_processing_allowed()) + + # enable overprovisioning in configuration + with patch.object(conf, "get_enable_overprovisioning", return_value=True): + # disable protocol support for over-provisioning + with patch.object(exthandlers_handler.protocol, 'supports_overprovisioning', return_value=False): + self.assertTrue(exthandlers_handler.extension_processing_allowed()) + + # enable protocol support for over-provisioning + with patch.object(exthandlers_handler.protocol, "supports_overprovisioning", return_value=True): + with patch.object(exthandlers_handler.protocol.get_artifacts_profile(), "is_on_hold", + side_effect=[True, False]): + # Enable on_hold property in artifact_blob + self.assertFalse(exthandlers_handler.extension_processing_allowed()) + + # Disable on_hold property in artifact_blob + self.assertTrue(exthandlers_handler.extension_processing_allowed()) + def test_handle_ext_handlers_on_hold_true(self, *args): test_data = WireProtocolData(DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) @@ -646,16 +939,16 @@ exthandlers_handler.protocol = protocol # Disable extension handling blocking - conf.get_enable_overprovisioning = Mock(return_value=False) - with patch.object(ExtHandlersHandler, 'handle_ext_handler') as patch_handle_ext_handler: - exthandlers_handler.handle_ext_handlers() - self.assertEqual(1, patch_handle_ext_handler.call_count) + exthandlers_handler.extension_processing_allowed = Mock(return_value=False) + with patch.object(ExtHandlersHandler, 'handle_ext_handlers') as patch_handle_ext_handlers: + exthandlers_handler.run() + self.assertEqual(0, patch_handle_ext_handlers.call_count) # enable extension handling blocking - conf.get_enable_overprovisioning = Mock(return_value=True) - with patch.object(ExtHandlersHandler, 'handle_ext_handler') as patch_handle_ext_handler: - exthandlers_handler.handle_ext_handlers() - self.assertEqual(0, patch_handle_ext_handler.call_count) + exthandlers_handler.extension_processing_allowed = Mock(return_value=True) + with patch.object(ExtHandlersHandler, 'handle_ext_handlers') as patch_handle_ext_handlers: + exthandlers_handler.run() + self.assertEqual(1, patch_handle_ext_handlers.call_count) def test_handle_ext_handlers_on_hold_false(self, *args): test_data = WireProtocolData(DATA_FILE) @@ -666,7 +959,7 @@ # enable extension handling blocking conf.get_enable_overprovisioning = Mock(return_value=True) - #Test when is_on_hold returns False + # Test when is_on_hold returns False from azurelinuxagent.common.protocol.wire import InVMArtifactsProfile mock_in_vm_artifacts_profile = InVMArtifactsProfile(MagicMock()) mock_in_vm_artifacts_profile.is_on_hold = Mock(return_value=False) @@ -675,12 +968,31 @@ exthandlers_handler.handle_ext_handlers() self.assertEqual(1, patch_handle_ext_handler.call_count) - #Test when in_vm_artifacts_profile is not available + # Test when in_vm_artifacts_profile is not available protocol.get_artifacts_profile = Mock(return_value=None) with patch.object(ExtHandlersHandler, 'handle_ext_handler') as patch_handle_ext_handler: exthandlers_handler.handle_ext_handlers() self.assertEqual(1, patch_handle_ext_handler.call_count) + def test_last_etag_on_extension_processing(self, *args): + test_data = WireProtocolData(DATA_FILE) + exthandlers_handler, protocol = self._create_mock(test_data, *args) + exthandlers_handler.ext_handlers, etag = protocol.get_ext_handlers() + exthandlers_handler.protocol = protocol + + # Disable extension handling blocking in the first run and enable in the 2nd run + with patch.object(exthandlers_handler, 'extension_processing_allowed', side_effect=[False, True]): + exthandlers_handler.run() + self.assertIsNone(exthandlers_handler.last_etag, + "The last etag should be None initially as extension_processing is False") + self.assertNotEqual(etag, exthandlers_handler.last_etag, + "Last etag and etag should not be same if extension processing is disabled") + exthandlers_handler.run() + self.assertIsNotNone(exthandlers_handler.last_etag, + "Last etag should not be none if extension processing is allowed") + self.assertEqual(etag, exthandlers_handler.last_etag, + "Last etag and etag should be same if extension processing is enabled") + def _assert_ext_status(self, report_ext_status, expected_status, expected_seq_no): self.assertTrue(report_ext_status.called) @@ -695,8 +1007,8 @@ exthandlers_handler.run() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") - #Remove status file and re-run collecting extension status - status_file = os.path.join(self.tmp_dir, + # Remove status file and re-run collecting extension status + status_file = os.path.join(self.tmp_dir, "OSTCExtensions.ExampleHandlerLinux-1.0.0", "status", "0.status") self.assertTrue(os.path.isfile(status_file)) @@ -731,7 +1043,7 @@ exthandler.properties.extensions.append(extension) # Override the timeout value to minimize the test duration - wait_until = datetime.datetime.utcnow() + datetime.timedelta(seconds=5) + wait_until = datetime.datetime.utcnow() + datetime.timedelta(seconds=0.1) return exthandlers_handler.wait_for_handler_successful_completion(exthandler, wait_until) def test_wait_for_handler_successful_completion_no_status(self, *args): @@ -818,7 +1130,7 @@ os.path.exists = MagicMock(return_value=case[2]) if case[2]: # when the status file exists, it is expected return the value from collect_ext_status() - ext_handler_i.collect_ext_status= MagicMock(return_value=case[3]) + ext_handler_i.collect_ext_status = MagicMock(return_value=case[3]) status = ext_handler_i.get_ext_handling_status(extension) if case[2]: @@ -835,14 +1147,14 @@ ''' test_data = WireProtocolData(DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) - + handler_name = "Handler" exthandler = ExtHandler(name=handler_name) extension = Extension(name=handler_name) exthandler.properties.extensions.append(extension) ext_handler_i = ExtHandlerInstance(exthandler, protocol) - + # Testing no status case ext_handler_i.get_ext_handling_status = MagicMock(return_value=None) completed, status = ext_handler_i.is_ext_handling_complete(extension) @@ -857,7 +1169,7 @@ "warning": (False, "warning"), "transitioning": (False, "transitioning") } - + for key in expected_results.keys(): ext_handler_i.get_ext_handling_status = MagicMock(return_value=key) completed, status = ext_handler_i.is_ext_handling_complete(extension) @@ -901,18 +1213,18 @@ # (installed_version, config_version, exptected_version, autoupgrade_expected_version) cases = [ - (None, '2.0', '2.0.0'), - (None, '2.0.0', '2.0.0'), - ('1.0', '1.0.0', '1.0.0'), - (None, '2.1.0', '2.1.0'), - (None, '2.1.1', '2.1.1'), - (None, '2.2.0', '2.2.0'), - (None, '2.3.0', '2.3.0'), - (None, '2.4.0', '2.4.0'), - (None, '3.0', '3.0'), - (None, '3.1', '3.1'), - (None, '4.0', '4.0.0.1'), - (None, '4.1', '4.1.0.0'), + (None, '2.0', '2.0.0'), + (None, '2.0.0', '2.0.0'), + ('1.0', '1.0.0', '1.0.0'), + (None, '2.1.0', '2.1.0'), + (None, '2.1.1', '2.1.1'), + (None, '2.2.0', '2.2.0'), + (None, '2.3.0', '2.3.0'), + (None, '2.4.0', '2.4.0'), + (None, '3.0', '3.0'), + (None, '3.1', '3.1'), + (None, '4.0', '4.0.0.1'), + (None, '4.1', '4.1.0.0'), ] _, protocol = self._create_mock(WireProtocolData(DATA_FILE), *args) @@ -925,7 +1237,7 @@ ext_handler.name = 'OSTCExtensions.ExampleHandlerLinux' ext_handler.versionUris = [version_uri] ext_handler.properties.version = config_version - + ext_handler_instance = ExtHandlerInstance(ext_handler, protocol) ext_handler_instance.get_installed_version = Mock(return_value=installed_version) @@ -967,8 +1279,10 @@ self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.1") self._assert_ext_status(protocol.report_ext_status, "success", 0) - @patch('subprocess.Popen.poll') - def test_install_failure(self, patch_poll, *args): + @patch('azurelinuxagent.ga.exthandlers.ExtHandlerInstance.install', side_effect=ExtHandlerInstance.install, + autospec=True) + @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_install_command') + def test_install_failure(self, patch_get_install_command, patch_install, *args): """ When extension install fails, the operation should not be retried. """ @@ -976,38 +1290,34 @@ exthandlers_handler, protocol = self._create_mock(test_data, *args) # Ensure initial install is unsuccessful - patch_poll.call_count = 0 - patch_poll.return_value = 1 + patch_get_install_command.return_value = "exit.sh 1" exthandlers_handler.run() - # capture process output also calls poll - self.assertEqual(2, patch_poll.call_count) + self.assertEqual(1, patch_install.call_count) self.assertEqual(1, protocol.report_vm_status.call_count) self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=0, version="1.0.0") # Ensure subsequent no further retries are made exthandlers_handler.run() - self.assertEqual(2, patch_poll.call_count) + self.assertEqual(1, patch_install.call_count) self.assertEqual(2, protocol.report_vm_status.call_count) - @patch('azurelinuxagent.ga.exthandlers.ExtHandlersHandler.handle_handle_ext_handler_error') - @patch('subprocess.Popen.poll') - def test_install_failure_check_exception_handling(self, patch_poll, patch_handle_handle_ext_handler_error, *args): + @patch('azurelinuxagent.ga.exthandlers.ExtHandlersHandler.handle_ext_handler_error') + @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_install_command') + def test_install_failure_check_exception_handling(self, patch_get_install_command, patch_handle_ext_handler_error, + *args): """ When extension install fails, the operation should be reported to our telemetry service. """ test_data = WireProtocolData(DATA_FILE_EXT_SINGLE) exthandlers_handler, protocol = self._create_mock(test_data, *args) - # Ensure initial install is unsuccessful - patch_poll.call_count = 0 - patch_poll.return_value = 1 + # Ensure install is unsuccessful + patch_get_install_command.return_value = "exit.sh 1" exthandlers_handler.run() - # capture process output also calls poll - self.assertEqual(2, patch_poll.call_count) self.assertEqual(1, protocol.report_vm_status.call_count) - self.assertEqual(1, patch_handle_handle_ext_handler_error.call_count) + self.assertEqual(1, patch_handle_ext_handler_error.call_count) @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_enable_command') def test_enable_failure(self, patch_get_enable_command, *args): @@ -1019,7 +1329,7 @@ # Ensure initial install is successful, but enable fails patch_get_enable_command.call_count = 0 - patch_get_enable_command.return_value = "exit 1" + patch_get_enable_command.return_value = "exit.sh 1" exthandlers_handler.run() self.assertEqual(1, patch_get_enable_command.call_count) @@ -1030,10 +1340,10 @@ self.assertEqual(1, patch_get_enable_command.call_count) self.assertEqual(2, protocol.report_vm_status.call_count) - @patch('azurelinuxagent.ga.exthandlers.ExtHandlersHandler.handle_handle_ext_handler_error') + @patch('azurelinuxagent.ga.exthandlers.ExtHandlersHandler.handle_ext_handler_error') @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_enable_command') def test_enable_failure_check_exception_handling(self, patch_get_enable_command, - patch_handle_handle_ext_handler_error, *args): + patch_handle_ext_handler_error, *args): """ When extension enable fails, the operation should be reported. """ @@ -1042,12 +1352,12 @@ # Ensure initial install is successful, but enable fails patch_get_enable_command.call_count = 0 - patch_get_enable_command.return_value = "exit 1" + patch_get_enable_command.return_value = "exit.sh 1" exthandlers_handler.run() self.assertEqual(1, patch_get_enable_command.call_count) self.assertEqual(1, protocol.report_vm_status.call_count) - self.assertEqual(1, patch_handle_handle_ext_handler_error.call_count) + self.assertEqual(1, patch_handle_ext_handler_error.call_count) @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') def test_disable_failure(self, patch_get_disable_command, *args): @@ -1059,7 +1369,7 @@ # Ensure initial install and enable is successful, but disable fails patch_get_disable_command.call_count = 0 - patch_get_disable_command.return_value = "exit 1" + patch_get_disable_command.return_value = "exit.sh 1" exthandlers_handler.run() self.assertEqual(0, patch_get_disable_command.call_count) @@ -1083,10 +1393,10 @@ self.assertEqual(3, protocol.report_vm_status.call_count) self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=1, version="1.0.0") - @patch('azurelinuxagent.ga.exthandlers.ExtHandlersHandler.handle_handle_ext_handler_error') + @patch('azurelinuxagent.ga.exthandlers.ExtHandlersHandler.handle_ext_handler_error') @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') def test_disable_failure_with_exception_handling(self, patch_get_disable_command, - patch_handle_handle_ext_handler_error, *args): + patch_handle_ext_handler_error, *args): """ When extension disable fails, the operation should be reported. """ @@ -1111,7 +1421,7 @@ self.assertEqual(1, patch_get_disable_command.call_count) self.assertEqual(2, protocol.report_vm_status.call_count) - self.assertEqual(1, patch_handle_handle_ext_handler_error.call_count) + self.assertEqual(1, patch_handle_ext_handler_error.call_count) @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_uninstall_command') def test_uninstall_failure(self, patch_get_uninstall_command, *args): @@ -1154,26 +1464,9 @@ """ Extension upgrade failure should not be retried """ - test_data = WireProtocolData(DATA_FILE_EXT_SINGLE) - exthandlers_handler, protocol = self._create_mock(test_data, *args) + test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_update_command, + *args) - # Ensure initial install and enable is successful - exthandlers_handler.run() - self.assertEqual(0, patch_get_update_command.call_count) - - self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.0") - self._assert_ext_status(protocol.report_ext_status, "success", 0) - - # Next incarnation, update version - test_data.goal_state = test_data.goal_state.replace("1<", - "2<") - test_data.ext_conf = test_data.ext_conf.replace('version="1.0.0"', - 'version="1.0.1"') - test_data.manifest = test_data.manifest.replace('1.0.0', - '1.0.1') - - # Update command should fail - patch_get_update_command.return_value = "exit 1" exthandlers_handler.run() self.assertEqual(1, patch_get_update_command.call_count) @@ -1183,33 +1476,422 @@ self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=1, version="1.0.1") - @patch('azurelinuxagent.ga.exthandlers.ExtHandlersHandler.handle_handle_ext_handler_error') + @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') + def test__extension_upgrade_failure_when_prev_version_disable_fails(self, patch_get_disable_command, *args): + test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_disable_command, + *args) + + with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_enable_command') as patch_get_enable_command: + exthandlers_handler.run() + + # When the previous version's disable fails, we expect the upgrade scenario to fail, so the enable + # for the new version is not called and the new version handler's status is reported as not ready. + self.assertEqual(1, patch_get_disable_command.call_count) + self.assertEqual(0, patch_get_enable_command.call_count) + self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=0, version="1.0.1") + + # Ensure we are processing the same goal state only once + loop_run = 5 + for x in range(loop_run): + exthandlers_handler.run() + + self.assertEqual(1, patch_get_disable_command.call_count) + self.assertEqual(0, patch_get_enable_command.call_count) + + @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') + def test__extension_upgrade_failure_when_prev_version_disable_fails_and_recovers_on_next_incarnation(self, patch_get_disable_command, + *args): + test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_disable_command, + *args) + + with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_enable_command') as patch_get_enable_command: + exthandlers_handler.run() + + # When the previous version's disable fails, we expect the upgrade scenario to fail, so the enable + # for the new version is not called and the new version handler's status is reported as not ready. + self.assertEqual(1, patch_get_disable_command.call_count) + self.assertEqual(0, patch_get_enable_command.call_count) + self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=0, version="1.0.1") + + # Ensure we are processing the same goal state only once + loop_run = 5 + for x in range(loop_run): + exthandlers_handler.run() + + self.assertEqual(1, patch_get_disable_command.call_count) + self.assertEqual(0, patch_get_enable_command.call_count) + + # Force a new goal state incarnation, only then will we attempt the upgrade again + test_data.goal_state = test_data.goal_state.replace("2<", "3<") + + # Ensure disable won't fail by making launch_command a no-op + with patch('azurelinuxagent.ga.exthandlers.ExtHandlerInstance.launch_command') as patch_launch_command: + exthandlers_handler.run() + self.assertEqual(2, patch_get_disable_command.call_count) + self.assertEqual(1, patch_get_enable_command.call_count) + self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.1") + + @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') + def test__extension_upgrade_failure_when_prev_version_disable_fails_incorrect_zip(self, patch_get_disable_command, + *args): + test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_disable_command, + *args) + + # The download logic has retry logic that sleeps before each try - make sleep a no-op. + with patch("time.sleep"): + with patch("zipfile.ZipFile.extractall") as patch_zipfile_extractall: + with patch( + 'azurelinuxagent.ga.exthandlers.HandlerManifest.get_enable_command') as patch_get_enable_command: + patch_zipfile_extractall.side_effect = raise_ioerror + # The zipfile was corrupt and the upgrade sequence failed + exthandlers_handler.run() + + # We never called the disable of the old version due to the failure when unzipping the new version, + # nor the enable of the new version + self.assertEqual(0, patch_get_disable_command.call_count) + self.assertEqual(0, patch_get_enable_command.call_count) + + # Ensure we are processing the same goal state only once + loop_run = 5 + for x in range(loop_run): + exthandlers_handler.run() + + self.assertEqual(0, patch_get_disable_command.call_count) + self.assertEqual(0, patch_get_enable_command.call_count) + + @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') + def test__old_handler_reports_failure_on_disable_fail_on_update(self, patch_get_disable_command, *args): + old_version, new_version = "1.0.0", "1.0.1" + test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_disable_command, + *args) + + with patch.object(ExtHandlerInstance, "report_event", autospec=True) as patch_report_event: + exthandlers_handler.run() # Download the new update the first time, and then we patch the download method. + self.assertEqual(1, patch_get_disable_command.call_count) + + old_version_args, old_version_kwargs = patch_report_event.call_args + new_version_args, new_version_kwargs = patch_report_event.call_args_list[0] + + self.assertEqual(new_version_args[0].ext_handler.properties.version, new_version, + "The first call to report event should be from the new version of the ext-handler " + "to report download succeeded") + + self.assertEqual(new_version_kwargs['message'], "Download succeeded", + "The message should be Download Succedded") + + self.assertEqual(old_version_args[0].ext_handler.properties.version, old_version, + "The last report event call should be from the old version ext-handler " + "to report the event from the previous version") + + self.assertFalse(old_version_kwargs['is_success'], "The last call to report event should be for a failure") + + self.assertTrue('Error' in old_version_kwargs['message'], "No error reported") + + # This is ensuring that the error status is being written to the new version + self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=0, version=new_version) + + @patch('azurelinuxagent.ga.exthandlers.ExtHandlersHandler.handle_ext_handler_error') @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_update_command') def test_upgrade_failure_with_exception_handling(self, patch_get_update_command, - patch_handle_handle_ext_handler_error, *args): + patch_handle_ext_handler_error, *args): """ Extension upgrade failure should not be retried """ + test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_update_command, + *args) + + exthandlers_handler.run() + self.assertEqual(1, patch_get_update_command.call_count) + self.assertEqual(1, patch_handle_ext_handler_error.call_count) + + @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') + def test_extension_upgrade_should_pass_when_continue_on_update_failure_is_true_and_prev_version_disable_fails( + self, patch_get_disable_command, *args): + test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_disable_command, + *args) + + with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.is_continue_on_update_failure', return_value=True) \ + as mock_continue_on_update_failure: + # These are just testing the mocks have been called and asserting the test conditions have been met + exthandlers_handler.run() + self.assertEqual(1, patch_get_disable_command.call_count) + self.assertEqual(2, mock_continue_on_update_failure.call_count, + "This should be called twice, for both disable and uninstall") + + # Ensure the handler status and ext_status is successful + self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.1") + self._assert_ext_status(protocol.report_ext_status, "success", 0) + + @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_uninstall_command') + def test_extension_upgrade_should_pass_when_continue_on_update_failue_is_true_and_prev_version_uninstall_fails( + self, patch_get_uninstall_command, *args): + test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_uninstall_command, + *args) + + with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.is_continue_on_update_failure', return_value=True) \ + as mock_continue_on_update_failure: + # These are just testing the mocks have been called and asserting the test conditions have been met + exthandlers_handler.run() + self.assertEqual(1, patch_get_uninstall_command.call_count) + self.assertEqual(2, mock_continue_on_update_failure.call_count, + "This should be called twice, for both disable and uninstall") + + # Ensure the handler status and ext_status is successful + self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.1") + self._assert_ext_status(protocol.report_ext_status, "success", 0) + + @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') + def test_extension_upgrade_should_fail_when_continue_on_update_failure_is_false_and_prev_version_disable_fails( + self, patch_get_disable_command, *args): + test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_disable_command, + *args) + + with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.is_continue_on_update_failure', return_value=False) \ + as mock_continue_on_update_failure: + # These are just testing the mocks have been called and asserting the test conditions have been met + exthandlers_handler.run() + self.assertEqual(1, patch_get_disable_command.call_count) + self.assertEqual(1, mock_continue_on_update_failure.call_count, + "The first call would raise an exception") + + # Assert test scenario + self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=0, version="1.0.1") + + @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_uninstall_command') + def test_extension_upgrade_should_fail_when_continue_on_update_failure_is_false_and_prev_version_uninstall_fails( + self, patch_get_uninstall_command, *args): + test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_uninstall_command, + *args) + + with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.is_continue_on_update_failure', return_value=False) \ + as mock_continue_on_update_failure: + # These are just testing the mocks have been called and asserting the test conditions have been met + exthandlers_handler.run() + self.assertEqual(1, patch_get_uninstall_command.call_count) + self.assertEqual(2, mock_continue_on_update_failure.call_count, + "The second call would raise an exception") + + # Assert test scenario + self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=0, version="1.0.1") + + @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') + def test_extension_upgrade_should_fail_when_continue_on_update_failure_is_true_and_old_disable_and_new_enable_fails( + self, patch_get_disable_command, *args): + test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_disable_command, + *args) + + with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.is_continue_on_update_failure', return_value=True) \ + as mock_continue_on_update_failure: + with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_enable_command', return_value="exit 1")\ + as patch_get_enable: + + # These are just testing the mocks have been called and asserting the test conditions have been met + exthandlers_handler.run() + self.assertEqual(1, patch_get_disable_command.call_count) + self.assertEqual(2, mock_continue_on_update_failure.call_count) + self.assertEqual(1, patch_get_enable.call_count) + + # Assert test scenario + self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=1, version="1.0.1") + + @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.is_continue_on_update_failure', return_value=True) + def test_uninstall_rc_env_var_should_report_not_run_for_non_update_calls_to_exthandler_run( + self, patch_continue_on_update, *args): + test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(Mock(), *args) + + with patch.object(CGroupConfigurator.get_instance(), "start_extension_command", + side_effect=[ExtensionError("Disable Failed"), "ok", ExtensionError("uninstall failed"), + "ok", "ok", "New enable run ok"]) as patch_start_cmd: + exthandlers_handler.run() + _, update_kwargs = patch_start_cmd.call_args_list[1] + _, install_kwargs = patch_start_cmd.call_args_list[3] + _, enable_kwargs = patch_start_cmd.call_args_list[4] + + # Ensure that the env variables were present in the first run when failures were thrown for update + self.assertEqual(2, patch_continue_on_update.call_count) + self.assertTrue( + '-update' in update_kwargs['command'] and ExtCommandEnvVariable.DisableReturnCode in update_kwargs['env'], + "The update command call should have Disable Failed in env variable") + self.assertTrue( + '-install' in install_kwargs['command'] and ExtCommandEnvVariable.DisableReturnCode not in install_kwargs[ + 'env'], + "The Disable Failed env variable should be removed from install command") + self.assertTrue( + '-install' in install_kwargs['command'] and ExtCommandEnvVariable.UninstallReturnCode in install_kwargs[ + 'env'], + "The install command call should have Uninstall Failed in env variable") + self.assertTrue( + '-enable' in enable_kwargs['command'] and ExtCommandEnvVariable.UninstallReturnCode in enable_kwargs['env'], + "The enable command call should have Uninstall Failed in env variable") + + # Initiating another run which shouldn't have any failed env variables in it if no failures + # Updating Incarnation + test_data.goal_state = test_data.goal_state.replace("2<", "3<") + exthandlers_handler.run() + _, new_enable_kwargs = patch_start_cmd.call_args + + # Ensure the new run didn't have Disable Return Code env variable + self.assertNotIn(ExtCommandEnvVariable.DisableReturnCode, new_enable_kwargs['env']) + + # Ensure the new run had Uninstall Return Code env variable == NOT_RUN + self.assertIn(ExtCommandEnvVariable.UninstallReturnCode, new_enable_kwargs['env']) + self.assertTrue( + new_enable_kwargs['env'][ExtCommandEnvVariable.UninstallReturnCode] == NOT_RUN) + + # Ensure the handler status and ext_status is successful + self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.1") + self._assert_ext_status(protocol.report_ext_status, "success", 0) + + def test_ext_path_and_version_env_variables_set_for_ever_operation(self, *args): test_data = WireProtocolData(DATA_FILE_EXT_SINGLE) exthandlers_handler, protocol = self._create_mock(test_data, *args) - # Ensure initial install and enable is successful - exthandlers_handler.run() - self.assertEqual(0, patch_get_update_command.call_count) + with patch.object(CGroupConfigurator.get_instance(), "start_extension_command") as patch_start_cmd: + exthandlers_handler.run() + + # Extension Path and Version should be set for all launch_command calls + for args, kwargs in patch_start_cmd.call_args_list: + self.assertIn(ExtCommandEnvVariable.ExtensionPath, kwargs['env']) + self.assertIn('OSTCExtensions.ExampleHandlerLinux-1.0.0', + kwargs['env'][ExtCommandEnvVariable.ExtensionPath]) + self.assertIn(ExtCommandEnvVariable.ExtensionVersion, kwargs['env']) + self.assertEqual("1.0.0", kwargs['env'][ExtCommandEnvVariable.ExtensionVersion]) self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.0") - self._assert_ext_status(protocol.report_ext_status, "success", 0) - # Next incarnation, update version + @patch("azurelinuxagent.common.cgroupconfigurator.handle_process_completion", side_effect="Process Successful") + def test_ext_sequence_no_should_be_set_for_every_command_call(self, _, *args): + test_data = WireProtocolData(DATA_FILE_MULTIPLE_EXT) + exthandlers_handler, protocol = self._create_mock(test_data, *args) + + with patch("subprocess.Popen") as patch_popen: + exthandlers_handler.run() + + for _, kwargs in patch_popen.call_args_list: + self.assertIn(ExtCommandEnvVariable.ExtensionSeqNumber, kwargs['env']) + self.assertEqual(kwargs['env'][ExtCommandEnvVariable.ExtensionSeqNumber], "0") + + self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.0") + + # Next incarnation and seq for extensions, update version test_data.goal_state = test_data.goal_state.replace("1<", "2<") test_data.ext_conf = test_data.ext_conf.replace('version="1.0.0"', 'version="1.0.1"') + test_data.ext_conf = test_data.ext_conf.replace('seqNo="0"', 'seqNo="1"') test_data.manifest = test_data.manifest.replace('1.0.0', '1.0.1') + exthandlers_handler, protocol = self._create_mock(test_data, *args) - # Update command should fail - patch_get_update_command.return_value = "exit 1" - exthandlers_handler.run() - self.assertEqual(1, patch_get_update_command.call_count) - self.assertEqual(1, patch_handle_handle_ext_handler_error.call_count) + with patch("subprocess.Popen") as patch_popen: + exthandlers_handler.run() + + for _, kwargs in patch_popen.call_args_list: + self.assertIn(ExtCommandEnvVariable.ExtensionSeqNumber, kwargs['env']) + self.assertEqual(kwargs['env'][ExtCommandEnvVariable.ExtensionSeqNumber], "1") + + self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.1") + + def test_ext_sequence_no_should_be_set_from_within_extension(self, *args): + + test_file_name = "testfile.sh" + handler_json = { + "installCommand": test_file_name, + "uninstallCommand": test_file_name, + "updateCommand": test_file_name, + "enableCommand": test_file_name, + "disableCommand": test_file_name, + "rebootAfterInstall": False, + "reportHeartbeat": False, + "continueOnUpdateFailure": False + } + manifest = HandlerManifest({'handlerManifest': handler_json}) + + # Script prints env variables passed to this process and prints all starting with ConfigSequenceNumber + test_file = """ + printenv | grep ConfigSequenceNumber + """ + + base_dir = os.path.join(conf.get_lib_dir(), 'OSTCExtensions.ExampleHandlerLinux-1.0.0', test_file_name) + self.create_script(test_file_name, test_file, base_dir) + + test_data = WireProtocolData(DATA_FILE_EXT_SINGLE) + exthandlers_handler, protocol = self._create_mock(test_data, *args) + expected_seq_no = 0 + + with patch.object(ExtHandlerInstance, "load_manifest", return_value=manifest): + with patch.object(ExtHandlerInstance, 'report_event') as mock_report_event: + exthandlers_handler.run() + + for _, kwargs in mock_report_event.call_args_list: + # The output is of the format - 'testfile.sh\n[stdout]ConfigSequenceNumber=N\n[stderr]' + if test_file_name not in kwargs['message']: + continue + self.assertIn("{0}={1}".format(ExtCommandEnvVariable.ExtensionSeqNumber, expected_seq_no), + kwargs['message']) + + # Update goal state, extension version and seq no + test_data.goal_state = test_data.goal_state.replace("1<", "2<") + test_data.ext_conf = test_data.ext_conf.replace('version="1.0.0"', 'version="1.0.1"') + test_data.ext_conf = test_data.ext_conf.replace('seqNo="0"', 'seqNo="1"') + test_data.manifest = test_data.manifest.replace('1.0.0', '1.0.1') + expected_seq_no = 1 + base_dir = os.path.join(conf.get_lib_dir(), 'OSTCExtensions.ExampleHandlerLinux-1.0.1', test_file_name) + self.create_script(test_file_name, test_file, base_dir) + + with patch.object(ExtHandlerInstance, 'report_event') as mock_report_event: + exthandlers_handler.run() + + for _, kwargs in mock_report_event.call_args_list: + # The output is of the format - 'testfile.sh\n[stdout]ConfigSequenceNumber=N\n[stderr]' + if test_file_name not in kwargs['message']: + continue + self.assertIn("{0}={1}".format(ExtCommandEnvVariable.ExtensionSeqNumber, expected_seq_no), + kwargs['message']) + + def test_correct_exit_code_should_be_set_on_uninstall_cmd_failure(self, *args): + test_file_name = "testfile.sh" + test_error_file_name = "error.sh" + handler_json = { + "installCommand": test_file_name + " -install", + "uninstallCommand": test_error_file_name, + "updateCommand": test_file_name + " -update", + "enableCommand": test_file_name + " -enable", + "disableCommand": test_error_file_name, + "rebootAfterInstall": False, + "reportHeartbeat": False, + "continueOnUpdateFailure": True + } + manifest = HandlerManifest({'handlerManifest': handler_json}) + + # Script prints env variables passed to this process and prints all starting with ConfigSequenceNumber + test_file = """ + printenv | grep AZURE_ + """ + + exit_code = 151 + test_error_content = """ + exit %s + """ % exit_code + + error_dir = os.path.join(conf.get_lib_dir(), 'OSTCExtensions.ExampleHandlerLinux-1.0.0', test_error_file_name) + self.create_script(test_error_file_name, test_error_content, error_dir) + + test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(Mock(), *args) + + base_dir = os.path.join(conf.get_lib_dir(), 'OSTCExtensions.ExampleHandlerLinux-1.0.1', test_file_name) + self.create_script(test_file_name, test_file, base_dir) + + with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.load_manifest", return_value=manifest): + with patch.object(ExtHandlerInstance, 'report_event') as mock_report_event: + exthandlers_handler.run() + + _, disable_kwargs = mock_report_event.call_args_list[1] + _, update_kwargs = mock_report_event.call_args_list[2] + _, uninstall_kwargs = mock_report_event.call_args_list[3] + _, install_kwargs = mock_report_event.call_args_list[4] + _, enable_kwargs = mock_report_event.call_args_list[5] + + self.assertIn("%s=%s" % (ExtCommandEnvVariable.DisableReturnCode, exit_code), update_kwargs['message']) + self.assertIn("%s=%s" % (ExtCommandEnvVariable.UninstallReturnCode, exit_code), install_kwargs['message']) + self.assertIn("%s=%s" % (ExtCommandEnvVariable.UninstallReturnCode, exit_code), enable_kwargs['message']) @patch("azurelinuxagent.common.protocol.wire.CryptUtil") @@ -1219,7 +1901,7 @@ def _create_mock(self, mock_http_get, MockCryptUtil): test_data = WireProtocolData(DATA_FILE) - #Mock protocol to return test data + # Mock protocol to return test data mock_http_get.side_effect = test_data.mock_http_get MockCryptUtil.side_effect = test_data.mock_crypt_util @@ -1235,7 +1917,9 @@ conf.get_enable_overprovisioning = Mock(return_value=False) def wait_for_handler_successful_completion(prev_handler, wait_until): - return orig_wait_for_handler_successful_completion(prev_handler, datetime.datetime.utcnow() + datetime.timedelta(seconds=5)) + return orig_wait_for_handler_successful_completion(prev_handler, + datetime.datetime.utcnow() + datetime.timedelta( + seconds=5)) orig_wait_for_handler_successful_completion = handler.wait_for_handler_successful_completion handler.wait_for_handler_successful_completion = wait_for_handler_successful_completion @@ -1266,7 +1950,8 @@ def _validate_extension_sequence(self, expected_sequence, exthandlers_handler): installed_extensions = [a[0].name for a, k in exthandlers_handler.handle_ext_handler.call_args_list] - self.assertListEqual(expected_sequence, installed_extensions, "Expected and actual list of extensions are not equal") + self.assertListEqual(expected_sequence, installed_extensions, + "Expected and actual list of extensions are not equal") def _run_test(self, extensions_to_be_failed, expected_sequence, exthandlers_handler): ''' @@ -1280,7 +1965,7 @@ status = "error" if ext.name in extensions_to_be_failed else "success" return status - ExtHandlerInstance.get_ext_handling_status = MagicMock(side_effect = get_ext_handling_status) + ExtHandlerInstance.get_ext_handling_status = MagicMock(side_effect=get_ext_handling_status) exthandlers_handler.handle_ext_handler = MagicMock() exthandlers_handler.run() self._validate_extension_sequence(expected_sequence, exthandlers_handler) @@ -1387,6 +2072,555 @@ extensions_to_be_failed = ["G"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) + +class TestInVMArtifactsProfile(AgentTestCase): + def test_it_should_parse_boolean_values(self): + profile_json = '{ "onHold": true }' + profile = InVMArtifactsProfile(profile_json) + self.assertTrue(profile.is_on_hold(), "Failed to parse '{0}'".format(profile_json)) + + profile_json = '{ "onHold": false }' + profile = InVMArtifactsProfile(profile_json) + self.assertFalse(profile.is_on_hold(), "Failed to parse '{0}'".format(profile_json)) + + def test_it_should_parse_boolean_values_encoded_as_strings(self): + profile_json = '{ "onHold": "true" }' + profile = InVMArtifactsProfile(profile_json) + self.assertTrue(profile.is_on_hold(), "Failed to parse '{0}'".format(profile_json)) + + profile_json = '{ "onHold": "false" }' + profile = InVMArtifactsProfile(profile_json) + self.assertFalse(profile.is_on_hold(), "Failed to parse '{0}'".format(profile_json)) + + profile_json = '{ "onHold": "TRUE" }' + profile = InVMArtifactsProfile(profile_json) + self.assertTrue(profile.is_on_hold(), "Failed to parse '{0}'".format(profile_json)) + + +@skip_if_predicate_false(are_cgroups_enabled, "Does not run when Cgroups are not enabled") +@patch('time.sleep', side_effect=lambda _: mock_sleep(0.001)) +@patch("azurelinuxagent.common.cgroupapi.CGroupsApi._is_systemd", return_value=True) +@patch("azurelinuxagent.common.conf.get_cgroups_enforce_limits", return_value=False) +@patch("azurelinuxagent.common.protocol.wire.CryptUtil") +@patch("azurelinuxagent.common.utils.restutil.http_get") +class TestExtensionWithCGroupsEnabled(AgentTestCase): + def _assert_handler_status(self, report_vm_status, expected_status, + expected_ext_count, version, + expected_handler_name="OSTCExtensions.ExampleHandlerLinux"): + self.assertTrue(report_vm_status.called) + args, kw = report_vm_status.call_args + vm_status = args[0] + self.assertNotEquals(0, len(vm_status.vmAgent.extensionHandlers)) + handler_status = vm_status.vmAgent.extensionHandlers[0] + self.assertEquals(expected_status, handler_status.status) + self.assertEquals(expected_handler_name, + handler_status.name) + self.assertEquals(version, handler_status.version) + self.assertEquals(expected_ext_count, len(handler_status.extensions)) + return + + def _assert_no_handler_status(self, report_vm_status): + self.assertTrue(report_vm_status.called) + args, kw = report_vm_status.call_args + vm_status = args[0] + self.assertEquals(0, len(vm_status.vmAgent.extensionHandlers)) + return + + def _assert_ext_status(self, report_ext_status, expected_status, + expected_seq_no): + self.assertTrue(report_ext_status.called) + args, kw = report_ext_status.call_args + ext_status = args[-1] + self.assertEquals(expected_status, ext_status.status) + self.assertEquals(expected_seq_no, ext_status.sequenceNumber) + + def _create_mock(self, test_data, mock_http_get, mock_crypt_util, *args): + """Test enable/disable/uninstall of an extension""" + ext_handler = get_exthandlers_handler() + monitor_handler = get_monitor_handler() + + # Mock protocol to return test data + mock_http_get.side_effect = test_data.mock_http_get + mock_crypt_util.side_effect = test_data.mock_crypt_util + + protocol = WireProtocol("foo.bar") + protocol.detect() + protocol.report_ext_status = MagicMock() + protocol.report_vm_status = MagicMock() + + ext_handler.protocol_util.get_protocol = Mock(return_value=protocol) + monitor_handler.protocol_util.get_protocol = Mock(return_value=protocol) + return ext_handler, monitor_handler, protocol + + @attr('requires_sudo') + def test_ext_handler_with_cgroup_enabled(self, *args): + self.assertTrue(i_am_root(), "Test does not run when non-root") + + test_data = WireProtocolData(DATA_FILE) + exthandlers_handler, _, protocol = self._create_mock(test_data, *args) + + # Test enable scenario. + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") + self._assert_ext_status(protocol.report_ext_status, "success", 0) + + # Test goal state not changed + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") + + # Test goal state changed + test_data.goal_state = test_data.goal_state.replace("1<", + "2<") + test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"0\"", + "seqNo=\"1\"") + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") + self._assert_ext_status(protocol.report_ext_status, "success", 1) + + # Test hotfix + test_data.goal_state = test_data.goal_state.replace("2<", + "3<") + test_data.ext_conf = test_data.ext_conf.replace("1.0.0", "1.1.1") + test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"1\"", + "seqNo=\"2\"") + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.1") + self._assert_ext_status(protocol.report_ext_status, "success", 2) + + # Test upgrade + test_data.goal_state = test_data.goal_state.replace("3<", + "4<") + test_data.ext_conf = test_data.ext_conf.replace("1.1.1", "1.2.0") + test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"2\"", + "seqNo=\"3\"") + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.2.0") + self._assert_ext_status(protocol.report_ext_status, "success", 3) + + # Test disable + test_data.goal_state = test_data.goal_state.replace("4<", + "5<") + test_data.ext_conf = test_data.ext_conf.replace("enabled", "disabled") + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "NotReady", + 1, "1.2.0") + + # Test uninstall + test_data.goal_state = test_data.goal_state.replace("5<", + "6<") + test_data.ext_conf = test_data.ext_conf.replace("disabled", "uninstall") + exthandlers_handler.run() + self._assert_no_handler_status(protocol.report_vm_status) + + # Test uninstall again! + test_data.goal_state = test_data.goal_state.replace("6<", + "7<") + exthandlers_handler.run() + self._assert_no_handler_status(protocol.report_vm_status) + + @patch('azurelinuxagent.common.event.EventLogger.add_event') + @attr('requires_sudo') + def test_ext_handler_and_monitor_handler_with_cgroup_enabled(self, patch_add_event, *args): + self.assertTrue(i_am_root(), "Test does not run when non-root") + + test_data = WireProtocolData(DATA_FILE) + exthandlers_handler, monitor_handler, protocol = self._create_mock(test_data, *args) + + monitor_handler.last_cgroup_polling_telemetry = datetime.datetime.utcnow() - timedelta(hours=1) + monitor_handler.last_cgroup_report_telemetry = datetime.datetime.utcnow() - timedelta(hours=1) + + # Test enable scenario. + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") + self._assert_ext_status(protocol.report_ext_status, "success", 0) + + monitor_handler.poll_telemetry_metrics() + monitor_handler.send_telemetry_metrics() + + self.assertEqual(patch_add_event.call_count, 4) + + name = patch_add_event.call_args[0][0] + fields = patch_add_event.call_args[1] + + self.assertEqual(name, "WALinuxAgent") + self.assertEqual(fields["op"], "ExtensionMetricsData") + self.assertEqual(fields["is_success"], True) + self.assertEqual(fields["log_event"], False) + self.assertEqual(fields["is_internal"], False) + self.assertIsInstance(fields["message"], ustr) + + monitor_handler.stop() + + @attr('requires_sudo') + def test_ext_handler_with_systemd_cgroup_enabled(self, *args): + self.assertTrue(i_am_root(), "Test does not run when non-root") + + from azurelinuxagent.common.cgroupapi import CGroupsApi + print(CGroupsApi._is_systemd()) + + test_data = WireProtocolData(DATA_FILE) + exthandlers_handler, _, protocol = self._create_mock(test_data, *args) + + # Test enable scenario. + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") + self._assert_ext_status(protocol.report_ext_status, "success", 0) + + # Test goal state not changed + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") + + # Test goal state changed + test_data.goal_state = test_data.goal_state.replace("1<", + "2<") + test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"0\"", + "seqNo=\"1\"") + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") + self._assert_ext_status(protocol.report_ext_status, "success", 1) + + # Test hotfix + test_data.goal_state = test_data.goal_state.replace("2<", + "3<") + test_data.ext_conf = test_data.ext_conf.replace("1.0.0", "1.1.1") + test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"1\"", + "seqNo=\"2\"") + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.1") + self._assert_ext_status(protocol.report_ext_status, "success", 2) + + # Test upgrade + test_data.goal_state = test_data.goal_state.replace("3<", + "4<") + test_data.ext_conf = test_data.ext_conf.replace("1.1.1", "1.2.0") + test_data.ext_conf = test_data.ext_conf.replace("seqNo=\"2\"", + "seqNo=\"3\"") + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.2.0") + self._assert_ext_status(protocol.report_ext_status, "success", 3) + + # Test disable + test_data.goal_state = test_data.goal_state.replace("4<", + "5<") + test_data.ext_conf = test_data.ext_conf.replace("enabled", "disabled") + exthandlers_handler.run() + self._assert_handler_status(protocol.report_vm_status, "NotReady", + 1, "1.2.0") + + # Test uninstall + test_data.goal_state = test_data.goal_state.replace("5<", + "6<") + test_data.ext_conf = test_data.ext_conf.replace("disabled", "uninstall") + exthandlers_handler.run() + self._assert_no_handler_status(protocol.report_vm_status) + + # Test uninstall again! + test_data.goal_state = test_data.goal_state.replace("6<", + "7<") + exthandlers_handler.run() + self._assert_no_handler_status(protocol.report_vm_status) + + +class TestExtensionUpdateOnFailure(ExtensionTestCase): + + @staticmethod + def _get_ext_handler_instance(name, version, handler=None, continue_on_update_failure=False): + + handler_json = { + "installCommand": "sample.py -install", + "uninstallCommand": "sample.py -uninstall", + "updateCommand": "sample.py -update", + "enableCommand": "sample.py -enable", + "disableCommand": "sample.py -disable", + "rebootAfterInstall": False, + "reportHeartbeat": False, + "continueOnUpdateFailure": continue_on_update_failure + } + + if handler: + handler_json.update(handler) + + ext_handler_properties = ExtHandlerProperties() + ext_handler_properties.version = version + ext_handler = ExtHandler(name=name) + ext_handler.properties = ext_handler_properties + ext_handler_i = ExtHandlerInstance(ext_handler=ext_handler, protocol=None) + ext_handler_i.load_manifest = MagicMock(return_value=HandlerManifest({'handlerManifest': handler_json})) + fileutil.mkdir(ext_handler_i.get_base_dir()) + return ext_handler_i + + def test_disable_failed_env_variable_should_be_set_for_update_cmd_when_continue_on_update_failure_is_true( + self, *args): + old_handler_i = self._get_ext_handler_instance('foo', '1.0.0') + new_handler_i = self._get_ext_handler_instance('foo', '1.0.1', continue_on_update_failure=True) + + with patch.object(CGroupConfigurator.get_instance(), "start_extension_command", + side_effect=ExtensionError('disable Failed')) as patch_start_cmd: + with self.assertRaises(ExtensionError): + ExtHandlersHandler._update_extension_handler_and_return_if_failed(old_handler_i, new_handler_i) + + args, kwargs = patch_start_cmd.call_args + + self.assertTrue('-update' in kwargs['command'] and ExtCommandEnvVariable.DisableReturnCode in kwargs['env'], + "The update command should have Disable Failed in env variable") + + def test_uninstall_failed_env_variable_should_set_for_install_when_continue_on_update_failure_is_true( + self, *args): + old_handler_i = self._get_ext_handler_instance('foo', '1.0.0') + new_handler_i = self._get_ext_handler_instance('foo', '1.0.1', continue_on_update_failure=True) + + with patch.object(CGroupConfigurator.get_instance(), "start_extension_command", + side_effect=['ok', 'ok', ExtensionError('uninstall Failed'), 'ok']) as patch_start_cmd: + + ExtHandlersHandler._update_extension_handler_and_return_if_failed(old_handler_i, new_handler_i) + + args, kwargs = patch_start_cmd.call_args + + self.assertTrue('-install' in kwargs['command'] and ExtCommandEnvVariable.UninstallReturnCode in kwargs['env'], + "The install command should have Uninstall Failed in env variable") + + def test_extension_error_should_be_raised_when_continue_on_update_failure_is_false_on_disable_failure(self, *args): + old_handler_i = self._get_ext_handler_instance('foo', '1.0.0') + new_handler_i = self._get_ext_handler_instance('foo', '1.0.1', continue_on_update_failure=False) + + with patch.object(ExtHandlerInstance, "disable", side_effect=ExtensionError("Disable Failed")): + with self.assertRaises(ExtensionUpdateError) as error: + # Ensure the error is of type ExtensionUpdateError + ExtHandlersHandler._update_extension_handler_and_return_if_failed(old_handler_i, new_handler_i) + + msg = str(error.exception) + self.assertIn("Disable Failed", msg, "Update should fail with Disable Failed error") + self.assertIn("ExtensionError", msg, "The Exception should initially be propagated as ExtensionError") + + @patch("azurelinuxagent.common.cgroupconfigurator.handle_process_completion", side_effect="Process Successful") + def test_extension_error_should_be_raised_when_continue_on_update_failure_is_false_on_uninstall_failure(self, *args): + old_handler_i = self._get_ext_handler_instance('foo', '1.0.0') + new_handler_i = self._get_ext_handler_instance('foo', '1.0.1', continue_on_update_failure=False) + + with patch.object(ExtHandlerInstance, "uninstall", side_effect=ExtensionError("Uninstall Failed")): + with self.assertRaises(ExtensionUpdateError) as error: + # Ensure the error is of type ExtensionUpdateError + ExtHandlersHandler._update_extension_handler_and_return_if_failed(old_handler_i, new_handler_i) + + msg = str(error.exception) + self.assertIn("Uninstall Failed", msg, "Update should fail with Uninstall Failed error") + self.assertIn("ExtensionError", msg, "The Exception should initially be propagated as ExtensionError") + + @patch("azurelinuxagent.common.cgroupconfigurator.handle_process_completion", side_effect="Process Successful") + def test_extension_error_should_be_raised_when_continue_on_update_failure_is_true_on_command_failure(self, *args): + old_handler_i = self._get_ext_handler_instance('foo', '1.0.0') + new_handler_i = self._get_ext_handler_instance('foo', '1.0.1', continue_on_update_failure=True) + + # Disable Failed and update failed + with patch.object(ExtHandlerInstance, "disable", side_effect=ExtensionError("Disable Failed")): + with patch.object(ExtHandlerInstance, "update", side_effect=ExtensionError("Update Failed")): + with self.assertRaises(ExtensionError) as error: + ExtHandlersHandler._update_extension_handler_and_return_if_failed(old_handler_i, new_handler_i) + msg = str(error.exception) + self.assertIn("Update Failed", msg, "Update should fail with Update Failed error") + self.assertNotIn("ExtensionUpdateError", msg, "The exception should not be ExtensionUpdateError") + + # Uninstall Failed and install failed + with patch.object(ExtHandlerInstance, "uninstall", side_effect=ExtensionError("Uninstall Failed")): + with patch.object(ExtHandlerInstance, "install", side_effect=ExtensionError("Install Failed")): + with self.assertRaises(ExtensionError) as error: + ExtHandlersHandler._update_extension_handler_and_return_if_failed(old_handler_i, new_handler_i) + msg = str(error.exception) + self.assertIn("Install Failed", msg, "Update should fail with Install Failed error") + self.assertNotIn("ExtensionUpdateError", msg, "The exception should not be ExtensionUpdateError") + + @patch("azurelinuxagent.common.cgroupconfigurator.handle_process_completion", side_effect="Process Successful") + def test_env_variable_should_not_set_when_continue_on_update_failure_is_false(self, *args): + old_handler_i = self._get_ext_handler_instance('foo', '1.0.0') + new_handler_i = self._get_ext_handler_instance('foo', '1.0.1', continue_on_update_failure=False) + + # When Disable Fails + with patch.object(ExtHandlerInstance, "launch_command") as patch_launch_command: + with patch.object(ExtHandlerInstance, "disable", side_effect=ExtensionError("Disable Failed")): + with self.assertRaises(ExtensionUpdateError): + ExtHandlersHandler._update_extension_handler_and_return_if_failed(old_handler_i, new_handler_i) + + self.assertEqual(0, patch_launch_command.call_count, "Launch command shouldn't be called even once for" + " disable failures") + + # When Uninstall Fails + with patch.object(ExtHandlerInstance, "launch_command") as patch_launch_command: + with patch.object(ExtHandlerInstance, "uninstall", side_effect=ExtensionError("Uninstall Failed")): + with self.assertRaises(ExtensionUpdateError): + ExtHandlersHandler._update_extension_handler_and_return_if_failed(old_handler_i, new_handler_i) + + self.assertEqual(2, patch_launch_command.call_count, "Launch command should be called 2 times for " + "Disable->Update") + + @patch('time.sleep', side_effect=lambda _: mock_sleep(0.001)) + def test_failed_env_variables_should_be_set_from_within_extension_commands(self, *args): + """ + This test will test from the perspective of the extensions command weather the env variables are + being set for those processes + """ + + test_file_name = "testfile.sh" + update_file_name = test_file_name + " -update" + install_file_name = test_file_name + " -install" + old_handler_i = TestExtensionUpdateOnFailure._get_ext_handler_instance('foo', '1.0.0') + new_handler_i = TestExtensionUpdateOnFailure._get_ext_handler_instance( + 'foo', '1.0.1', + handler={"updateCommand": update_file_name, "installCommand": install_file_name}, + continue_on_update_failure=True + ) + + # Script prints env variables passed to this process and prints all starting with AZURE_ + test_file = """ + printenv | grep AZURE_ + """ + + self.create_script(file_name=test_file_name, contents=test_file, + file_path=os.path.join(new_handler_i.get_base_dir(), test_file_name)) + + with patch.object(new_handler_i, 'report_event', autospec=True) as mock_report: + # Since we're not mocking the azurelinuxagent.common.cgroupconfigurator..handle_process_completion, + # both disable.cmd and uninstall.cmd would raise ExtensionError exceptions and set the + # ExtCommandEnvVariable.DisableReturnCode and ExtCommandEnvVariable.UninstallReturnCode env variables. + # For update and install we're running the script above to print all the env variables starting with AZURE_ + # and verify accordingly if the corresponding env variables are set properly or not + ExtHandlersHandler._update_extension_handler_and_return_if_failed(old_handler_i, new_handler_i) + + _, update_kwargs = mock_report.call_args_list[0] + _, install_kwargs = mock_report.call_args_list[1] + + # Ensure we're checking variables for update scenario + self.assertIn(update_file_name, update_kwargs['message']) + self.assertIn(ExtCommandEnvVariable.DisableReturnCode, update_kwargs['message']) + self.assertTrue(ExtCommandEnvVariable.ExtensionPath in update_kwargs['message'] and + ExtCommandEnvVariable.ExtensionVersion in update_kwargs['message']) + self.assertNotIn(ExtCommandEnvVariable.UninstallReturnCode, update_kwargs['message']) + + # Ensure we're checking variables for install scenario + self.assertIn(install_file_name, install_kwargs['message']) + self.assertIn(ExtCommandEnvVariable.UninstallReturnCode, install_kwargs['message']) + self.assertTrue(ExtCommandEnvVariable.ExtensionPath in install_kwargs['message'] and + ExtCommandEnvVariable.ExtensionVersion in install_kwargs['message']) + self.assertNotIn(ExtCommandEnvVariable.DisableReturnCode, install_kwargs['message']) + + @patch('time.sleep', side_effect=lambda _: mock_sleep(0.001)) + def test_correct_exit_code_should_set_on_disable_cmd_failure(self, _): + test_env_file_name = "test_env.sh" + test_failure_file_name = "test_fail.sh" + # update_file_name = test_env_file_name + " -update" + old_handler_i = TestExtensionUpdateOnFailure._get_ext_handler_instance('foo', '1.0.0', handler={ + "disableCommand": test_failure_file_name, + "uninstallCommand": test_failure_file_name}) + new_handler_i = TestExtensionUpdateOnFailure._get_ext_handler_instance( + 'foo', '1.0.1', + handler={"updateCommand": test_env_file_name, + "updateMode": "UpdateWithoutInstall"}, + continue_on_update_failure=True + ) + + exit_code = 150 + error_test_file = """ + exit %s + """ % exit_code + + test_env_file = """ + printenv | grep AZURE_ + """ + + self.create_script(file_name=test_env_file_name, contents=test_env_file, + file_path=os.path.join(new_handler_i.get_base_dir(), test_env_file_name)) + self.create_script(file_name=test_failure_file_name, contents=error_test_file, + file_path=os.path.join(old_handler_i.get_base_dir(), test_failure_file_name)) + + with patch.object(new_handler_i, 'report_event', autospec=True) as mock_report: + + uninstall_rc = ExtHandlersHandler._update_extension_handler_and_return_if_failed(old_handler_i, new_handler_i) + _, kwargs = mock_report.call_args + + self.assertEqual(exit_code, uninstall_rc) + self.assertIn("%s=%s" % (ExtCommandEnvVariable.DisableReturnCode, exit_code), kwargs['message']) + + @patch('time.sleep', side_effect=lambda _: mock_sleep(0.0001)) + def test_timeout_code_should_set_on_cmd_timeout(self, _): + test_env_file_name = "test_env.sh" + test_failure_file_name = "test_fail.sh" + old_handler_i = TestExtensionUpdateOnFailure._get_ext_handler_instance('foo', '1.0.0', handler={ + "disableCommand": test_failure_file_name, + "uninstallCommand": test_failure_file_name}) + new_handler_i = TestExtensionUpdateOnFailure._get_ext_handler_instance( + 'foo', '1.0.1', + handler={"updateCommand": test_env_file_name + " -u", "installCommand": test_env_file_name + " -i"}, + continue_on_update_failure=True + ) + + exit_code = 156 + error_test_file = """ + sleep 1m + exit %s + """ % exit_code + + test_env_file = """ + printenv | grep AZURE_ + """ + + self.create_script(file_name=test_env_file_name, contents=test_env_file, + file_path=os.path.join(new_handler_i.get_base_dir(), test_env_file_name)) + self.create_script(file_name=test_failure_file_name, contents=error_test_file, + file_path=os.path.join(old_handler_i.get_base_dir(), test_failure_file_name)) + + with patch.object(new_handler_i, 'report_event', autospec=True) as mock_report: + uninstall_rc = ExtHandlersHandler._update_extension_handler_and_return_if_failed(old_handler_i, + new_handler_i) + _, update_kwargs = mock_report.call_args_list[0] + _, install_kwargs = mock_report.call_args_list[1] + + self.assertNotEqual(exit_code, uninstall_rc) + self.assertEqual(ExtensionErrorCodes.PluginHandlerScriptTimedout, uninstall_rc) + self.assertTrue(test_env_file_name + " -i" in install_kwargs['message'] and "%s=%s" % ( + ExtCommandEnvVariable.UninstallReturnCode, ExtensionErrorCodes.PluginHandlerScriptTimedout) in + install_kwargs['message']) + self.assertTrue(test_env_file_name + " -u" in update_kwargs['message'] and "%s=%s" % ( + ExtCommandEnvVariable.DisableReturnCode, ExtensionErrorCodes.PluginHandlerScriptTimedout) in + update_kwargs['message']) + + @patch('time.sleep', side_effect=lambda _: mock_sleep(0.0001)) + def test_success_code_should_set_in_env_variables_on_cmd_success(self, _): + test_env_file_name = "test_env.sh" + test_success_file_name = "test_success.sh" + old_handler_i = TestExtensionUpdateOnFailure._get_ext_handler_instance('foo', '1.0.0', handler={ + "disableCommand": test_success_file_name, + "uninstallCommand": test_success_file_name}) + new_handler_i = TestExtensionUpdateOnFailure._get_ext_handler_instance( + 'foo', '1.0.1', + handler={"updateCommand": test_env_file_name + " -u", "installCommand": test_env_file_name + " -i"}, + continue_on_update_failure=False + ) + + exit_code = 0 + success_test_file = """ + exit %s + """ % exit_code + + test_env_file = """ + printenv | grep AZURE_ + """ + + self.create_script(file_name=test_env_file_name, contents=test_env_file, + file_path=os.path.join(new_handler_i.get_base_dir(), test_env_file_name)) + self.create_script(file_name=test_success_file_name, contents=success_test_file, + file_path=os.path.join(old_handler_i.get_base_dir(), test_success_file_name)) + + with patch.object(new_handler_i, 'report_event', autospec=True) as mock_report: + uninstall_rc = ExtHandlersHandler._update_extension_handler_and_return_if_failed(old_handler_i, + new_handler_i) + _, update_kwargs = mock_report.call_args_list[0] + _, install_kwargs = mock_report.call_args_list[1] + + self.assertEqual(exit_code, uninstall_rc) + self.assertTrue(test_env_file_name + " -i" in install_kwargs['message'] and "%s=%s" % ( + ExtCommandEnvVariable.UninstallReturnCode, exit_code) in install_kwargs['message']) + self.assertTrue(test_env_file_name + " -u" in update_kwargs['message'] and "%s=%s" % ( + ExtCommandEnvVariable.DisableReturnCode, exit_code) in update_kwargs['message']) + + if __name__ == '__main__': unittest.main() - diff -Nru waagent-2.2.34/tests/ga/test_exthandlers.py waagent-2.2.45/tests/ga/test_exthandlers.py --- waagent-2.2.34/tests/ga/test_exthandlers.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/ga/test_exthandlers.py 2019-11-07 00:36:56.000000000 +0000 @@ -1,9 +1,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache License. import json +import subprocess from azurelinuxagent.common.protocol.restapi import ExtensionStatus, Extension, ExtHandler, ExtHandlerProperties -from azurelinuxagent.ga.exthandlers import parse_ext_status, ExtHandlerInstance +from azurelinuxagent.ga.exthandlers import parse_ext_status, ExtHandlerInstance, get_exthandlers_handler, \ + ExtCommandEnvVariable +from azurelinuxagent.common.exception import ProtocolError, ExtensionError, ExtensionErrorCodes +from azurelinuxagent.common.event import WALAEventOperation +from azurelinuxagent.common.utils.extensionprocessutil import TELEMETRY_MESSAGE_MAX_LEN, format_stdout_stderr, read_output from tests.tools import * @@ -73,6 +78,57 @@ self.assertEqual(0, ext_status.sequenceNumber) self.assertEqual(0, len(ext_status.substatusList)) + def test_parse_ext_status_should_parse_missing_substatus_as_empty(self): + status = '''[{ + "status": { + "status": "success", + "formattedMessage": { + "lang": "en-US", + "message": "Command is finished." + }, + "operation": "Enable", + "code": "0", + "name": "Microsoft.OSTCExtensions.CustomScriptForLinux" + }, + + "version": "1.0", + "timestampUTC": "2018-04-20T21:20:24Z" + } + ]''' + + extension_status = ExtensionStatus(seq_no=0) + + parse_ext_status(extension_status, json.loads(status)) + + self.assertTrue(isinstance(extension_status.substatusList, list), 'substatus was not parsed correctly') + self.assertEqual(0, len(extension_status.substatusList)) + + def test_parse_ext_status_should_parse_null_substatus_as_empty(self): + status = '''[{ + "status": { + "status": "success", + "formattedMessage": { + "lang": "en-US", + "message": "Command is finished." + }, + "operation": "Enable", + "code": "0", + "name": "Microsoft.OSTCExtensions.CustomScriptForLinux", + "substatus": null + }, + + "version": "1.0", + "timestampUTC": "2018-04-20T21:20:24Z" + } + ]''' + + extension_status = ExtensionStatus(seq_no=0) + + parse_ext_status(extension_status, json.loads(status)) + + self.assertTrue(isinstance(extension_status.substatusList, list), 'substatus was not parsed correctly') + self.assertEqual(0, len(extension_status.substatusList)) + @patch('azurelinuxagent.common.event.EventLogger.add_event') @patch('azurelinuxagent.ga.exthandlers.ExtHandlerInstance.get_largest_seq_no') def assert_extension_sequence_number(self, @@ -132,3 +188,421 @@ self.assert_extension_sequence_number(goal_state_sequence_number="-1", disk_sequence_number=3, expected_sequence_number=-1) + + @patch("azurelinuxagent.ga.exthandlers.add_event") + @patch("azurelinuxagent.common.errorstate.ErrorState.is_triggered") + @patch("azurelinuxagent.common.protocol.util.ProtocolUtil.get_protocol") + def test_it_should_report_an_error_if_the_wireserver_cannot_be_reached(self, patch_get_protocol, patch_is_triggered, patch_add_event): + test_message = "TEST MESSAGE" + + patch_get_protocol.side_effect = ProtocolError(test_message) # get_protocol will throw if the wire server cannot be reached + patch_is_triggered.return_value = True # protocol errors are reported only after a delay; force the error to be reported now + + get_exthandlers_handler().run() + + self.assertEquals(patch_add_event.call_count, 2) + + _, first_call_args = patch_add_event.call_args_list[0] + self.assertEquals(first_call_args['op'], WALAEventOperation.GetArtifactExtended) + self.assertEquals(first_call_args['is_success'], False) + + _, second_call_args = patch_add_event.call_args_list[1] + self.assertEquals(second_call_args['op'], WALAEventOperation.ExtensionProcessing) + self.assertEquals(second_call_args['is_success'], False) + self.assertIn(test_message, second_call_args['message']) + + +class LaunchCommandTestCase(AgentTestCase): + """ + Test cases for launch_command + """ + + def setUp(self): + AgentTestCase.setUp(self) + + ext_handler_properties = ExtHandlerProperties() + ext_handler_properties.version = "1.2.3" + self.ext_handler = ExtHandler(name='foo') + self.ext_handler.properties = ext_handler_properties + self.ext_handler_instance = ExtHandlerInstance(ext_handler=self.ext_handler, protocol=None) + + self.mock_get_base_dir = patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.get_base_dir", lambda *_: self.tmp_dir) + self.mock_get_base_dir.start() + + self.log_dir = os.path.join(self.tmp_dir, "log") + self.mock_get_log_dir = patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.get_log_dir", lambda *_: self.log_dir) + self.mock_get_log_dir.start() + + self.mock_sleep = patch("time.sleep", lambda *_: mock_sleep(0.01)) + self.mock_sleep.start() + + self.cgroups_enabled = CGroupConfigurator.get_instance().enabled() + CGroupConfigurator.get_instance().disable() + + def tearDown(self): + if self.cgroups_enabled: + CGroupConfigurator.get_instance().enable() + else: + CGroupConfigurator.get_instance().disable() + + self.mock_get_log_dir.stop() + self.mock_get_base_dir.stop() + self.mock_sleep.stop() + + AgentTestCase.tearDown(self) + + @staticmethod + def _output_regex(stdout, stderr): + return r"\[stdout\]\s+{0}\s+\[stderr\]\s+{1}".format(stdout, stderr) + + @staticmethod + def _find_process(command): + for pid in [pid for pid in os.listdir('/proc') if pid.isdigit()]: + try: + with open(os.path.join('/proc', pid, 'cmdline'), 'r') as cmdline: + for line in cmdline.readlines(): + if command in line: + return True + except IOError: # proc has already terminated + continue + return False + + def test_it_should_capture_the_output_of_the_command(self): + stdout = "stdout" * 5 + stderr = "stderr" * 5 + + command = self.create_script("produce_output.py", ''' +import sys + +sys.stdout.write("{0}") +sys.stderr.write("{1}") + +'''.format(stdout, stderr)) + + def list_directory(): + base_dir = self.ext_handler_instance.get_base_dir() + return [i for i in os.listdir(base_dir) if not i.endswith(".tld")] # ignore telemetry files + + files_before = list_directory() + + output = self.ext_handler_instance.launch_command(command) + + files_after = list_directory() + + self.assertRegex(output, LaunchCommandTestCase._output_regex(stdout, stderr)) + + self.assertListEqual(files_before, files_after, "Not all temporary files were deleted. File list: {0}".format(files_after)) + + def test_it_should_raise_an_exception_when_the_command_times_out(self): + extension_error_code = ExtensionErrorCodes.PluginHandlerScriptTimedout + stdout = "stdout" * 7 + stderr = "stderr" * 7 + + # the signal file is used by the test command to indicate it has produced output + signal_file = os.path.join(self.tmp_dir, "signal_file.txt") + + # the test command produces some output then goes into an infinite loop + command = self.create_script("produce_output_then_hang.py", ''' +import sys +import time + +sys.stdout.write("{0}") +sys.stdout.flush() + +sys.stderr.write("{1}") +sys.stderr.flush() + +with open("{2}", "w") as file: + while True: + file.write(".") + time.sleep(1) + +'''.format(stdout, stderr, signal_file)) + + # mock time.sleep to wait for the signal file (launch_command implements the time out using polling and sleep) + original_sleep = time.sleep + + def sleep(seconds): + if not os.path.exists(signal_file): + original_sleep(seconds) + + timeout = 60 + + start_time = time.time() + + with patch("time.sleep", side_effect=sleep, autospec=True) as mock_sleep: + + with self.assertRaises(ExtensionError) as context_manager: + self.ext_handler_instance.launch_command(command, timeout=timeout, extension_error_code=extension_error_code) + + # the command name and its output should be part of the message + message = str(context_manager.exception) + command_full_path = os.path.join(self.tmp_dir, command.lstrip(os.path.sep)) + self.assertRegex(message, r"Timeout\(\d+\):\s+{0}\s+{1}".format(command_full_path, LaunchCommandTestCase._output_regex(stdout, stderr))) + + # the exception code should be as specified in the call to launch_command + self.assertEquals(context_manager.exception.code, extension_error_code) + + # the timeout period should have elapsed + self.assertGreaterEqual(mock_sleep.call_count, timeout) + + # the command should have been terminated + self.assertFalse(LaunchCommandTestCase._find_process(command), "The command was not terminated") + + # as a check for the test itself, verify it completed in just a few seconds + self.assertLessEqual(time.time() - start_time, 5) + + def test_it_should_raise_an_exception_when_the_command_fails(self): + extension_error_code = 2345 + stdout = "stdout" * 3 + stderr = "stderr" * 3 + exit_code = 99 + + command = self.create_script("fail.py", ''' +import sys + +sys.stdout.write("{0}") +sys.stderr.write("{1}") +exit({2}) + +'''.format(stdout, stderr, exit_code)) + + # the output is captured as part of the exception message + with self.assertRaises(ExtensionError) as context_manager: + self.ext_handler_instance.launch_command(command, extension_error_code=extension_error_code) + + message = str(context_manager.exception) + self.assertRegex(message, r"Non-zero exit code: {0}.+{1}\s+{2}".format(exit_code, command, LaunchCommandTestCase._output_regex(stdout, stderr))) + + self.assertEquals(context_manager.exception.code, extension_error_code) + + def test_it_should_not_wait_for_child_process(self): + stdout = "stdout" + stderr = "stderr" + + command = self.create_script("start_child_process.py", ''' +import os +import sys +import time + +pid = os.fork() + +if pid == 0: + time.sleep(60) +else: + sys.stdout.write("{0}") + sys.stderr.write("{1}") + +'''.format(stdout, stderr)) + + start_time = time.time() + + output = self.ext_handler_instance.launch_command(command) + + self.assertLessEqual(time.time() - start_time, 5) + + # Also check that we capture the parent's output + self.assertRegex(output, LaunchCommandTestCase._output_regex(stdout, stderr)) + + def test_it_should_capture_the_output_of_child_process(self): + parent_stdout = "PARENT STDOUT" + parent_stderr = "PARENT STDERR" + child_stdout = "CHILD STDOUT" + child_stderr = "CHILD STDERR" + more_parent_stdout = "MORE PARENT STDOUT" + more_parent_stderr = "MORE PARENT STDERR" + + # the child process uses the signal file to indicate it has produced output + signal_file = os.path.join(self.tmp_dir, "signal_file.txt") + + command = self.create_script("start_child_with_output.py", ''' +import os +import sys +import time + +sys.stdout.write("{0}") +sys.stderr.write("{1}") + +pid = os.fork() + +if pid == 0: + sys.stdout.write("{2}") + sys.stderr.write("{3}") + + open("{6}", "w").close() +else: + sys.stdout.write("{4}") + sys.stderr.write("{5}") + + while not os.path.exists("{6}"): + time.sleep(0.5) + +'''.format(parent_stdout, parent_stderr, child_stdout, child_stderr, more_parent_stdout, more_parent_stderr, signal_file)) + + output = self.ext_handler_instance.launch_command(command) + + self.assertIn(parent_stdout, output) + self.assertIn(parent_stderr, output) + + self.assertIn(child_stdout, output) + self.assertIn(child_stderr, output) + + self.assertIn(more_parent_stdout, output) + self.assertIn(more_parent_stderr, output) + + def test_it_should_capture_the_output_of_child_process_that_fails_to_start(self): + parent_stdout = "PARENT STDOUT" + parent_stderr = "PARENT STDERR" + child_stdout = "CHILD STDOUT" + child_stderr = "CHILD STDERR" + + command = self.create_script("start_child_that_fails.py", ''' +import os +import sys +import time + +pid = os.fork() + +if pid == 0: + sys.stdout.write("{0}") + sys.stderr.write("{1}") + exit(1) +else: + sys.stdout.write("{2}") + sys.stderr.write("{3}") + +'''.format(child_stdout, child_stderr, parent_stdout, parent_stderr)) + + output = self.ext_handler_instance.launch_command(command) + + self.assertIn(parent_stdout, output) + self.assertIn(parent_stderr, output) + + self.assertIn(child_stdout, output) + self.assertIn(child_stderr, output) + + def test_it_should_execute_commands_with_no_output(self): + # file used to verify the command completed successfully + signal_file = os.path.join(self.tmp_dir, "signal_file.txt") + + command = self.create_script("create_file.py", ''' +open("{0}", "w").close() + +'''.format(signal_file)) + + output = self.ext_handler_instance.launch_command(command) + + self.assertTrue(os.path.exists(signal_file)) + self.assertRegex(output, LaunchCommandTestCase._output_regex('', '')) + + def test_it_should_not_capture_the_output_of_commands_that_do_their_own_redirection(self): + # the test script redirects its output to this file + command_output_file = os.path.join(self.tmp_dir, "command_output.txt") + stdout = "STDOUT" + stderr = "STDERR" + + # the test script mimics the redirection done by the Custom Script extension + command = self.create_script("produce_output", ''' +exec &> {0} +echo {1} +>&2 echo {2} + +'''.format(command_output_file, stdout, stderr)) + + output = self.ext_handler_instance.launch_command(command) + + self.assertRegex(output, LaunchCommandTestCase._output_regex('', '')) + + with open(command_output_file, "r") as command_output: + output = command_output.read() + self.assertEquals(output, "{0}\n{1}\n".format(stdout, stderr)) + + def test_it_should_truncate_the_command_output(self): + stdout = "STDOUT" + stderr = "STDERR" + + command = self.create_script("produce_long_output.py", ''' +import sys + +sys.stdout.write( "{0}" * {1}) +sys.stderr.write( "{2}" * {3}) +'''.format(stdout, int(TELEMETRY_MESSAGE_MAX_LEN / len(stdout)), stderr, int(TELEMETRY_MESSAGE_MAX_LEN / len(stderr)))) + + output = self.ext_handler_instance.launch_command(command) + + self.assertLessEqual(len(output), TELEMETRY_MESSAGE_MAX_LEN) + self.assertIn(stdout, output) + self.assertIn(stderr, output) + + def test_it_should_read_only_the_head_of_large_outputs(self): + command = self.create_script("produce_long_output.py", ''' +import sys + +sys.stdout.write("O" * 5 * 1024 * 1024) +sys.stderr.write("E" * 5 * 1024 * 1024) +''') + + # Mocking the call to file.read() is difficult, so instead we mock the call to format_stdout_stderr, which takes the + # return value of the calls to file.read(). The intention of the test is to verify we never read (and load in memory) + # more than a few KB of data from the files used to capture stdout/stderr + with patch('azurelinuxagent.common.utils.extensionprocessutil.format_stdout_stderr', side_effect=format_stdout_stderr) as mock_format: + output = self.ext_handler_instance.launch_command(command) + + self.assertGreaterEqual(len(output), 1024) + self.assertLessEqual(len(output), TELEMETRY_MESSAGE_MAX_LEN) + + mock_format.assert_called_once() + + args, kwargs = mock_format.call_args + stdout, stderr = args + + self.assertGreaterEqual(len(stdout), 1024) + self.assertLessEqual(len(stdout), TELEMETRY_MESSAGE_MAX_LEN) + + self.assertGreaterEqual(len(stderr), 1024) + self.assertLessEqual(len(stderr), TELEMETRY_MESSAGE_MAX_LEN) + + def test_it_should_handle_errors_while_reading_the_command_output(self): + command = self.create_script("produce_output.py", ''' +import sys + +sys.stdout.write("STDOUT") +sys.stderr.write("STDERR") +''') + # Mocking the call to file.read() is difficult, so instead we mock the call to_capture_process_output, + # which will call file.read() and we force stdout/stderr to be None; this will produce an exception when + # trying to use these files. + original_capture_process_output = read_output + + def capture_process_output(stdout_file, stderr_file): + return original_capture_process_output(None, None) + + with patch('azurelinuxagent.common.utils.extensionprocessutil.read_output', side_effect=capture_process_output): + output = self.ext_handler_instance.launch_command(command) + + self.assertIn("[stderr]\nCannot read stdout/stderr:", output) + + def test_it_should_contain_all_helper_environment_variables(self): + + helper_env_vars = {ExtCommandEnvVariable.ExtensionSeqNumber: self.ext_handler_instance.get_seq_no(), + ExtCommandEnvVariable.ExtensionPath: self.tmp_dir, + ExtCommandEnvVariable.ExtensionVersion: self.ext_handler_instance.ext_handler.properties.version} + + command = """ + printenv | grep -E '(%s)' + """ % '|'.join(helper_env_vars.keys()) + + test_file = self.create_script('printHelperEnvironments.sh', command) + + with patch("subprocess.Popen", wraps=subprocess.Popen) as patch_popen: + output = self.ext_handler_instance.launch_command(test_file) + + args, kwagrs = patch_popen.call_args + without_os_env = dict((k, v) for (k, v) in kwagrs['env'].items() if k not in os.environ) + + # This check will fail if any helper environment variables are added/removed later on + self.assertEqual(helper_env_vars, without_os_env) + + # This check is checking if the expected values are set for the extension commands + for helper_var in helper_env_vars: + self.assertIn("%s=%s" % (helper_var, helper_env_vars[helper_var]), output) diff -Nru waagent-2.2.34/tests/ga/test_exthandlers_download_extension.py waagent-2.2.45/tests/ga/test_exthandlers_download_extension.py --- waagent-2.2.34/tests/ga/test_exthandlers_download_extension.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/ga/test_exthandlers_download_extension.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,211 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the Apache License. + +import zipfile, time + +from azurelinuxagent.common.protocol.restapi import ExtHandler, ExtHandlerProperties, ExtHandlerPackage, ExtHandlerVersionUri +from azurelinuxagent.common.protocol.wire import WireProtocol +from azurelinuxagent.ga.exthandlers import ExtHandlerInstance, NUMBER_OF_DOWNLOAD_RETRIES +from azurelinuxagent.common.exception import ExtensionDownloadError, ExtensionErrorCodes +from tests.tools import * + + +class DownloadExtensionTestCase(AgentTestCase): + """ + Test cases for launch_command + """ + @classmethod + def setUpClass(cls): + AgentTestCase.setUpClass() + cls.mock_cgroups = patch("azurelinuxagent.ga.exthandlers.CGroupConfigurator") + cls.mock_cgroups.start() + + @classmethod + def tearDownClass(cls): + cls.mock_cgroups.stop() + + AgentTestCase.tearDownClass() + + def setUp(self): + AgentTestCase.setUp(self) + + ext_handler_properties = ExtHandlerProperties() + ext_handler_properties.version = "1.0.0" + ext_handler = ExtHandler(name='Microsoft.CPlat.Core.RunCommandLinux') + ext_handler.properties = ext_handler_properties + + protocol = WireProtocol("http://Microsoft.CPlat.Core.RunCommandLinux/foo-bar") + + self.pkg = ExtHandlerPackage() + self.pkg.uris = [ ExtHandlerVersionUri(), ExtHandlerVersionUri(), ExtHandlerVersionUri(), ExtHandlerVersionUri(), ExtHandlerVersionUri() ] + self.pkg.uris[0].uri = 'https://zrdfepirv2cy4prdstr00a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712-foobar/Microsoft.CPlat.Core__RunCommandLinux__1.0.0' + self.pkg.uris[1].uri = 'https://zrdfepirv2cy4prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712-foobar/Microsoft.CPlat.Core__RunCommandLinux__1.0.0' + self.pkg.uris[2].uri = 'https://zrdfepirv2cy4prdstr02a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712-foobar/Microsoft.CPlat.Core__RunCommandLinux__1.0.0' + self.pkg.uris[3].uri = 'https://zrdfepirv2cy4prdstr03a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712-foobar/Microsoft.CPlat.Core__RunCommandLinux__1.0.0' + self.pkg.uris[4].uri = 'https://zrdfepirv2cy4prdstr04a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712-foobar/Microsoft.CPlat.Core__RunCommandLinux__1.0.0' + + self.ext_handler_instance = ExtHandlerInstance(ext_handler=ext_handler, protocol=protocol) + self.ext_handler_instance.pkg = self.pkg + + self.extension_dir = os.path.join(self.tmp_dir, "Microsoft.CPlat.Core.RunCommandLinux-1.0.0") + self.mock_get_base_dir = patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.get_base_dir", return_value=self.extension_dir) + self.mock_get_base_dir.start() + + self.mock_get_log_dir = patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.get_log_dir", return_value=self.tmp_dir) + self.mock_get_log_dir.start() + + self.agent_dir = self.tmp_dir + self.mock_get_lib_dir = patch("azurelinuxagent.ga.exthandlers.conf.get_lib_dir", return_value=self.agent_dir) + self.mock_get_lib_dir.start() + + def tearDown(self): + self.mock_get_lib_dir.stop() + self.mock_get_log_dir.stop() + self.mock_get_base_dir.stop() + + AgentTestCase.tearDown(self) + + _extension_command = "RunCommandLinux.sh" + + @staticmethod + def _create_zip_file(filename): + file = None + try: + file = zipfile.ZipFile(filename, "w") + info = zipfile.ZipInfo(DownloadExtensionTestCase._extension_command) + info.date_time = time.localtime(time.time())[:6] + info.compress_type = zipfile.ZIP_DEFLATED + file.writestr(info, "#!/bin/sh\necho 'RunCommandLinux executed successfully'\n") + finally: + if file is not None: + file.close() + + @staticmethod + def _create_invalid_zip_file(filename): + with open(filename, "w") as file: + file.write("An invalid ZIP file\n") + + def _get_extension_package_file(self): + return os.path.join(self.agent_dir, self.ext_handler_instance.get_extension_package_zipfile_name()) + + def _get_extension_command_file(self): + return os.path.join(self.extension_dir, DownloadExtensionTestCase._extension_command) + + def _assert_download_and_expand_succeeded(self): + self.assertTrue(os.path.exists(self._get_extension_package_file()), "The extension package was not downloaded to the expected location") + self.assertTrue(os.path.exists(self._get_extension_command_file()), "The extension package was not expanded to the expected location") + + def test_it_should_download_and_expand_extension_package(self): + def download_ext_handler_pkg(_uri, destination): + DownloadExtensionTestCase._create_zip_file(destination) + return True + + with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg", side_effect=download_ext_handler_pkg) as mock_download_ext_handler_pkg: + with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.report_event") as mock_report_event: + self.ext_handler_instance.download() + + # first download attempt should succeed + mock_download_ext_handler_pkg.assert_called_once() + mock_report_event.assert_called_once() + + self._assert_download_and_expand_succeeded() + + def test_it_should_use_existing_extension_package_when_already_downloaded(self): + DownloadExtensionTestCase._create_zip_file(self._get_extension_package_file()) + + with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg") as mock_download_ext_handler_pkg: + with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.report_event") as mock_report_event: + self.ext_handler_instance.download() + + mock_download_ext_handler_pkg.assert_not_called() + mock_report_event.assert_not_called() + + self.assertTrue(os.path.exists(self._get_extension_command_file()), "The extension package was not expanded to the expected location") + + def test_it_should_ignore_existing_extension_package_when_it_is_invalid(self): + def download_ext_handler_pkg(_uri, destination): + DownloadExtensionTestCase._create_zip_file(destination) + return True + + DownloadExtensionTestCase._create_invalid_zip_file(self._get_extension_package_file()) + + with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg", side_effect=download_ext_handler_pkg) as mock_download_ext_handler_pkg: + self.ext_handler_instance.download() + + mock_download_ext_handler_pkg.assert_called_once() + + self._assert_download_and_expand_succeeded() + + def test_it_should_use_alternate_uris_when_download_fails(self): + self.download_failures = 0 + + def download_ext_handler_pkg(_uri, destination): + # fail a few times, then succeed + if self.download_failures < 3: + self.download_failures += 1 + return False + DownloadExtensionTestCase._create_zip_file(destination) + return True + + with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg", side_effect=download_ext_handler_pkg) as mock_download_ext_handler_pkg: + self.ext_handler_instance.download() + + self.assertEquals(mock_download_ext_handler_pkg.call_count, self.download_failures + 1) + + self._assert_download_and_expand_succeeded() + + def test_it_should_use_alternate_uris_when_download_raises_an_exception(self): + self.download_failures = 0 + + def download_ext_handler_pkg(_uri, destination): + # fail a few times, then succeed + if self.download_failures < 3: + self.download_failures += 1 + raise Exception("Download failed") + DownloadExtensionTestCase._create_zip_file(destination) + return True + + with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg", side_effect=download_ext_handler_pkg) as mock_download_ext_handler_pkg: + self.ext_handler_instance.download() + + self.assertEquals(mock_download_ext_handler_pkg.call_count, self.download_failures + 1) + + self._assert_download_and_expand_succeeded() + + def test_it_should_use_alternate_uris_when_it_downloads_an_invalid_package(self): + self.download_failures = 0 + + def download_ext_handler_pkg(_uri, destination): + # fail a few times, then succeed + if self.download_failures < 3: + self.download_failures += 1 + DownloadExtensionTestCase._create_invalid_zip_file(destination) + else: + DownloadExtensionTestCase._create_zip_file(destination) + return True + + with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg", side_effect=download_ext_handler_pkg) as mock_download_ext_handler_pkg: + self.ext_handler_instance.download() + + self.assertEquals(mock_download_ext_handler_pkg.call_count, self.download_failures + 1) + + self._assert_download_and_expand_succeeded() + + def test_it_should_raise_an_exception_when_all_downloads_fail(self): + def download_ext_handler_pkg(_uri, _destination): + DownloadExtensionTestCase._create_invalid_zip_file(self._get_extension_package_file()) + return True + + with patch("time.sleep", lambda *_: None): + with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg", side_effect=download_ext_handler_pkg) as mock_download_ext_handler_pkg: + with self.assertRaises(ExtensionDownloadError) as context_manager: + self.ext_handler_instance.download() + + self.assertEquals(mock_download_ext_handler_pkg.call_count, NUMBER_OF_DOWNLOAD_RETRIES * len(self.pkg.uris)) + + self.assertRegex(str(context_manager.exception), "Failed to download extension") + self.assertEquals(context_manager.exception.code, ExtensionErrorCodes.PluginManifestDownloadError) + + self.assertFalse(os.path.exists(self.extension_dir), "The extension directory was not removed") + self.assertFalse(os.path.exists(self._get_extension_package_file()), "The extension package was not removed") + diff -Nru waagent-2.2.34/tests/ga/test_exthandlers_exthandlerinstance.py waagent-2.2.45/tests/ga/test_exthandlers_exthandlerinstance.py --- waagent-2.2.34/tests/ga/test_exthandlers_exthandlerinstance.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/ga/test_exthandlers_exthandlerinstance.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,128 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the Apache License. + +from azurelinuxagent.ga.exthandlers import ExtHandlerInstance +from azurelinuxagent.common.protocol.restapi import ExtHandler, ExtHandlerProperties, ExtHandlerPackage, \ + ExtHandlerVersionUri +from tests.tools import * + + +class ExtHandlerInstanceTestCase(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + + ext_handler_properties = ExtHandlerProperties() + ext_handler_properties.version = "1.2.3" + ext_handler = ExtHandler(name='foo') + ext_handler.properties = ext_handler_properties + self.ext_handler_instance = ExtHandlerInstance(ext_handler=ext_handler, protocol=None) + + pkg_uri = ExtHandlerVersionUri() + pkg_uri.uri = "http://bar/foo__1.2.3" + self.ext_handler_instance.pkg = ExtHandlerPackage(ext_handler_properties.version) + self.ext_handler_instance.pkg.uris.append(pkg_uri) + + self.base_dir = self.tmp_dir + self.extension_directory = os.path.join(self.tmp_dir, "extension_directory") + self.mock_get_base_dir = patch.object(self.ext_handler_instance, "get_base_dir", return_value=self.extension_directory) + self.mock_get_base_dir.start() + + def tearDown(self): + self.mock_get_base_dir.stop() + + def test_rm_ext_handler_dir_should_remove_the_extension_packages(self): + os.mkdir(self.extension_directory) + open(os.path.join(self.extension_directory, "extension_file1"), 'w').close() + open(os.path.join(self.extension_directory, "extension_file2"), 'w').close() + open(os.path.join(self.extension_directory, "extension_file3"), 'w').close() + open(os.path.join(self.base_dir, "foo__1.2.3.zip"), 'w').close() + + self.ext_handler_instance.remove_ext_handler() + + self.assertFalse(os.path.exists(self.extension_directory)) + self.assertFalse(os.path.exists(os.path.join(self.base_dir, "foo__1.2.3.zip"))) + + def test_rm_ext_handler_dir_should_remove_the_extension_directory(self): + os.mkdir(self.extension_directory) + os.mknod(os.path.join(self.extension_directory, "extension_file1")) + os.mknod(os.path.join(self.extension_directory, "extension_file2")) + os.mknod(os.path.join(self.extension_directory, "extension_file3")) + + self.ext_handler_instance.remove_ext_handler() + + self.assertFalse(os.path.exists(self.extension_directory)) + + def test_rm_ext_handler_dir_should_not_report_an_event_if_the_extension_directory_does_not_exist(self): + if os.path.exists(self.extension_directory): + os.rmdir(self.extension_directory) + + with patch.object(self.ext_handler_instance, "report_event") as mock_report_event: + self.ext_handler_instance.remove_ext_handler() + + mock_report_event.assert_not_called() + + def test_rm_ext_handler_dir_should_not_report_an_event_if_a_child_is_removed_asynchronously_while_deleting_the_extension_directory(self): + os.mkdir(self.extension_directory) + os.mknod(os.path.join(self.extension_directory, "extension_file1")) + os.mknod(os.path.join(self.extension_directory, "extension_file2")) + os.mknod(os.path.join(self.extension_directory, "extension_file3")) + + # + # Some extensions uninstall asynchronously and the files we are trying to remove may be removed + # while shutil.rmtree is traversing the extension's directory. Mock this by deleting a file + # twice (the second call will produce "[Errno 2] No such file or directory", which should not be + # reported as a telemetry event. + # In order to mock this, we need to know that remove_ext_handler invokes Pyhon's shutil.rmtree, + # which in turn invokes os.unlink (Python 3) or os.remove (Python 2) + # + remove_api_name = "unlink" if sys.version_info >= (3, 0) else "remove" + + original_remove_api = getattr(shutil.os, remove_api_name) + + extension_directory = self.extension_directory + + def mock_remove(path, dir_fd=None): + if dir_fd is not None: # path is relative, make it absolute + path = os.path.join(extension_directory, path) + + if path.endswith("extension_file2"): + original_remove_api(path) + mock_remove.file_deleted_asynchronously = True + original_remove_api(path) + + mock_remove.file_deleted_asynchronously = False + + with patch.object(shutil.os, remove_api_name, mock_remove): + with patch.object(self.ext_handler_instance, "report_event") as mock_report_event: + self.ext_handler_instance.remove_ext_handler() + + mock_report_event.assert_not_called() + + # The next 2 asserts are checks on the mock itself, in case the implementation of remove_ext_handler changes (mocks may need to be updated then) + self.assertTrue(mock_remove.file_deleted_asynchronously) # verify the mock was actually called + self.assertFalse(os.path.exists(self.extension_directory)) # verify the error produced by the mock did not prevent the deletion + + def test_rm_ext_handler_dir_should_report_an_event_if_an_error_occurs_while_deleting_the_extension_directory(self): + os.mkdir(self.extension_directory) + os.mknod(os.path.join(self.extension_directory, "extension_file1")) + os.mknod(os.path.join(self.extension_directory, "extension_file2")) + os.mknod(os.path.join(self.extension_directory, "extension_file3")) + + # The mock below relies on the knowledge that remove_ext_handler invokes Pyhon's shutil.rmtree, + # which in turn invokes os.unlink (Python 3) or os.remove (Python 2) + remove_api_name = "unlink" if sys.version_info >= (3, 0) else "remove" + + original_remove_api = getattr(shutil.os, remove_api_name) + + def mock_remove(path, dir_fd=None): + if path.endswith("extension_file2"): + raise IOError("A mocked error") + original_remove_api(path) + + with patch.object(shutil.os, remove_api_name, mock_remove): + with patch.object(self.ext_handler_instance, "report_event") as mock_report_event: + self.ext_handler_instance.remove_ext_handler() + + args, kwargs = mock_report_event.call_args + self.assertTrue("A mocked error" in kwargs["message"]) + diff -Nru waagent-2.2.34/tests/ga/test_monitor.py waagent-2.2.45/tests/ga/test_monitor.py --- waagent-2.2.34/tests/ga/test_monitor.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/ga/test_monitor.py 2019-11-07 00:36:56.000000000 +0000 @@ -14,11 +14,90 @@ # # Requires Python 2.6+ and Openssl 1.0+ # +import datetime +import json +import os +import platform +import random +import shutil +import string +import sys +import tempfile +import time from datetime import timedelta +from mock import Mock, MagicMock, patch +from nose.plugins.attrib import attr + +from azurelinuxagent.common import logger +from azurelinuxagent.common.cgroup import CGroup +from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry +from azurelinuxagent.common.datacontract import get_properties +from azurelinuxagent.common.event import EventLogger, WALAEventOperation, CONTAINER_ID_ENV_VARIABLE +from azurelinuxagent.common.exception import HttpError +from azurelinuxagent.common.future import ustr +from azurelinuxagent.common.protocol.imds import ComputeInfo +from azurelinuxagent.common.protocol.restapi import VMInfo from azurelinuxagent.common.protocol.wire import WireProtocol -from tests.tools import * -from azurelinuxagent.ga.monitor import * +from azurelinuxagent.common.telemetryevent import TelemetryEventParam, TelemetryEvent +from azurelinuxagent.common.utils import restutil, fileutil +from azurelinuxagent.common.version import AGENT_VERSION, CURRENT_VERSION, AGENT_NAME, CURRENT_AGENT +from azurelinuxagent.ga.monitor import parse_xml_event, get_monitor_handler, MonitorHandler, \ + generate_extension_metrics_telemetry_dictionary, parse_json_event +from tests.common.test_cgroupstelemetry import make_new_cgroup, consume_cpu_time, consume_memory +from tests.protocol.mockwiredata import WireProtocolData, DATA_FILE, conf +from tests.tools import load_data, AgentTestCase, data_dir, are_cgroups_enabled, i_am_root, skip_if_predicate_false + + +class ResponseMock(Mock): + def __init__(self, status=restutil.httpclient.OK, response=None, reason=None): + Mock.__init__(self) + self.status = status + self.reason = reason + self.response = response + + def read(self): + return self.response + + +def random_generator(size=6, chars=string.ascii_uppercase + string.digits + string.ascii_lowercase): + return ''.join(random.choice(chars) for x in range(size)) + + +def create_dummy_event(size=0, + name="DummyExtension", + op=WALAEventOperation.Unknown, + is_success=True, + duration=0, + version=CURRENT_VERSION, + is_internal=False, + evt_type="", + message="DummyMessage", + invalid_chars=False): + return get_event_message(name=size if size != 0 else name, + op=op, + is_success=is_success, + duration=duration, + version=version, + message=random_generator(size) if size != 0 else message, + evt_type=evt_type, + is_internal=is_internal) + + +def get_event_message(duration, evt_type, is_internal, is_success, message, name, op, version, eventId=1): + event = TelemetryEvent(eventId, "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") + event.parameters.append(TelemetryEventParam('Name', name)) + event.parameters.append(TelemetryEventParam('Version', str(version))) + event.parameters.append(TelemetryEventParam('IsInternal', is_internal)) + event.parameters.append(TelemetryEventParam('Operation', op)) + event.parameters.append(TelemetryEventParam('OperationSuccess', is_success)) + event.parameters.append(TelemetryEventParam('Message', message)) + event.parameters.append(TelemetryEventParam('Duration', duration)) + event.parameters.append(TelemetryEventParam('ExtensionType', evt_type)) + event.parameters.append(TelemetryEventParam('OpcodeName', '2019-11-06 02:00:44.307835')) + + data = get_properties(event) + return json.dumps(data) @patch('azurelinuxagent.common.event.EventLogger.add_event') @@ -30,68 +109,208 @@ class TestMonitor(AgentTestCase): def test_parse_xml_event(self, *args): - data_str = load_data('ext/event.xml') + data_str = load_data('ext/event_from_extension.xml') event = parse_xml_event(data_str) self.assertNotEqual(None, event) self.assertNotEqual(0, event.parameters) - self.assertNotEqual(None, event.parameters[0]) + self.assertTrue(all(param is not None for param in event.parameters)) + + def test_parse_json_event(self, *args): + data_str = load_data('ext/event.json') + event = parse_json_event(data_str) + self.assertNotEqual(None, event) + self.assertNotEqual(0, event.parameters) + self.assertTrue(all(param is not None for param in event.parameters)) + + def test_add_sysinfo_should_honor_sysinfo_values_from_agent_for_agent_events(self, *args): + data_str = load_data('ext/event_from_agent.json') + event = parse_json_event(data_str) - def test_add_sysinfo(self, *args): - data_str = load_data('ext/event.xml') - event = parse_xml_event(data_str) monitor_handler = get_monitor_handler() - vm_name = 'dummy_vm' - tenant_name = 'dummy_tenant' - role_name = 'dummy_role' - role_instance_name = 'dummy_role_instance' - container_id = 'dummy_container_id' + sysinfo_vm_name_value = "sysinfo_dummy_vm" + sysinfo_tenant_name_value = "sysinfo_dummy_tenant" + sysinfo_role_name_value = "sysinfo_dummy_role" + sysinfo_role_instance_name_value = "sysinfo_dummy_role_instance" + sysinfo_execution_mode_value = "sysinfo_IAAS" + container_id_value = "TEST-CONTAINER-ID-ALREADY-PRESENT-GUID" + GAVersion_value = "WALinuxAgent-2.2.44" + OpcodeName_value = "2019-11-02 01:42:49.188030" + EventTid_value = 140240384030528 + EventPid_value = 108573 + TaskName_value = "ExtHandler" + KeywordName_value = "" vm_name_param = "VMName" tenant_name_param = "TenantName" role_name_param = "RoleName" role_instance_name_param = "RoleInstanceName" + execution_mode_param = "ExecutionMode" container_id_param = "ContainerId" + GAVersion_param = "GAVersion" + OpcodeName_param = "OpcodeName" + EventTid_param = "EventTid" + EventPid_param = "EventPid" + TaskName_param = "TaskName" + KeywordName_param = "KeywordName" + + sysinfo = [ + TelemetryEventParam(role_instance_name_param, sysinfo_role_instance_name_value), + TelemetryEventParam(vm_name_param, sysinfo_vm_name_value), + TelemetryEventParam(execution_mode_param, sysinfo_execution_mode_value), + TelemetryEventParam(tenant_name_param, sysinfo_tenant_name_value), + TelemetryEventParam(role_name_param, sysinfo_role_name_value) + ] + monitor_handler.sysinfo = sysinfo + monitor_handler.add_sysinfo(event) - sysinfo = [TelemetryEventParam(vm_name_param, vm_name), - TelemetryEventParam(tenant_name_param, tenant_name), - TelemetryEventParam(role_name_param, role_name), - TelemetryEventParam(role_instance_name_param, role_instance_name), - TelemetryEventParam(container_id_param, container_id)] + self.assertNotEqual(None, event) + self.assertNotEqual(0, event.parameters) + self.assertTrue(all(param is not None for param in event.parameters)) + + counter = 0 + for p in event.parameters: + if p.name == vm_name_param: + self.assertEqual(sysinfo_vm_name_value, p.value) + counter += 1 + elif p.name == tenant_name_param: + self.assertEqual(sysinfo_tenant_name_value, p.value) + counter += 1 + elif p.name == role_name_param: + self.assertEqual(sysinfo_role_name_value, p.value) + counter += 1 + elif p.name == role_instance_name_param: + self.assertEqual(sysinfo_role_instance_name_value, p.value) + counter += 1 + elif p.name == execution_mode_param: + self.assertEqual(sysinfo_execution_mode_value, p.value) + counter += 1 + elif p.name == container_id_param: + self.assertEqual(container_id_value, p.value) + counter += 1 + elif p.name == GAVersion_param: + self.assertEqual(GAVersion_value, p.value) + counter += 1 + elif p.name == OpcodeName_param: + self.assertEqual(OpcodeName_value, p.value) + counter += 1 + elif p.name == EventTid_param: + self.assertEqual(EventTid_value, p.value) + counter += 1 + elif p.name == EventPid_param: + self.assertEqual(EventPid_value, p.value) + counter += 1 + elif p.name == TaskName_param: + self.assertEqual(TaskName_value, p.value) + counter += 1 + elif p.name == KeywordName_param: + self.assertEqual(KeywordName_value, p.value) + counter += 1 + + self.assertEqual(12, counter) + + def test_add_sysinfo_should_honor_sysinfo_values_from_agent_for_extension_events(self, *args): + # The difference between agent and extension events is that extension events don't have the container id + # populated on the fly like the agent events do. Ensure the container id is populated in add_sysinfo. + data_str = load_data('ext/event_from_extension.xml') + event = parse_xml_event(data_str) + monitor_handler = get_monitor_handler() + + # Prepare the os environment variable to read the container id value from + container_id_value = "TEST-CONTAINER-ID-ADDED-IN-SYSINFO-GUID" + os.environ[CONTAINER_ID_ENV_VARIABLE] = container_id_value + + sysinfo_vm_name_value = "sysinfo_dummy_vm" + sysinfo_tenant_name_value = "sysinfo_dummy_tenant" + sysinfo_role_name_value = "sysinfo_dummy_role" + sysinfo_role_instance_name_value = "sysinfo_dummy_role_instance" + sysinfo_execution_mode_value = "sysinfo_IAAS" + GAVersion_value = "WALinuxAgent-2.2.44" + OpcodeName_value = "" + EventTid_value = 0 + EventPid_value = 0 + TaskName_value = "" + KeywordName_value = "" + + vm_name_param = "VMName" + tenant_name_param = "TenantName" + role_name_param = "RoleName" + role_instance_name_param = "RoleInstanceName" + execution_mode_param = "ExecutionMode" + container_id_param = "ContainerId" + GAVersion_param = "GAVersion" + OpcodeName_param = "OpcodeName" + EventTid_param = "EventTid" + EventPid_param = "EventPid" + TaskName_param = "TaskName" + KeywordName_param = "KeywordName" + + sysinfo = [ + TelemetryEventParam(role_instance_name_param, sysinfo_role_instance_name_value), + TelemetryEventParam(vm_name_param, sysinfo_vm_name_value), + TelemetryEventParam(execution_mode_param, sysinfo_execution_mode_value), + TelemetryEventParam(tenant_name_param, sysinfo_tenant_name_value), + TelemetryEventParam(role_name_param, sysinfo_role_name_value) + ] monitor_handler.sysinfo = sysinfo monitor_handler.add_sysinfo(event) self.assertNotEqual(None, event) self.assertNotEqual(0, event.parameters) - self.assertNotEqual(None, event.parameters[0]) + self.assertTrue(all(param is not None for param in event.parameters)) + counter = 0 for p in event.parameters: if p.name == vm_name_param: - self.assertEqual(vm_name, p.value) + self.assertEqual(sysinfo_vm_name_value, p.value) counter += 1 elif p.name == tenant_name_param: - self.assertEqual(tenant_name, p.value) + self.assertEqual(sysinfo_tenant_name_value, p.value) counter += 1 elif p.name == role_name_param: - self.assertEqual(role_name, p.value) + self.assertEqual(sysinfo_role_name_value, p.value) counter += 1 elif p.name == role_instance_name_param: - self.assertEqual(role_instance_name, p.value) + self.assertEqual(sysinfo_role_instance_name_value, p.value) + counter += 1 + elif p.name == execution_mode_param: + self.assertEqual(sysinfo_execution_mode_value, p.value) counter += 1 elif p.name == container_id_param: - self.assertEqual(container_id, p.value) + self.assertEqual(container_id_value, p.value) + counter += 1 + elif p.name == GAVersion_param: + self.assertEqual(GAVersion_value, p.value) + counter += 1 + elif p.name == OpcodeName_param: + self.assertEqual(OpcodeName_value, p.value) + counter += 1 + elif p.name == EventTid_param: + self.assertEqual(EventTid_value, p.value) + counter += 1 + elif p.name == EventPid_param: + self.assertEqual(EventPid_value, p.value) + counter += 1 + elif p.name == TaskName_param: + self.assertEqual(TaskName_value, p.value) + counter += 1 + elif p.name == KeywordName_param: + self.assertEqual(KeywordName_value, p.value) counter += 1 - self.assertEqual(5, counter) + self.assertEqual(12, counter) + os.environ.pop(CONTAINER_ID_ENV_VARIABLE) @patch("azurelinuxagent.ga.monitor.MonitorHandler.send_telemetry_heartbeat") @patch("azurelinuxagent.ga.monitor.MonitorHandler.collect_and_send_events") @patch("azurelinuxagent.ga.monitor.MonitorHandler.send_host_plugin_heartbeat") - @patch("azurelinuxagent.ga.monitor.MonitorHandler.send_cgroup_telemetry") + @patch("azurelinuxagent.ga.monitor.MonitorHandler.poll_telemetry_metrics") + @patch("azurelinuxagent.ga.monitor.MonitorHandler.send_telemetry_metrics") @patch("azurelinuxagent.ga.monitor.MonitorHandler.send_imds_heartbeat") def test_heartbeats(self, patch_imds_heartbeat, - patch_cgroup_telemetry, + patch_send_telemetry_metrics, + patch_poll_telemetry_metrics, patch_hostplugin_heartbeat, patch_send_events, patch_telemetry_heartbeat, @@ -107,7 +326,8 @@ self.assertEqual(0, patch_send_events.call_count) self.assertEqual(0, patch_telemetry_heartbeat.call_count) self.assertEqual(0, patch_imds_heartbeat.call_count) - self.assertEqual(0, patch_cgroup_telemetry.call_count) + self.assertEqual(0, patch_send_telemetry_metrics.call_count) + self.assertEqual(0, patch_poll_telemetry_metrics.call_count) monitor_handler.start() time.sleep(1) @@ -117,11 +337,13 @@ self.assertNotEqual(0, patch_send_events.call_count) self.assertNotEqual(0, patch_telemetry_heartbeat.call_count) self.assertNotEqual(0, patch_imds_heartbeat.call_count) - self.assertNotEqual(0, patch_cgroup_telemetry.call_count) + self.assertNotEqual(0, patch_send_telemetry_metrics.call_count) + self.assertNotEqual(0, patch_poll_telemetry_metrics.call_count) monitor_handler.stop() - @patch("azurelinuxagent.ga.monitor.MonitorHandler.send_cgroup_telemetry") + @patch("azurelinuxagent.ga.monitor.MonitorHandler.send_telemetry_metrics") + @patch("azurelinuxagent.ga.monitor.MonitorHandler.poll_telemetry_metrics") def test_heartbeat_timings_updates_after_window(self, *args): monitor_handler = get_monitor_handler() @@ -158,7 +380,8 @@ monitor_handler.stop() - @patch("azurelinuxagent.ga.monitor.MonitorHandler.send_cgroup_telemetry") + @patch("azurelinuxagent.ga.monitor.MonitorHandler.send_telemetry_metrics") + @patch("azurelinuxagent.ga.monitor.MonitorHandler.poll_telemetry_metrics") def test_heartbeat_timings_no_updates_within_window(self, *args): monitor_handler = get_monitor_handler() @@ -218,6 +441,556 @@ self.assertEqual(False, args[5].call_args[1]['is_success']) monitor_handler.stop() + @patch('azurelinuxagent.common.logger.Logger.info') + def test_reset_loggers(self, mock_info, *args): + # Adding 100 different messages + for i in range(100): + event_message = "Test {0}".format(i) + logger.periodic_info(logger.EVERY_DAY, event_message) + + self.assertIn(hash(event_message), logger.DEFAULT_LOGGER.periodic_messages) + self.assertEqual(i + 1, mock_info.call_count) # range starts from 0. + + self.assertEqual(100, len(logger.DEFAULT_LOGGER.periodic_messages)) + + # Adding 1 message 100 times, but the same message. Mock Info should be called only once. + for i in range(100): + logger.periodic_info(logger.EVERY_DAY, "Test-Message") + + self.assertIn(hash("Test-Message"), logger.DEFAULT_LOGGER.periodic_messages) + self.assertEqual(101, mock_info.call_count) # 100 calls from the previous section. Adding only 1. + self.assertEqual(101, len(logger.DEFAULT_LOGGER.periodic_messages)) # One new message in the hash map. + + # Resetting the logger time states. + monitor_handler = get_monitor_handler() + monitor_handler.last_reset_loggers_time = datetime.datetime.utcnow() - timedelta(hours=1) + MonitorHandler.RESET_LOGGERS_PERIOD = timedelta(milliseconds=100) + + monitor_handler.reset_loggers() + + # The hash map got cleaned up by the reset_loggers method + self.assertEqual(0, len(logger.DEFAULT_LOGGER.periodic_messages)) + + monitor_handler.stop() + + @patch("azurelinuxagent.common.logger.reset_periodic", side_effect=Exception()) + def test_reset_loggers_ensuring_timestamp_gets_updated(self, *args): + # Resetting the logger time states. + monitor_handler = get_monitor_handler() + initial_time = datetime.datetime.utcnow() - timedelta(hours=1) + monitor_handler.last_reset_loggers_time = initial_time + MonitorHandler.RESET_LOGGERS_PERIOD = timedelta(milliseconds=100) + + # noinspection PyBroadException + try: + monitor_handler.reset_loggers() + except: + pass + + # The hash map got cleaned up by the reset_loggers method + self.assertGreater(monitor_handler.last_reset_loggers_time, initial_time) + monitor_handler.stop() + + +@patch('azurelinuxagent.common.osutil.get_osutil') +@patch("azurelinuxagent.common.protocol.healthservice.HealthService._report") +@patch("azurelinuxagent.common.protocol.wire.CryptUtil") +@patch("azurelinuxagent.common.utils.restutil.http_get") +class TestEventMonitoring(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + self.lib_dir = tempfile.mkdtemp() + + self.event_logger = EventLogger() + self.event_logger.event_dir = os.path.join(self.lib_dir, "events") + + def tearDown(self): + fileutil.rm_dirs(self.lib_dir) + + def _create_mock(self, test_data, mock_http_get, MockCryptUtil, *args): + """Test enable/disable/uninstall of an extension""" + monitor_handler = get_monitor_handler() + + # Mock protocol to return test data + mock_http_get.side_effect = test_data.mock_http_get + MockCryptUtil.side_effect = test_data.mock_crypt_util + + protocol = WireProtocol("foo.bar") + protocol.detect() + protocol.report_ext_status = MagicMock() + protocol.report_vm_status = MagicMock() + + monitor_handler.protocol_util.get_protocol = Mock(return_value=protocol) + return monitor_handler, protocol + + @patch("azurelinuxagent.common.protocol.imds.ImdsClient.get_compute", + return_value=ComputeInfo(subscriptionId="DummySubId", + location="DummyVMLocation", + vmId="DummyVmId", + resourceGroupName="DummyRG", + publisher="")) + @patch("azurelinuxagent.common.protocol.wire.WireProtocol.get_vminfo", + return_value=VMInfo(subscriptionId="DummySubId", + vmName="DummyVMName", + containerId="DummyContainerId", + roleName="DummyRoleName", + roleInstanceName="DummyRoleInstanceName", tenantName="DummyTenant")) + @patch("platform.release", return_value="platform-release") + @patch("platform.system", return_value="Linux") + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_processor_cores", return_value=4) + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_total_mem", return_value=10000) + def mock_init_sysinfo(self, monitor_handler, *args): + # Mock all values that are dependent on the environment to ensure consistency across testing environments. + monitor_handler.init_sysinfo() + + # Replacing OSVersion to make it platform agnostic. We can't mock global constants (eg. DISTRO_NAME, + # DISTRO_VERSION, DISTRO_CODENAME), so to make them constant during the test-time, we need to replace the + # OSVersion field in the event object. + for i in monitor_handler.sysinfo: + if i.name == "OSVersion": + i.value = "{0}:{1}-{2}-{3}:{4}".format(platform.system(), + "DISTRO_NAME", + "DISTRO_VERSION", + "DISTRO_CODE_NAME", + platform.release()) + + @patch("azurelinuxagent.common.conf.get_lib_dir") + def test_collect_and_send_events_should_prepare_all_fields_for_all_event_files(self, mock_lib_dir, *args): + # Test collecting and sending both agent and extension events from the moment they're created to the moment + # they are to be reported. Ensure all necessary fields from sysinfo are present, as well as the container id. + mock_lib_dir.return_value = self.lib_dir + + test_data = WireProtocolData(DATA_FILE) + monitor_handler, protocol = self._create_mock(test_data, *args) + monitor_handler.init_protocols() + self.mock_init_sysinfo(monitor_handler) + + # Add agent event file + self.event_logger.add_event(name=AGENT_NAME, + version=CURRENT_VERSION, + op=WALAEventOperation.HeartBeat, + is_success=True, + message="Heartbeat", + log_event=False) + + # Add extension event file the way extension do it, by dropping a .tld file in the events folder + source_file = os.path.join(data_dir, "ext/dsc_event.json") + dest_file = os.path.join(conf.get_lib_dir(), "events", "dsc_event.tld") + shutil.copyfile(source_file, dest_file) + + # Collect these events and assert they are being sent with the correct sysinfo parameters from the agent + with patch.object(protocol, "report_event") as patch_report_event: + monitor_handler.collect_and_send_events() + + telemetry_events_list = patch_report_event.call_args_list[0][0][0] + self.assertEqual(len(telemetry_events_list.events), 2) + + for event in telemetry_events_list.events: + # All sysinfo parameters coming from the agent have to be present in the telemetry event to be emitted + for param in monitor_handler.sysinfo: + self.assertTrue(param in event.parameters) + + # The container id is a special parameter that is not a part of the static sysinfo parameter list. + # The container id value is obtained from the goal state and must be present in all telemetry events. + container_id_param = TelemetryEventParam("ContainerId", protocol.client.goal_state.container_id) + self.assertTrue(container_id_param in event.parameters) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.send_event") + @patch("azurelinuxagent.common.conf.get_lib_dir") + def test_collect_and_send_events(self, mock_lib_dir, patch_send_event, *args): + mock_lib_dir.return_value = self.lib_dir + + test_data = WireProtocolData(DATA_FILE) + monitor_handler, protocol = self._create_mock(test_data, *args) + monitor_handler.init_protocols() + self.mock_init_sysinfo(monitor_handler) + + self.event_logger.save_event(create_dummy_event(message="Message-Test")) + + monitor_handler.last_event_collection = None + monitor_handler.collect_and_send_events() + + # Validating the crafted message by the collect_and_send_events call. + self.assertEqual(1, patch_send_event.call_count) + send_event_call_args = protocol.client.send_event.call_args[0] + sample_message = '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + ']]>' \ + ''.format(AGENT_VERSION, CURRENT_AGENT) + + self.maxDiff = None + self.assertEqual(sample_message, send_event_call_args[1]) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.send_event") + @patch("azurelinuxagent.common.conf.get_lib_dir") + def test_collect_and_send_events_with_small_events(self, mock_lib_dir, patch_send_event, *args): + mock_lib_dir.return_value = self.lib_dir + + test_data = WireProtocolData(DATA_FILE) + monitor_handler, protocol = self._create_mock(test_data, *args) + monitor_handler.init_protocols() + + sizes = [15, 15, 15, 15] # get the powers of 2 - 2**16 is the limit + + for power in sizes: + size = 2 ** power + self.event_logger.save_event(create_dummy_event(size)) + monitor_handler.collect_and_send_events() + + # The send_event call would be called each time, as we are filling up the buffer up to the brim for each call. + + self.assertEqual(4, patch_send_event.call_count) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.send_event") + @patch("azurelinuxagent.common.conf.get_lib_dir") + def test_collect_and_send_events_with_large_events(self, mock_lib_dir, patch_send_event, *args): + mock_lib_dir.return_value = self.lib_dir + + test_data = WireProtocolData(DATA_FILE) + monitor_handler, protocol = self._create_mock(test_data, *args) + monitor_handler.init_protocols() + + sizes = [17, 17, 17] # get the powers of 2 + + for power in sizes: + size = 2 ** power + self.event_logger.save_event(create_dummy_event(size)) + + with patch("azurelinuxagent.common.logger.periodic_warn") as patch_periodic_warn: + monitor_handler.collect_and_send_events() + self.assertEqual(3, patch_periodic_warn.call_count) + + # The send_event call should never be called as the events are larger than 2**16. + self.assertEqual(0, patch_send_event.call_count) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.send_event") + @patch("azurelinuxagent.common.conf.get_lib_dir") + def test_collect_and_send_events_with_invalid_events(self, mock_lib_dir, patch_send_event, *args): + mock_lib_dir.return_value = self.lib_dir + dummy_events_dir = os.path.join(data_dir, "events", "collect_and_send_events_invalid_data") + fileutil.mkdir(self.event_logger.event_dir) + + test_data = WireProtocolData(DATA_FILE) + monitor_handler, protocol = self._create_mock(test_data, *args) + monitor_handler.init_protocols() + + for filename in os.listdir(dummy_events_dir): + shutil.copy(os.path.join(dummy_events_dir, filename), self.event_logger.event_dir) + + monitor_handler.collect_and_send_events() + + # Invalid events + self.assertEqual(0, patch_send_event.call_count) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.send_event") + @patch("azurelinuxagent.common.conf.get_lib_dir") + def test_collect_and_send_events_cannot_read_events(self, mock_lib_dir, patch_send_event, *args): + mock_lib_dir.return_value = self.lib_dir + dummy_events_dir = os.path.join(data_dir, "events", "collect_and_send_events_unreadable_data") + fileutil.mkdir(self.event_logger.event_dir) + + test_data = WireProtocolData(DATA_FILE) + monitor_handler, protocol = self._create_mock(test_data, *args) + monitor_handler.init_protocols() + + for filename in os.listdir(dummy_events_dir): + shutil.copy(os.path.join(dummy_events_dir, filename), self.event_logger.event_dir) + + def builtins_version(): + if sys.version_info[0] == 2: + return "__builtin__" + else: + return "builtins" + + with patch("{0}.open".format(builtins_version())) as mock_open: + mock_open.side_effect = OSError(13, "Permission denied") + monitor_handler.collect_and_send_events() + + # Invalid events + self.assertEqual(0, patch_send_event.call_count) + + @patch("azurelinuxagent.common.conf.get_lib_dir") + def test_collect_and_send_with_http_post_returning_503(self, mock_lib_dir, *args): + mock_lib_dir.return_value = self.lib_dir + fileutil.mkdir(self.event_logger.event_dir) + + test_data = WireProtocolData(DATA_FILE) + monitor_handler, protocol = self._create_mock(test_data, *args) + monitor_handler.init_protocols() + + sizes = [1, 2, 3] # get the powers of 2, and multiple by 1024. + + for power in sizes: + size = 2 ** power * 1024 + self.event_logger.save_event(create_dummy_event(size)) + + with patch("azurelinuxagent.common.logger.error") as mock_error: + with patch("azurelinuxagent.common.utils.restutil.http_post") as mock_http_post: + mock_http_post.return_value = ResponseMock( + status=restutil.httpclient.SERVICE_UNAVAILABLE, + response="") + monitor_handler.collect_and_send_events() + self.assertEqual(1, mock_error.call_count) + self.assertEqual("[ProtocolError] [Wireserver Exception] [ProtocolError] [Wireserver Failed] " + "URI http://foo.bar/machine?comp=telemetrydata [HTTP Failed] Status Code 503", + mock_error.call_args[0][1]) + self.assertEqual(0, len(os.listdir(self.event_logger.event_dir))) + + @patch("azurelinuxagent.common.conf.get_lib_dir") + def test_collect_and_send_with_send_event_generating_exception(self, mock_lib_dir, *args): + mock_lib_dir.return_value = self.lib_dir + fileutil.mkdir(self.event_logger.event_dir) + + test_data = WireProtocolData(DATA_FILE) + monitor_handler, protocol = self._create_mock(test_data, *args) + monitor_handler.init_protocols() + + sizes = [1, 2, 3] # get the powers of 2, and multiple by 1024. + + for power in sizes: + size = 2 ** power * 1024 + self.event_logger.save_event(create_dummy_event(size)) + + monitor_handler.last_event_collection = datetime.datetime.utcnow() - timedelta(hours=1) + # This test validates that if we hit an issue while sending an event, we never send it again. + with patch("azurelinuxagent.common.logger.warn") as mock_warn: + with patch("azurelinuxagent.common.protocol.wire.WireClient.send_event") as patch_send_event: + patch_send_event.side_effect = Exception() + monitor_handler.collect_and_send_events() + + self.assertEqual(1, mock_warn.call_count) + self.assertEqual(0, len(os.listdir(self.event_logger.event_dir))) + + @patch("azurelinuxagent.common.conf.get_lib_dir") + def test_collect_and_send_with_call_wireserver_returns_http_error(self, mock_lib_dir, *args): + mock_lib_dir.return_value = self.lib_dir + fileutil.mkdir(self.event_logger.event_dir) + + test_data = WireProtocolData(DATA_FILE) + monitor_handler, protocol = self._create_mock(test_data, *args) + monitor_handler.init_protocols() + + sizes = [1, 2, 3] # get the powers of 2, and multiple by 1024. + + for power in sizes: + size = 2 ** power * 1024 + self.event_logger.save_event(create_dummy_event(size)) + + monitor_handler.last_event_collection = datetime.datetime.utcnow() - timedelta(hours=1) + with patch("azurelinuxagent.common.logger.error") as mock_error: + with patch("azurelinuxagent.common.protocol.wire.WireClient.call_wireserver") as patch_call_wireserver: + patch_call_wireserver.side_effect = HttpError + monitor_handler.collect_and_send_events() + + self.assertEqual(1, mock_error.call_count) + self.assertEqual(0, len(os.listdir(self.event_logger.event_dir))) + + +@patch('azurelinuxagent.common.osutil.get_osutil') +@patch('azurelinuxagent.common.protocol.get_protocol_util') +@patch('azurelinuxagent.common.protocol.util.ProtocolUtil.get_protocol') +@patch("azurelinuxagent.common.protocol.healthservice.HealthService._report") +@patch("azurelinuxagent.common.utils.restutil.http_get") +class TestExtensionMetricsDataTelemetry(AgentTestCase): + + def setUp(self): + AgentTestCase.setUp(self) + CGroupsTelemetry.reset() + + @patch('azurelinuxagent.common.event.EventLogger.add_event') + @patch("azurelinuxagent.common.cgroupstelemetry.CGroupsTelemetry.poll_all_tracked") + @patch("azurelinuxagent.common.cgroupstelemetry.CGroupsTelemetry.report_all_tracked") + def test_send_extension_metrics_telemetry(self, patch_report_all_tracked, patch_poll_all_tracked, patch_add_event, + *args): + patch_report_all_tracked.return_value = { + "memory": { + "cur_mem": [1, 1, 1, 1, 1, str(datetime.datetime.utcnow()), str(datetime.datetime.utcnow())], + "max_mem": [1, 1, 1, 1, 1, str(datetime.datetime.utcnow()), str(datetime.datetime.utcnow())] + }, + "cpu": { + "cur_cpu": [1, 1, 1, 1, 1, str(datetime.datetime.utcnow()), str(datetime.datetime.utcnow())] + } + } + + monitor_handler = get_monitor_handler() + monitor_handler.init_protocols() + monitor_handler.last_cgroup_polling_telemetry = datetime.datetime.utcnow() - timedelta(hours=1) + monitor_handler.last_cgroup_report_telemetry = datetime.datetime.utcnow() - timedelta(hours=1) + monitor_handler.poll_telemetry_metrics() + monitor_handler.send_telemetry_metrics() + self.assertEqual(1, patch_poll_all_tracked.call_count) + self.assertEqual(1, patch_report_all_tracked.call_count) + self.assertEqual(1, patch_add_event.call_count) + monitor_handler.stop() + + @patch('azurelinuxagent.common.event.EventLogger.add_event') + @patch("azurelinuxagent.common.cgroupstelemetry.CGroupsTelemetry.poll_all_tracked") + @patch("azurelinuxagent.common.cgroupstelemetry.CGroupsTelemetry.report_all_tracked", return_value={}) + def test_send_extension_metrics_telemetry_for_empty_cgroup(self, patch_report_all_tracked, patch_poll_all_tracked, + patch_add_event, *args): + patch_report_all_tracked.return_value = {} + + monitor_handler = get_monitor_handler() + monitor_handler.init_protocols() + monitor_handler.last_cgroup_polling_telemetry = datetime.datetime.utcnow() - timedelta(hours=1) + monitor_handler.last_cgroup_report_telemetry = datetime.datetime.utcnow() - timedelta(hours=1) + monitor_handler.poll_telemetry_metrics() + monitor_handler.send_telemetry_metrics() + self.assertEqual(1, patch_poll_all_tracked.call_count) + self.assertEqual(1, patch_report_all_tracked.call_count) + self.assertEqual(0, patch_add_event.call_count) + monitor_handler.stop() + + @skip_if_predicate_false(are_cgroups_enabled, "Does not run when Cgroups are not enabled") + @patch('azurelinuxagent.common.event.EventLogger.add_event') + @attr('requires_sudo') + def test_send_extension_metrics_telemetry_with_actual_cgroup(self, patch_add_event, *args): + self.assertTrue(i_am_root(), "Test does not run when non-root") + + num_polls = 5 + name = "test-cgroup" + + cgs = make_new_cgroup(name) + + self.assertEqual(len(cgs), 2) + + for cgroup in cgs: + CGroupsTelemetry.track_cgroup(cgroup) + + for i in range(num_polls): + CGroupsTelemetry.poll_all_tracked() + consume_cpu_time() # Eat some CPU + consume_memory() + + monitor_handler = get_monitor_handler() + monitor_handler.init_protocols() + monitor_handler.last_cgroup_polling_telemetry = datetime.datetime.utcnow() - timedelta(hours=1) + monitor_handler.last_cgroup_report_telemetry = datetime.datetime.utcnow() - timedelta(hours=1) + monitor_handler.poll_telemetry_metrics() + monitor_handler.send_telemetry_metrics() + self.assertEqual(1, patch_add_event.call_count) + + name = patch_add_event.call_args[0][0] + fields = patch_add_event.call_args[1] + + self.assertEqual(name, "WALinuxAgent") + self.assertEqual(fields["op"], "ExtensionMetricsData") + self.assertEqual(fields["is_success"], True) + self.assertEqual(fields["log_event"], False) + self.assertEqual(fields["is_internal"], False) + self.assertIsInstance(fields["message"], ustr) + + monitor_handler.stop() + + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_stat") + def test_generate_extension_metrics_telemetry_dictionary(self, *args): + num_polls = 10 + num_extensions = 1 + num_summarization_values = 7 + + cpu_percent_values = [random.randint(0, 100) for _ in range(num_polls)] + + # only verifying calculations and not validity of the values. + memory_usage_values = [random.randint(0, 8 * 1024 ** 3) for _ in range(num_polls)] + max_memory_usage_values = [random.randint(0, 8 * 1024 ** 3) for _ in range(num_polls)] + + for i in range(num_extensions): + dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) + + dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory", + "dummy_extension_{0}".format(i)) + CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) + + self.assertEqual(2 * num_extensions, len(CGroupsTelemetry._tracked)) + + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") as patch_get_memory_max_usage: + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") as patch_get_memory_usage: + with patch("azurelinuxagent.common.cgroup.CpuCgroup._get_cpu_percent") as patch_get_cpu_percent: + with patch("azurelinuxagent.common.cgroup.CpuCgroup._update_cpu_data") as patch_update_cpu_data: + with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: + for i in range(num_polls): + patch_is_active.return_value = True + patch_get_cpu_percent.return_value = cpu_percent_values[i] + patch_get_memory_usage.return_value = memory_usage_values[i] # example 200 MB + patch_get_memory_max_usage.return_value = max_memory_usage_values[i] # example 450 MB + CGroupsTelemetry.poll_all_tracked() + + performance_metrics = CGroupsTelemetry.report_all_tracked() + + message_json = generate_extension_metrics_telemetry_dictionary(schema_version=1.0, + performance_metrics=performance_metrics) + + for i in range(num_extensions): + self.assertTrue(CGroupsTelemetry.is_tracked("dummy_cpu_path_{0}".format(i))) + self.assertTrue(CGroupsTelemetry.is_tracked("dummy_memory_path_{0}".format(i))) + + self.assertIn("SchemaVersion", message_json) + self.assertIn("PerfMetrics", message_json) + + collected_metrics = message_json["PerfMetrics"] + + for i in range(num_extensions): + extn_name = "dummy_extension_{0}".format(i) + + self.assertIn("memory", collected_metrics[extn_name]) + self.assertIn("cur_mem", collected_metrics[extn_name]["memory"]) + self.assertIn("max_mem", collected_metrics[extn_name]["memory"]) + self.assertEqual(len(collected_metrics[extn_name]["memory"]["cur_mem"]), num_summarization_values) + self.assertEqual(len(collected_metrics[extn_name]["memory"]["max_mem"]), num_summarization_values) + + self.assertIsInstance(collected_metrics[extn_name]["memory"]["cur_mem"][5], str) + self.assertIsInstance(collected_metrics[extn_name]["memory"]["cur_mem"][6], str) + self.assertIsInstance(collected_metrics[extn_name]["memory"]["max_mem"][5], str) + self.assertIsInstance(collected_metrics[extn_name]["memory"]["max_mem"][6], str) + + self.assertIn("cpu", collected_metrics[extn_name]) + self.assertIn("cur_cpu", collected_metrics[extn_name]["cpu"]) + self.assertEqual(len(collected_metrics[extn_name]["cpu"]["cur_cpu"]), num_summarization_values) + + self.assertIsInstance(collected_metrics[extn_name]["cpu"]["cur_cpu"][5], str) + self.assertIsInstance(collected_metrics[extn_name]["cpu"]["cur_cpu"][6], str) + + message_json = generate_extension_metrics_telemetry_dictionary(schema_version=1.0, + performance_metrics=None) + self.assertIn("SchemaVersion", message_json) + self.assertNotIn("PerfMetrics", message_json) + + message_json = generate_extension_metrics_telemetry_dictionary(schema_version=2.0, + performance_metrics=None) + self.assertEqual(message_json, None) + + message_json = generate_extension_metrics_telemetry_dictionary(schema_version="z", + performance_metrics=None) + self.assertEqual(message_json, None) + @patch('azurelinuxagent.common.event.EventLogger.add_event') @patch("azurelinuxagent.common.utils.restutil.http_post") diff -Nru waagent-2.2.34/tests/ga/test_update.py waagent-2.2.45/tests/ga/test_update.py --- waagent-2.2.34/tests/ga/test_update.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/ga/test_update.py 2019-11-07 00:36:56.000000000 +0000 @@ -1104,13 +1104,12 @@ self._test_run_latest(mock_time=mock_time) self.assertEqual(1, mock_time.sleep_interval) - def test_run_latest_polls_moderately_if_installed_not_latest(self): + def test_run_latest_polls_every_second_if_installed_not_latest(self): self.prepare_agents() - mock_child = ChildMock(return_value=0) mock_time = TimeMock(time_increment=CHILD_HEALTH_INTERVAL/2) self._test_run_latest(mock_time=mock_time) - self.assertNotEqual(1, mock_time.sleep_interval) + self.assertEqual(1, mock_time.sleep_interval) def test_run_latest_defaults_to_current(self): self.assertEqual(None, self.update_handler.get_latest_agent()) @@ -1341,15 +1340,6 @@ def test_upgrade_available_returns_true_on_first_use(self): self.assertTrue(self._test_upgrade_available()) - def test_upgrade_available_will_refresh_goal_state(self): - protocol = self._create_protocol() - protocol.emulate_stale_goal_state() - self.assertTrue(self._test_upgrade_available(protocol=protocol)) - self.assertEqual(2, protocol.call_counts["get_vmagent_manifests"]) - self.assertEqual(1, protocol.call_counts["get_vmagent_pkgs"]) - self.assertEqual(1, protocol.call_counts["update_goal_state"]) - self.assertTrue(protocol.goal_state_forced) - def test_upgrade_available_handles_missing_family(self): extensions_config = ExtensionsConfig(load_data("wire/ext_conf_missing_family.xml")) protocol = ProtocolMock() diff -Nru waagent-2.2.34/tests/pa/test_provision.py waagent-2.2.45/tests/pa/test_provision.py --- waagent-2.2.34/tests/pa/test_provision.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/pa/test_provision.py 2019-11-07 00:36:56.000000000 +0000 @@ -19,6 +19,7 @@ from azurelinuxagent.common.osutil.default import DefaultOSUtil from azurelinuxagent.common.protocol import OVF_FILE_NAME from azurelinuxagent.pa.provision import get_provision_handler +from azurelinuxagent.pa.provision.cloudinit import CloudInitProvisionHandler from azurelinuxagent.pa.provision.default import ProvisionHandler from tests.tools import * @@ -113,10 +114,11 @@ self.assertEqual(1, deprovision_handler.run_changed_unique_id.call_count) @distros() + @patch('azurelinuxagent.common.conf.get_provisioning_agent', return_value='waagent') def test_provision_telemetry_pga_false(self, distro_name, distro_version, - distro_full_name): + distro_full_name, _): """ ProvisionGuestAgent flag is 'false' """ @@ -128,10 +130,11 @@ True) @distros() + @patch('azurelinuxagent.common.conf.get_provisioning_agent', return_value='waagent') def test_provision_telemetry_pga_true(self, distro_name, distro_version, - distro_full_name): + distro_full_name, _): """ ProvisionGuestAgent flag is 'true' """ @@ -143,10 +146,11 @@ True) @distros() + @patch('azurelinuxagent.common.conf.get_provisioning_agent', return_value='waagent') def test_provision_telemetry_pga_empty(self, distro_name, distro_version, - distro_full_name): + distro_full_name, _): """ ProvisionGuestAgent flag is '' """ @@ -158,10 +162,11 @@ False) @distros() + @patch('azurelinuxagent.common.conf.get_provisioning_agent', return_value='waagent') def test_provision_telemetry_pga_bad(self, distro_name, distro_version, - distro_full_name): + distro_full_name, _): """ ProvisionGuestAgent flag is 'bad data' """ @@ -238,11 +243,12 @@ @patch( 'azurelinuxagent.common.osutil.default.DefaultOSUtil.get_instance_id', return_value='B9F3C233-9913-9F42-8EB3-BA656DF32502') + @patch('azurelinuxagent.common.conf.get_provisioning_agent', return_value='waagent') def test_provision_telemetry_fail(self, mock_util, distro_name, distro_version, - distro_full_name): + distro_full_name, _): """ Assert that the agent issues one telemetry message as part of a failed provisioning. @@ -311,6 +317,60 @@ ph.handle_provision_guest_agent(provision_guest_agent='TRUE') self.assertEqual(3, patch_write_agent_disabled.call_count) + @patch( + 'azurelinuxagent.common.conf.get_provisioning_agent', + return_value='auto' + ) + @patch( + 'azurelinuxagent.pa.provision.factory.cloud_init_is_enabled', + return_value=False + ) + def test_get_provision_handler_config_auto_no_cloudinit( + self, + patch_cloud_init_is_enabled, + patch_get_provisioning_agent): + provisioning_handler = get_provision_handler() + self.assertIsInstance(provisioning_handler, ProvisionHandler, 'Auto provisioning handler should be waagent if cloud-init is not enabled') + + @patch( + 'azurelinuxagent.common.conf.get_provisioning_agent', + return_value='waagent' + ) + @patch( + 'azurelinuxagent.pa.provision.factory.cloud_init_is_enabled', + return_value=True + ) + def test_get_provision_handler_config_waagent( + self, + patch_cloud_init_is_enabled, + patch_get_provisioning_agent): + provisioning_handler = get_provision_handler() + self.assertIsInstance(provisioning_handler, ProvisionHandler, 'Provisioning handler should be waagent if agent is set to waagent') + + @patch( + 'azurelinuxagent.common.conf.get_provisioning_agent', + return_value='auto' + ) + @patch( + 'azurelinuxagent.pa.provision.factory.cloud_init_is_enabled', + return_value=True + ) + def test_get_provision_handler_config_auto_cloudinit( + self, + patch_cloud_init_is_enabled, + patch_get_provisioning_agent): + provisioning_handler = get_provision_handler() + self.assertIsInstance(provisioning_handler, CloudInitProvisionHandler, 'Auto provisioning handler should be cloud-init if cloud-init is enabled') + + @patch( + 'azurelinuxagent.common.conf.get_provisioning_agent', + return_value='cloud-init' + ) + def test_get_provision_handler_config_cloudinit( + self, + patch_get_provisioning_agent): + provisioning_handler = get_provision_handler() + self.assertIsInstance(provisioning_handler, CloudInitProvisionHandler, 'Provisioning handler should be cloud-init if agent is set to cloud-init') if __name__ == '__main__': unittest.main() diff -Nru waagent-2.2.34/tests/protocol/mockwiredata.py waagent-2.2.45/tests/protocol/mockwiredata.py --- waagent-2.2.34/tests/protocol/mockwiredata.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/protocol/mockwiredata.py 2019-11-07 00:36:56.000000000 +0000 @@ -64,6 +64,14 @@ DATA_FILE_EXT_SINGLE = DATA_FILE.copy() DATA_FILE_EXT_SINGLE["manifest"] = "wire/manifest_deletion.xml" +DATA_FILE_MULTIPLE_EXT = DATA_FILE.copy() +DATA_FILE_MULTIPLE_EXT["ext_conf"] = "wire/ext_conf_multiple_extensions.xml" + +DATA_FILE_NO_CERT_FORMAT = DATA_FILE.copy() +DATA_FILE_NO_CERT_FORMAT["certs"] = "wire/certs_no_format_specified.xml" + +DATA_FILE_CERT_FORMAT_NOT_PFX = DATA_FILE.copy() +DATA_FILE_CERT_FORMAT_NOT_PFX["certs"] = "wire/certs_format_not_pfx.xml" class WireProtocolData(object): def __init__(self, data_files=DATA_FILE): diff -Nru waagent-2.2.34/tests/protocol/test_datacontract.py waagent-2.2.45/tests/protocol/test_datacontract.py --- waagent-2.2.34/tests/protocol/test_datacontract.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/protocol/test_datacontract.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,49 @@ +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# + +import unittest +from azurelinuxagent.common.datacontract import get_properties, set_properties +from azurelinuxagent.common.protocol.restapi import * + + +class SampleDataContract(DataContract): + def __init__(self): + self.foo = None + self.bar = DataContractList(int) + + +class TestDataContract(unittest.TestCase): + def test_get_properties(self): + obj = SampleDataContract() + obj.foo = "foo" + obj.bar.append(1) + data = get_properties(obj) + self.assertEquals("foo", data["foo"]) + self.assertEquals(list, type(data["bar"])) + + def test_set_properties(self): + obj = SampleDataContract() + data = { + 'foo' : 1, + 'baz': 'a' + } + set_properties('sample', obj, data) + self.assertFalse(hasattr(obj, 'baz')) + + +if __name__ == '__main__': + unittest.main() diff -Nru waagent-2.2.34/tests/protocol/test_hostplugin.py waagent-2.2.45/tests/protocol/test_hostplugin.py --- waagent-2.2.34/tests/protocol/test_hostplugin.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/protocol/test_hostplugin.py 2019-11-07 00:36:56.000000000 +0000 @@ -76,7 +76,25 @@ status_blob.vm_status = restapi.VMStatus(message="Ready", status="Ready") return status_blob + def _relax_timestamp(self, headers): + new_headers = [] + + for header in headers: + header_value = header['headerValue'] + if header['headerName'] == 'x-ms-date': + timestamp = header['headerValue'] + header_value = timestamp[:timestamp.rfind(":")] + + new_header = {header['headerName']: header_value} + new_headers.append(new_header) + + return new_headers + def _compare_data(self, actual, expected): + # Remove seconds from the timestamps for testing purposes, that level or granularity introduces test flakiness + actual['headers'] = self._relax_timestamp(actual['headers']) + expected['headers'] = self._relax_timestamp(expected['headers']) + for k in iter(expected.keys()): if k == 'content' or k == 'requestUri': if actual[k] != expected[k]: @@ -660,11 +678,8 @@ # put status blob patch_http_put.return_value = MockResponse(None, 500) - if sys.version_info < (2, 7): - self.assertRaises(HttpError, host_plugin.put_vm_status, status_blob, sas_url) - else: - with self.assertRaises(HttpError): - host_plugin.put_vm_status(status_blob=status_blob, sas_url=sas_url) + with self.assertRaises(HttpError): + host_plugin.put_vm_status(status_blob=status_blob, sas_url=sas_url) self.assertEqual(1, patch_http_get.call_count) self.assertEqual(hostplugin_versions_url, patch_http_get.call_args[0][0]) @@ -703,11 +718,8 @@ host_plugin.status_error_state.is_triggered = Mock(return_value=True) - if sys.version_info < (2, 7): - self.assertRaises(HttpError, host_plugin.put_vm_status, status_blob, sas_url) - else: - with self.assertRaises(HttpError): - host_plugin.put_vm_status(status_blob=status_blob, sas_url=sas_url) + with self.assertRaises(HttpError): + host_plugin.put_vm_status(status_blob=status_blob, sas_url=sas_url) self.assertEqual(1, patch_http_get.call_count) self.assertEqual(hostplugin_versions_url, patch_http_get.call_args[0][0]) diff -Nru waagent-2.2.34/tests/protocol/test_image_info_matcher.py waagent-2.2.45/tests/protocol/test_image_info_matcher.py --- waagent-2.2.34/tests/protocol/test_image_info_matcher.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/protocol/test_image_info_matcher.py 2019-11-07 00:36:56.000000000 +0000 @@ -1,13 +1,23 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. +# Microsoft Azure Linux Agent +# +# Copyright 2018 Microsoft Corporation +# # Licensed under the Apache License, Version 2.0 (the "License"); -import json +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ -from azurelinuxagent.common.exception import HttpError -from azurelinuxagent.common.protocol.imds import ComputeInfo, ImdsClient, IMDS_IMAGE_ORIGIN_CUSTOM, \ - IMDS_IMAGE_ORIGIN_ENDORSED, IMDS_IMAGE_ORIGIN_PLATFORM, ImageInfoMatcher -from azurelinuxagent.common.protocol.restapi import set_properties -from azurelinuxagent.common.utils import restutil -from tests.ga.test_update import ResponseMock +from azurelinuxagent.common.datacontract import set_properties +from azurelinuxagent.common.protocol.imds import ImageInfoMatcher from tests.tools import * diff -Nru waagent-2.2.34/tests/protocol/test_imds.py waagent-2.2.45/tests/protocol/test_imds.py --- waagent-2.2.34/tests/protocol/test_imds.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/protocol/test_imds.py 2019-11-07 00:36:56.000000000 +0000 @@ -1,12 +1,28 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. +# Microsoft Azure Linux Agent +# +# Copyright 2018 Microsoft Corporation +# # Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ + import json import azurelinuxagent.common.protocol.imds as imds -from azurelinuxagent.common.exception import HttpError -from azurelinuxagent.common.future import ustr -from azurelinuxagent.common.protocol.restapi import set_properties +from azurelinuxagent.common.datacontract import set_properties +from azurelinuxagent.common.exception import HttpError, ResourceGoneError +from azurelinuxagent.common.future import ustr, httpclient from azurelinuxagent.common.utils import restutil from tests.ga.test_update import ResponseMock from tests.tools import * @@ -52,6 +68,13 @@ self.assertRaises(HttpError, test_subject.get_compute) @patch("azurelinuxagent.ga.update.restutil.http_get") + def test_get_internal_service_error(self, mock_http_get): + mock_http_get.return_value = ResponseMock(status=restutil.httpclient.INTERNAL_SERVER_ERROR) + + test_subject = imds.ImdsClient() + self.assertRaises(HttpError, test_subject.get_compute) + + @patch("azurelinuxagent.ga.update.restutil.http_get") def test_get_empty_response(self, mock_http_get): mock_http_get.return_value = ResponseMock(response=''.encode('utf-8')) @@ -255,14 +278,20 @@ self._assert_validation(http_status_code=500, http_response='error response', expected_valid=False, - expected_response='[HTTP Failed] [500: reason] error response') + expected_response='IMDS error in /metadata/instance: [HTTP Failed] [500: reason] error response') - # 429 response + # 429 response - throttling does not mean service is unhealthy self._assert_validation(http_status_code=429, http_response='server busy', - expected_valid=False, + expected_valid=True, expected_response='[HTTP Failed] [429: reason] server busy') + # 404 response - error responses do not mean service is unhealthy + self._assert_validation(http_status_code=404, + http_response='not found', + expected_valid=True, + expected_response='[HTTP Failed] [404: reason] not found') + # valid json self._assert_validation(http_status_code=200, http_response=self._imds_response('valid'), @@ -337,13 +366,125 @@ self.assertEqual(restutil.HTTP_USER_AGENT_HEALTH, kw_args['headers']['User-Agent']) self.assertTrue('Metadata' in kw_args['headers']) self.assertEqual(True, kw_args['headers']['Metadata']) - self.assertEqual('http://169.254.169.254/metadata/instance/?api-version=2018-02-01', + self.assertEqual('http://169.254.169.254/metadata/instance?api-version=2018-02-01', positional_args[0]) self.assertEqual(expected_valid, validate_response[0]) self.assertTrue(expected_response in validate_response[1], "Expected: '{0}', Actual: '{1}'" .format(expected_response, validate_response[1])) + @patch("azurelinuxagent.common.protocol.util.ProtocolUtil") + def test_endpoint_fallback(self, ProtocolUtil): + # http error status codes are tested in test_response_validation, none of which + # should trigger a fallback. This is confirmed as _assert_validation will count + # http GET calls and enforces a single GET call (fallback would cause 2) and + # checks the url called. + + test_subject = imds.ImdsClient() + ProtocolUtil().get_wireserver_endpoint.return_value = "foo.bar" + + # ensure user-agent gets set correctly + for is_health, expected_useragent in [(False, restutil.HTTP_USER_AGENT), (True, restutil.HTTP_USER_AGENT_HEALTH)]: + # set a different resource path for health query to make debugging unit test easier + resource_path = 'something/health' if is_health else 'something' + + for has_primary_ioerror in (False, True): + # secondary endpoint unreachable + test_subject._http_get = Mock(side_effect=self._mock_http_get) + self._mock_imds_setup(primary_ioerror=has_primary_ioerror, secondary_ioerror=True) + result = test_subject.get_metadata(resource_path=resource_path, is_health=is_health) + self.assertFalse(result.success) if has_primary_ioerror else self.assertTrue(result.success) + self.assertFalse(result.service_error) + if has_primary_ioerror: + self.assertEqual('IMDS error in /metadata/{0}: Unable to connect to endpoint'.format(resource_path), result.response) + else: + self.assertEqual('Mock success response', result.response) + for _, kwargs in test_subject._http_get.call_args_list: + self.assertTrue('User-Agent' in kwargs['headers']) + self.assertEqual(expected_useragent, kwargs['headers']['User-Agent']) + self.assertEqual(2 if has_primary_ioerror else 1, test_subject._http_get.call_count) + + # IMDS success + test_subject._http_get = Mock(side_effect=self._mock_http_get) + self._mock_imds_setup(primary_ioerror=has_primary_ioerror) + result = test_subject.get_metadata(resource_path=resource_path, is_health=is_health) + self.assertTrue(result.success) + self.assertFalse(result.service_error) + self.assertEqual('Mock success response', result.response) + for _, kwargs in test_subject._http_get.call_args_list: + self.assertTrue('User-Agent' in kwargs['headers']) + self.assertEqual(expected_useragent, kwargs['headers']['User-Agent']) + self.assertEqual(2 if has_primary_ioerror else 1, test_subject._http_get.call_count) + + # IMDS throttled + test_subject._http_get = Mock(side_effect=self._mock_http_get) + self._mock_imds_setup(primary_ioerror=has_primary_ioerror, throttled=True) + result = test_subject.get_metadata(resource_path=resource_path, is_health=is_health) + self.assertFalse(result.success) + self.assertFalse(result.service_error) + self.assertEqual('IMDS error in /metadata/{0}: Throttled'.format(resource_path), result.response) + for _, kwargs in test_subject._http_get.call_args_list: + self.assertTrue('User-Agent' in kwargs['headers']) + self.assertEqual(expected_useragent, kwargs['headers']['User-Agent']) + self.assertEqual(2 if has_primary_ioerror else 1, test_subject._http_get.call_count) + + # IMDS gone error + test_subject._http_get = Mock(side_effect=self._mock_http_get) + self._mock_imds_setup(primary_ioerror=has_primary_ioerror, gone_error=True) + result = test_subject.get_metadata(resource_path=resource_path, is_health=is_health) + self.assertFalse(result.success) + self.assertTrue(result.service_error) + self.assertEqual('IMDS error in /metadata/{0}: HTTP Failed with Status Code 410: Gone'.format(resource_path), result.response) + for _, kwargs in test_subject._http_get.call_args_list: + self.assertTrue('User-Agent' in kwargs['headers']) + self.assertEqual(expected_useragent, kwargs['headers']['User-Agent']) + self.assertEqual(2 if has_primary_ioerror else 1, test_subject._http_get.call_count) + + # IMDS bad request + test_subject._http_get = Mock(side_effect=self._mock_http_get) + self._mock_imds_setup(primary_ioerror=has_primary_ioerror, bad_request=True) + result = test_subject.get_metadata(resource_path=resource_path, is_health=is_health) + self.assertFalse(result.success) + self.assertFalse(result.service_error) + self.assertEqual('IMDS error in /metadata/{0}: [HTTP Failed] [404: reason] Mock not found'.format(resource_path), result.response) + for _, kwargs in test_subject._http_get.call_args_list: + self.assertTrue('User-Agent' in kwargs['headers']) + self.assertEqual(expected_useragent, kwargs['headers']['User-Agent']) + self.assertEqual(2 if has_primary_ioerror else 1, test_subject._http_get.call_count) + + def _mock_imds_setup(self, primary_ioerror=False, secondary_ioerror=False, gone_error=False, throttled=False, bad_request=False): + self._mock_imds_expect_fallback = primary_ioerror + self._mock_imds_primary_ioerror = primary_ioerror + self._mock_imds_secondary_ioerror = secondary_ioerror + self._mock_imds_gone_error = gone_error + self._mock_imds_throttled = throttled + self._mock_imds_bad_request = bad_request + + def _mock_http_get(self, *_, **kwargs): + if "foo.bar" == kwargs['endpoint'] and not self._mock_imds_expect_fallback: + raise Exception("Unexpected endpoint called") + if self._mock_imds_primary_ioerror and "169.254.169.254" == kwargs['endpoint']: + raise HttpError("[HTTP Failed] GET http://{0}/metadata/{1} -- IOError timed out -- 6 attempts made" + .format(kwargs['endpoint'], kwargs['resource_path'])) + if self._mock_imds_secondary_ioerror and "foo.bar" == kwargs['endpoint']: + raise HttpError("[HTTP Failed] GET http://{0}/metadata/{1} -- IOError timed out -- 6 attempts made" + .format(kwargs['endpoint'], kwargs['resource_path'])) + if self._mock_imds_gone_error: + raise ResourceGoneError("Resource is gone") + if self._mock_imds_throttled: + raise HttpError("[HTTP Retry] GET http://{0}/metadata/{1} -- Status Code 429 -- 25 attempts made" + .format(kwargs['endpoint'], kwargs['resource_path'])) + + resp = MagicMock() + resp.reason = 'reason' + if self._mock_imds_bad_request: + resp.status = httpclient.NOT_FOUND + resp.read.return_value = 'Mock not found' + else: + resp.status = httpclient.OK + resp.read.return_value = 'Mock success response' + return resp + if __name__ == '__main__': unittest.main() diff -Nru waagent-2.2.34/tests/protocol/test_metadata.py waagent-2.2.45/tests/protocol/test_metadata.py --- waagent-2.2.34/tests/protocol/test_metadata.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/protocol/test_metadata.py 2019-11-07 00:36:56.000000000 +0000 @@ -15,17 +15,17 @@ # Requires Python 2.6+ and Openssl 1.0+ # -import json - -from azurelinuxagent.common.future import ustr - -from azurelinuxagent.common.utils.restutil import httpclient +from azurelinuxagent.common.datacontract import get_properties, set_properties +from azurelinuxagent.common.exception import ProtocolError from azurelinuxagent.common.protocol.metadata import * from azurelinuxagent.common.protocol.restapi import * +from azurelinuxagent.common.telemetryevent import TelemetryEventList, TelemetryEvent +from azurelinuxagent.common.utils import restutil from tests.protocol.mockmetadata import * from tests.tools import * + class TestMetadataProtocolGetters(AgentTestCase): def load_json(self, path): return json.loads(ustr(load_data(path)), encoding="utf-8") @@ -49,7 +49,6 @@ test_data = MetadataProtocolData(DATA_FILE_NO_EXT) self._test_getters(test_data, *args) - @patch("azurelinuxagent.common.protocol.metadata.MetadataProtocol.update_goal_state") @patch("azurelinuxagent.common.protocol.metadata.MetadataProtocol._get_data") def test_get_vmagents_manifests(self, mock_get, mock_update): diff -Nru waagent-2.2.34/tests/protocol/test_restapi.py waagent-2.2.45/tests/protocol/test_restapi.py --- waagent-2.2.34/tests/protocol/test_restapi.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/protocol/test_restapi.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,50 +0,0 @@ -# Copyright 2018 Microsoft Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Requires Python 2.6+ and Openssl 1.0+ -# - -from tests.tools import * -import uuid -import unittest -import os -import shutil -import time -from azurelinuxagent.common.protocol.restapi import * - -class SampleDataContract(DataContract): - def __init__(self): - self.foo = None - self.bar = DataContractList(int) - -class TestDataContract(unittest.TestCase): - def test_get_properties(self): - obj = SampleDataContract() - obj.foo = "foo" - obj.bar.append(1) - data = get_properties(obj) - self.assertEquals("foo", data["foo"]) - self.assertEquals(list, type(data["bar"])) - - def test_set_properties(self): - obj = SampleDataContract() - data = { - 'foo' : 1, - 'baz': 'a' - } - set_properties('sample', obj, data) - self.assertFalse(hasattr(obj, 'baz')) - -if __name__ == '__main__': - unittest.main() diff -Nru waagent-2.2.34/tests/protocol/test_wire.py waagent-2.2.45/tests/protocol/test_wire.py --- waagent-2.2.34/tests/protocol/test_wire.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/protocol/test_wire.py 2019-11-07 00:36:56.000000000 +0000 @@ -1,3 +1,4 @@ +# -*- encoding: utf-8 -*- # Copyright 2018 Microsoft Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +16,12 @@ # Requires Python 2.6+ and Openssl 1.0+ # -import glob -import stat import zipfile -from azurelinuxagent.common import event +from azurelinuxagent.common.telemetryevent import TelemetryEvent, TelemetryEventParam from azurelinuxagent.common.protocol.wire import * from azurelinuxagent.common.utils.shellutil import run_get_output -from tests.common.osutil.test_default import running_under_travis +from tests.ga.test_monitor import random_generator from tests.protocol.mockwiredata import * data_with_bom = b'\xef\xbb\xbfhehe' @@ -30,6 +29,21 @@ testtype = 'BlockBlob' wireserver_url = '168.63.129.16' + +def get_event(message, duration=30000, evt_type="", is_internal=False, is_success=True, + name="", op="Unknown", version=CURRENT_VERSION, eventId=1): + event = TelemetryEvent(eventId, "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") + event.parameters.append(TelemetryEventParam('Name', name)) + event.parameters.append(TelemetryEventParam('Version', str(version))) + event.parameters.append(TelemetryEventParam('IsInternal', is_internal)) + event.parameters.append(TelemetryEventParam('Operation', op)) + event.parameters.append(TelemetryEventParam('OperationSuccess', is_success)) + event.parameters.append(TelemetryEventParam('Message', message)) + event.parameters.append(TelemetryEventParam('Duration', duration)) + event.parameters.append(TelemetryEventParam('ExtensionType', evt_type)) + return event + + @patch("time.sleep") @patch("azurelinuxagent.common.protocol.wire.CryptUtil") @patch("azurelinuxagent.common.protocol.healthservice.HealthService._report") @@ -38,8 +52,8 @@ def setUp(self): super(TestWireProtocol, self).setUp() HostPluginProtocol.set_default_channel(False) - - def _test_getters(self, test_data, __, MockCryptUtil, _): + + def _test_getters(self, test_data, certsMustBePresent, __, MockCryptUtil, _): MockCryptUtil.side_effect = test_data.mock_crypt_util with patch.object(restutil, 'http_get', test_data.mock_http_get): @@ -57,39 +71,52 @@ '4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3.crt') prv2 = os.path.join(self.tmp_dir, '4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3.prv') - - self.assertTrue(os.path.isfile(crt1)) - self.assertTrue(os.path.isfile(crt2)) - self.assertTrue(os.path.isfile(prv2)) - + if certsMustBePresent: + self.assertTrue(os.path.isfile(crt1)) + self.assertTrue(os.path.isfile(crt2)) + self.assertTrue(os.path.isfile(prv2)) + else: + self.assertFalse(os.path.isfile(crt1)) + self.assertFalse(os.path.isfile(crt2)) + self.assertFalse(os.path.isfile(prv2)) self.assertEqual("1", protocol.get_incarnation()) def test_getters(self, *args): """Normal case""" test_data = WireProtocolData(DATA_FILE) - self._test_getters(test_data, *args) + self._test_getters(test_data, True, *args) def test_getters_no_ext(self, *args): """Provision with agent is not checked""" test_data = WireProtocolData(DATA_FILE_NO_EXT) - self._test_getters(test_data, *args) + self._test_getters(test_data, True, *args) def test_getters_ext_no_settings(self, *args): """Extensions without any settings""" test_data = WireProtocolData(DATA_FILE_EXT_NO_SETTINGS) - self._test_getters(test_data, *args) + self._test_getters(test_data, True, *args) def test_getters_ext_no_public(self, *args): """Extensions without any public settings""" test_data = WireProtocolData(DATA_FILE_EXT_NO_PUBLIC) - self._test_getters(test_data, *args) + self._test_getters(test_data, True, *args) + + def test_getters_ext_no_cert_format(self, *args): + """Certificate format not specified""" + test_data = WireProtocolData(DATA_FILE_NO_CERT_FORMAT) + self._test_getters(test_data, True, *args) + + def test_getters_ext_cert_format_not_pfx(self, *args): + """Certificate format is not Pkcs7BlobWithPfxContents specified""" + test_data = WireProtocolData(DATA_FILE_CERT_FORMAT_NOT_PFX) + self._test_getters(test_data, False, *args) @patch("azurelinuxagent.common.protocol.healthservice.HealthService.report_host_plugin_extension_artifact") def test_getters_with_stale_goal_state(self, patch_report, *args): test_data = WireProtocolData(DATA_FILE) test_data.emulate_stale_goal_state = True - self._test_getters(test_data, *args) + self._test_getters(test_data, True, *args) # Ensure HostPlugin was invoked self.assertEqual(1, test_data.call_counts["/versions"]) self.assertEqual(2, test_data.call_counts["extensionArtifact"]) @@ -134,7 +161,7 @@ use_proxy=True) # assert self.assertTrue(http_patch.call_count == 5) - for i in range(0,5): + for i in range(0, 5): c = http_patch.call_args_list[i][-1]['use_proxy'] self.assertTrue(c == (True if i != 3 else False)) @@ -154,7 +181,7 @@ wire_protocol_client = WireProtocol(wireserver_url).client goal_state = GoalState(WireProtocolData(DATA_FILE).goal_state) - with patch.object(WireClient, "get_goal_state", return_value = goal_state) as patch_get_goal_state: + with patch.object(WireClient, "get_goal_state", return_value=goal_state) as patch_get_goal_state: host_plugin = wire_protocol_client.get_host_plugin() self.assertEqual(goal_state.container_id, host_plugin.container_id) self.assertEqual(goal_state.role_config_name, host_plugin.role_config_name) @@ -188,7 +215,7 @@ self.assertTrue(os.path.exists(destination)) # verify size - self.assertEqual(18380915, os.stat(destination).st_size) + self.assertEqual(33193077, os.stat(destination).st_size) # verify unzip zipfile.ZipFile(destination).extractall(tmp) @@ -197,14 +224,13 @@ fileutil.chmod(packer, os.stat(packer).st_mode | stat.S_IXUSR) # verify unpacked size - self.assertEqual(87393596, os.stat(packer).st_size) + self.assertEqual(105552030, os.stat(packer).st_size) # execute, verify result packer_version = '{0} --version'.format(packer) rc, stdout = run_get_output(packer_version) self.assertEqual(0, rc) - self.assertEqual('1.2.5\n', stdout) - + self.assertEqual('1.3.5\n', stdout) @patch("azurelinuxagent.common.protocol.wire.WireClient.update_goal_state") def test_upload_status_blob_default(self, *args): @@ -298,7 +324,7 @@ # Test when artifacts_profile_blob is null/None self.assertEqual(None, wire_protocol_client.get_artifacts_profile()) - #Test when artifacts_profile_blob is whitespace + # Test when artifacts_profile_blob is whitespace wire_protocol_client.ext_conf.artifacts_profile_blob = " " self.assertEqual(None, wire_protocol_client.get_artifacts_profile()) @@ -310,18 +336,18 @@ wire_protocol_client.get_goal_state = Mock(return_value=goal_state) with patch.object(HostPluginProtocol, "get_artifact_request", - return_value = ['dummy_url', {}]) as host_plugin_get_artifact_url_and_headers: - #Test when response body is None + return_value=['dummy_url', {}]) as host_plugin_get_artifact_url_and_headers: + # Test when response body is None wire_protocol_client.call_storage_service = Mock(return_value=MockResponse(None, 200)) in_vm_artifacts_profile = wire_protocol_client.get_artifacts_profile() self.assertTrue(in_vm_artifacts_profile is None) - #Test when response body is None + # Test when response body is None wire_protocol_client.call_storage_service = Mock(return_value=MockResponse(' '.encode('utf-8'), 200)) in_vm_artifacts_profile = wire_protocol_client.get_artifacts_profile() self.assertTrue(in_vm_artifacts_profile is None) - #Test when response body is None + # Test when response body is None wire_protocol_client.call_storage_service = Mock(return_value=MockResponse('{ }'.encode('utf-8'), 200)) in_vm_artifacts_profile = wire_protocol_client.get_artifacts_profile() self.assertEqual(dict(), in_vm_artifacts_profile.__dict__, @@ -358,7 +384,8 @@ goal_state = GoalState(WireProtocolData(DATA_FILE).goal_state) wire_protocol_client.get_goal_state = Mock(return_value=goal_state) - wire_protocol_client.call_storage_service = Mock(return_value=MockResponse('{"onHold": "true"}'.encode('utf-8'), 200)) + wire_protocol_client.call_storage_service = Mock( + return_value=MockResponse('{"onHold": "true"}'.encode('utf-8'), 200)) in_vm_artifacts_profile = wire_protocol_client.get_artifacts_profile() self.assertEqual(dict(onHold='true'), in_vm_artifacts_profile.__dict__) self.assertTrue(in_vm_artifacts_profile.is_on_hold()) @@ -373,21 +400,48 @@ 'container_id', 'role_config') client = WireProtocol(wireserver_url).client - with patch.object(WireClient, - "fetch", - return_value=None) as patch_fetch: - with patch.object(WireClient, - "get_host_plugin", - return_value=mock_host): - with patch.object(HostPluginProtocol, - "get_artifact_request", - return_value=[host_uri, {}]): + + with patch.object(WireClient, "fetch", return_value=None) as patch_fetch: + with patch.object(WireClient, "get_host_plugin", return_value=mock_host): + with patch.object(HostPluginProtocol, "get_artifact_request", return_value=[host_uri, {}]): HostPluginProtocol.set_default_channel(False) - self.assertRaises(ProtocolError, client.fetch_manifest, uris) + self.assertRaises(ExtensionDownloadError, client.fetch_manifest, uris) self.assertEqual(patch_fetch.call_count, 2) self.assertEqual(patch_fetch.call_args_list[0][0][0], uri1.uri) self.assertEqual(patch_fetch.call_args_list[1][0][0], host_uri) + # This test checks if the manifest_uri variable is set in the host object of WireClient + # This variable is used when we make /extensionArtifact API calls to the HostGA + def test_fetch_manifest_ensure_manifest_uri_is_set(self, *args): + uri1 = ExtHandlerVersionUri() + uri1.uri = 'ext_uri' + uris = DataContractList(ExtHandlerVersionUri) + uris.append(uri1) + host_uri = 'host_uri' + mock_host = HostPluginProtocol(host_uri, 'container_id', 'role_config') + client = WireProtocol(wireserver_url).client + manifest_return = "manifest.xml" + + with patch.object(WireClient, "get_host_plugin", return_value=mock_host): + mock_host.get_artifact_request = MagicMock(return_value=[host_uri, {}]) + + # First test tried to download directly from blob and asserts manifest_uri is set + with patch.object(WireClient, "fetch", return_value=manifest_return) as patch_fetch: + fetch_manifest_mock = client.fetch_manifest(uris) + self.assertEqual(fetch_manifest_mock, manifest_return) + self.assertEqual(patch_fetch.call_count, 1) + self.assertEqual(mock_host.manifest_uri, uri1.uri) + + # Second test tries to download from the HostGA (by failing the direct download) + # and asserts manifest_uri is set + with patch.object(WireClient, "fetch") as patch_fetch: + patch_fetch.side_effect = [None, manifest_return] + fetch_manifest_mock = client.fetch_manifest(uris) + self.assertEqual(fetch_manifest_mock, manifest_return) + self.assertEqual(patch_fetch.call_count, 2) + self.assertEqual(mock_host.manifest_uri, uri1.uri) + self.assertTrue(HostPluginProtocol.is_default_channel()) + def test_get_in_vm_artifacts_profile_host_ga_plugin(self, *args): wire_protocol_client = WireProtocol(wireserver_url).client wire_protocol_client.ext_conf = ExtensionsConfig(None) @@ -438,17 +492,596 @@ 'version': '1.1', 'timestampUTC': timestamp, 'aggregateStatus': v1_agg_status, - 'guestOSInfo' : v1_ga_guest_info + 'guestOSInfo': v1_ga_guest_info } self.assertEqual(json.dumps(v1_vm_status), actual.to_json()) + @patch("azurelinuxagent.common.utils.restutil.http_request") + def test_send_event(self, mock_http_request, *args): + mock_http_request.return_value = MockResponse("", 200) + + event_str = u'a test string' + client = WireProtocol(wireserver_url).client + client.send_event("foo", event_str) + + first_call = mock_http_request.call_args_list[0] + args, kwargs = first_call + method, url, body_received = args + headers = kwargs['headers'] + + # the headers should include utf-8 encoding... + self.assertTrue("utf-8" in headers['Content-Type']) + # the body is not encoded, just check for equality + self.assertIn(event_str, body_received) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.send_event") + def test_report_event_small_event(self, patch_send_event, *args): + event_list = TelemetryEventList() + client = WireProtocol(wireserver_url).client + + event_str = random_generator(10) + event_list.events.append(get_event(message=event_str)) + + event_str = random_generator(100) + event_list.events.append(get_event(message=event_str)) + + event_str = random_generator(1000) + event_list.events.append(get_event(message=event_str)) + + event_str = random_generator(10000) + event_list.events.append(get_event(message=event_str)) + + client.report_event(event_list) + + # It merges the messages into one message + self.assertEqual(patch_send_event.call_count, 1) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.send_event") + def test_report_event_multiple_events_to_fill_buffer(self, patch_send_event, *args): + event_list = TelemetryEventList() + client = WireProtocol(wireserver_url).client + + event_str = random_generator(2 ** 15) + event_list.events.append(get_event(message=event_str)) + event_list.events.append(get_event(message=event_str)) + + client.report_event(event_list) + + # It merges the messages into one message + self.assertEqual(patch_send_event.call_count, 2) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.send_event") + def test_report_event_large_event(self, patch_send_event, *args): + event_list = TelemetryEventList() + event_str = random_generator(2 ** 18) + event_list.events.append(get_event(message=event_str)) + client = WireProtocol(wireserver_url).client + client.report_event(event_list) + + self.assertEqual(patch_send_event.call_count, 0) + + +class TestWireClient(AgentTestCase): + + def test_save_or_update_goal_state_should_save_new_goal_state_file(self): + # Assert the file didn't exist before + incarnation = 42 + goal_state_file = os.path.join(conf.get_lib_dir(), "GoalState.{0}.xml".format(incarnation)) + self.assertFalse(os.path.exists(goal_state_file)) + + xml_text = WireProtocolData(DATA_FILE).goal_state + client = WireClient(wireserver_url) + client.save_or_update_goal_state_file(incarnation, xml_text) + + # Assert the file exists and its contents + self.assertTrue(os.path.exists(goal_state_file)) + with open(goal_state_file, "r") as f: + contents = f.readlines() + self.assertEquals("".join(contents), xml_text) + + def test_save_or_update_goal_state_should_update_existing_goal_state_file(self): + incarnation = 42 + goal_state_file = os.path.join(conf.get_lib_dir(), "GoalState.{0}.xml".format(incarnation)) + xml_text = WireProtocolData(DATA_FILE).goal_state + + with open(goal_state_file, "w") as f: + f.write(xml_text) + + # Assert the file exists and its contents + self.assertTrue(os.path.exists(goal_state_file)) + with open(goal_state_file, "r") as f: + contents = f.readlines() + self.assertEquals("".join(contents), xml_text) + + # Update the container id + new_goal_state = WireProtocolData(DATA_FILE).goal_state.replace("c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2", + "z6d5526c-5ac2-4200-b6e2-56f2b70c5ab2") + client = WireClient(wireserver_url) + client.save_or_update_goal_state_file(incarnation, new_goal_state) + + # Assert the file exists and its contents + self.assertTrue(os.path.exists(goal_state_file)) + with open(goal_state_file, "r") as f: + contents = f.readlines() + self.assertEquals("".join(contents), new_goal_state) + + def test_save_or_update_goal_state_should_update_goal_state_and_container_id_when_not_forced(self): + incarnation = "1" # Match the incarnation number from dummy goal state file + incarnation_file = os.path.join(conf.get_lib_dir(), INCARNATION_FILE_NAME) + with open(incarnation_file, "w") as f: + f.write(incarnation) + + xml_text = WireProtocolData(DATA_FILE).goal_state + goal_state_file = os.path.join(conf.get_lib_dir(), "GoalState.{0}.xml".format(incarnation)) + + with open(goal_state_file, "w") as f: + f.write(xml_text) + + client = WireClient(wireserver_url) + host = client.get_host_plugin() + old_container_id = host.container_id + + # Update the container id + new_goal_state = WireProtocolData(DATA_FILE).goal_state.replace("c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2", + "z6d5526c-5ac2-4200-b6e2-56f2b70c5ab2") + with patch("azurelinuxagent.common.protocol.wire.WireClient.fetch_config", return_value=new_goal_state): + client.update_goal_state(forced=False) + + self.assertNotEqual(old_container_id, host.container_id) + self.assertEquals(host.container_id, "z6d5526c-5ac2-4200-b6e2-56f2b70c5ab2") + + @patch("azurelinuxagent.common.protocol.wire.WireClient.get_goal_state") + @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.get_artifact_request") + def test_download_ext_handler_pkg_should_not_invoke_host_channel_when_direct_channel_succeeds(self, mock_get_artifact_request, *args): + mock_get_artifact_request.return_value = "dummy_url", "dummy_header" + protocol = WireProtocol("foo.bar") + HostPluginProtocol.set_default_channel(False) + + mock_successful_response = MockResponse(body=b"OK", status_code=200) + destination = os.path.join(self.tmp_dir, "tmp_file") + + # Direct channel succeeds + with patch("azurelinuxagent.common.utils.restutil._http_request", return_value=mock_successful_response): + with patch("azurelinuxagent.common.protocol.wire.WireClient.update_goal_state") as mock_update_goal_state: + with patch("azurelinuxagent.common.protocol.wire.WireClient.stream", wraps=protocol.client.stream) \ + as patch_direct: + with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg_through_host", + wraps=protocol.download_ext_handler_pkg_through_host) as patch_host: + ret = protocol.download_ext_handler_pkg("uri", destination) + self.assertEquals(ret, True) + + self.assertEquals(patch_host.call_count, 0) + self.assertEquals(patch_direct.call_count, 1) + self.assertEquals(mock_update_goal_state.call_count, 0) + + self.assertEquals(HostPluginProtocol.is_default_channel(), False) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.get_goal_state") + @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.get_artifact_request") + def test_download_ext_handler_pkg_should_use_host_channel_when_direct_channel_fails(self, mock_get_artifact_request, *args): + mock_get_artifact_request.return_value = "dummy_url", "dummy_header" + protocol = WireProtocol("foo.bar") + HostPluginProtocol.set_default_channel(False) + + mock_failed_response = MockResponse(body=b"", status_code=httpclient.GONE) + mock_successful_response = MockResponse(body=b"OK", status_code=200) + destination = os.path.join(self.tmp_dir, "tmp_file") + + # Direct channel fails, host channel succeeds. Goal state should not have been updated and host channel + # should have been set as default. + with patch("azurelinuxagent.common.utils.restutil._http_request", + side_effect=[mock_failed_response, mock_successful_response]): + with patch("azurelinuxagent.common.protocol.wire.WireClient.update_goal_state") as mock_update_goal_state: + with patch("azurelinuxagent.common.protocol.wire.WireClient.stream", wraps=protocol.client.stream) \ + as patch_direct: + with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg_through_host", + wraps=protocol.download_ext_handler_pkg_through_host) as patch_host: + ret = protocol.download_ext_handler_pkg("uri", destination) + self.assertEquals(ret, True) + + self.assertEquals(patch_host.call_count, 1) + # The host channel calls the direct function under the covers + self.assertEquals(patch_direct.call_count, 1 + patch_host.call_count) + self.assertEquals(mock_update_goal_state.call_count, 0) + + self.assertEquals(HostPluginProtocol.is_default_channel(), True) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.get_goal_state") + @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.get_artifact_request") + def test_download_ext_handler_pkg_should_retry_the_host_channel_after_reloading_goal_state(self, mock_get_artifact_request, *args): + mock_get_artifact_request.return_value = "dummy_url", "dummy_header" + protocol = WireProtocol("foo.bar") + HostPluginProtocol.set_default_channel(False) + + mock_failed_response = MockResponse(body=b"", status_code=httpclient.GONE) + mock_successful_response = MockResponse(body=b"OK", status_code=200) + destination = os.path.join(self.tmp_dir, "tmp_file") + + # Direct channel fails, host channel fails due to stale goal state, host channel succeeds after refresh. + # As a consequence, goal state should have been updated and host channel should have been set as default. + with patch("azurelinuxagent.common.utils.restutil._http_request", + side_effect=[mock_failed_response, mock_failed_response, mock_successful_response]): + with patch("azurelinuxagent.common.protocol.wire.WireClient.update_goal_state") as mock_update_goal_state: + with patch("azurelinuxagent.common.protocol.wire.WireClient.stream", wraps=protocol.client.stream) \ + as patch_direct: + with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg_through_host", + wraps=protocol.download_ext_handler_pkg_through_host) as patch_host: + ret = protocol.download_ext_handler_pkg("uri", destination) + self.assertEquals(ret, True) + + self.assertEquals(patch_host.call_count, 2) + # The host channel calls the direct function under the covers + self.assertEquals(patch_direct.call_count, 1 + patch_host.call_count) + self.assertEquals(mock_update_goal_state.call_count, 1) + + self.assertEquals(HostPluginProtocol.is_default_channel(), True) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.get_goal_state") + @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.get_artifact_request") + def test_download_ext_handler_pkg_should_update_goal_state_and_not_change_default_channel_if_host_fails(self, mock_get_artifact_request, *args): + mock_get_artifact_request.return_value = "dummy_url", "dummy_header" + protocol = WireProtocol("foo.bar") + HostPluginProtocol.set_default_channel(False) + + mock_failed_response = MockResponse(body=b"", status_code=httpclient.GONE) + destination = os.path.join(self.tmp_dir, "tmp_file") + + # Everything fails. Goal state should have been updated and host channel should not have been set as default. + with patch("azurelinuxagent.common.utils.restutil._http_request", return_value=mock_failed_response): + with patch("azurelinuxagent.common.protocol.wire.WireClient.update_goal_state") as mock_update_goal_state: + with patch("azurelinuxagent.common.protocol.wire.WireClient.stream", wraps=protocol.client.stream) \ + as patch_direct: + with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg_through_host", + wraps=protocol.download_ext_handler_pkg_through_host) as patch_host: + ret = protocol.download_ext_handler_pkg("uri", destination) + self.assertEquals(ret, False) + + self.assertEquals(patch_host.call_count, 2) + # The host channel calls the direct function under the covers + self.assertEquals(patch_direct.call_count, 1 + patch_host.call_count) + self.assertEquals(mock_update_goal_state.call_count, 1) + + self.assertEquals(HostPluginProtocol.is_default_channel(), False) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.get_goal_state") + @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.get_artifact_request") + def test_fetch_manifest_should_not_invoke_host_channel_when_direct_channel_succeeds(self, mock_get_artifact_request, *args): + mock_get_artifact_request.return_value = "dummy_url", "dummy_header" + client = WireClient("foo.bar") + + HostPluginProtocol.set_default_channel(False) + mock_successful_response = MockResponse(body=b"OK", status_code=200) + + # Direct channel succeeds + with patch("azurelinuxagent.common.utils.restutil._http_request", return_value=mock_successful_response): + with patch("azurelinuxagent.common.protocol.wire.WireClient.update_goal_state") as mock_update_goal_state: + with patch("azurelinuxagent.common.protocol.wire.WireClient.fetch", wraps=client.fetch) as patch_direct: + with patch("azurelinuxagent.common.protocol.wire.WireClient.fetch_manifest_through_host", + wraps=client.fetch_manifest_through_host) as patch_host: + ret = client.fetch_manifest([VMAgentManifestUri(uri="uri1")]) + self.assertEquals(ret, "OK") + + self.assertEquals(patch_host.call_count, 0) + # The host channel calls the direct function under the covers + self.assertEquals(patch_direct.call_count, 1) + self.assertEquals(mock_update_goal_state.call_count, 0) + + self.assertEquals(HostPluginProtocol.is_default_channel(), False) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.get_goal_state") + @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.get_artifact_request") + def test_fetch_manifest_should_use_host_channel_when_direct_channel_fails(self, mock_get_artifact_request, *args): + mock_get_artifact_request.return_value = "dummy_url", "dummy_header" + client = WireClient("foo.bar") + + HostPluginProtocol.set_default_channel(False) + + mock_failed_response = MockResponse(body=b"", status_code=httpclient.GONE) + mock_successful_response = MockResponse(body=b"OK", status_code=200) + + # Direct channel fails, host channel succeeds. Goal state should not have been updated and host channel + # should have been set as default + with patch("azurelinuxagent.common.utils.restutil._http_request", + side_effect=[mock_failed_response, mock_successful_response]): + with patch("azurelinuxagent.common.protocol.wire.WireClient.update_goal_state") as mock_update_goal_state: + with patch("azurelinuxagent.common.protocol.wire.WireClient.fetch", wraps=client.fetch) as patch_direct: + with patch("azurelinuxagent.common.protocol.wire.WireClient.fetch_manifest_through_host", + wraps=client.fetch_manifest_through_host) as patch_host: + ret = client.fetch_manifest([VMAgentManifestUri(uri="uri1")]) + self.assertEquals(ret, "OK") + + self.assertEquals(patch_host.call_count, 1) + # The host channel calls the direct function under the covers + self.assertEquals(patch_direct.call_count, 1 + patch_host.call_count) + self.assertEquals(mock_update_goal_state.call_count, 0) + + self.assertEquals(HostPluginProtocol.is_default_channel(), True) + + # Reset default channel + HostPluginProtocol.set_default_channel(False) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.get_goal_state") + @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.get_artifact_request") + def test_fetch_manifest_should_retry_the_host_channel_after_reloading_goal_state(self, mock_get_artifact_request, *args): + mock_get_artifact_request.return_value = "dummy_url", "dummy_header" + client = WireClient("foo.bar") + + HostPluginProtocol.set_default_channel(False) + + mock_failed_response = MockResponse(body=b"", status_code=httpclient.GONE) + mock_successful_response = MockResponse(body=b"OK", status_code=200) + + # Direct channel fails, host channel fails due to stale goal state, host channel succeeds after refresh. + # As a consequence, goal state should have been updated and host channel should have been set as default. + with patch("azurelinuxagent.common.utils.restutil._http_request", + side_effect=[mock_failed_response, mock_failed_response, mock_successful_response]): + with patch("azurelinuxagent.common.protocol.wire.WireClient.update_goal_state") as mock_update_goal_state: + with patch("azurelinuxagent.common.protocol.wire.WireClient.fetch", wraps=client.fetch) as patch_direct: + with patch("azurelinuxagent.common.protocol.wire.WireClient.fetch_manifest_through_host", + wraps=client.fetch_manifest_through_host) as patch_host: + ret = client.fetch_manifest([VMAgentManifestUri(uri="uri1")]) + self.assertEquals(ret, "OK") + + self.assertEquals(patch_host.call_count, 2) + # The host channel calls the direct function under the covers + self.assertEquals(patch_direct.call_count, 1 + patch_host.call_count) + self.assertEquals(mock_update_goal_state.call_count, 1) + + self.assertEquals(HostPluginProtocol.is_default_channel(), True) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.get_goal_state") + @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.get_artifact_request") + def test_fetch_manifest_should_update_goal_state_and_not_change_default_channel_if_host_fails(self, mock_get_artifact_request, *args): + mock_get_artifact_request.return_value = "dummy_url", "dummy_header" + client = WireClient("foo.bar") + + HostPluginProtocol.set_default_channel(False) + mock_failed_response = MockResponse(body=b"", status_code=httpclient.GONE) + + # Everything fails. Goal state should have been updated and host channel should not have been set as default. + with patch("azurelinuxagent.common.utils.restutil._http_request", return_value=mock_failed_response): + with patch("azurelinuxagent.common.protocol.wire.WireClient.update_goal_state") as mock_update_goal_state: + with patch("azurelinuxagent.common.protocol.wire.WireClient.fetch", wraps=client.fetch) as patch_direct: + with patch("azurelinuxagent.common.protocol.wire.WireClient.fetch_manifest_through_host", + wraps=client.fetch_manifest_through_host) as patch_host: + with self.assertRaises(ExtensionDownloadError): + client.fetch_manifest([VMAgentManifestUri(uri="uri1")]) + + self.assertEquals(patch_host.call_count, 2) + # The host channel calls the direct function under the covers + self.assertEquals(patch_direct.call_count, 1 + patch_host.call_count) + self.assertEquals(mock_update_goal_state.call_count, 1) + + self.assertEquals(HostPluginProtocol.is_default_channel(), False) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.get_goal_state") + @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.get_artifact_request") + def test_get_artifacts_profile_should_not_invoke_host_channel_when_direct_channel_succeeds(self, mock_get_artifact_request, *args): + mock_get_artifact_request.return_value = "dummy_url", "dummy_header" + client = WireClient("foo.bar") + client.ext_conf = ExtensionsConfig(None) + client.ext_conf.artifacts_profile_blob = "testurl" + json_profile = b'{ "onHold": true }' + + HostPluginProtocol.set_default_channel(False) + mock_successful_response = MockResponse(body=json_profile, status_code=200) + + # Direct channel succeeds + with patch("azurelinuxagent.common.utils.restutil._http_request", return_value=mock_successful_response): + with patch("azurelinuxagent.common.protocol.wire.WireClient.update_goal_state") as mock_update_goal_state: + with patch("azurelinuxagent.common.protocol.wire.WireClient.fetch", wraps=client.fetch) as patch_direct: + with patch("azurelinuxagent.common.protocol.wire.WireClient.get_artifacts_profile_through_host", + wraps=client.get_artifacts_profile_through_host) as patch_host: + ret = client.get_artifacts_profile() + self.assertIsInstance(ret, InVMArtifactsProfile) + + self.assertEquals(patch_host.call_count, 0) + self.assertEquals(patch_direct.call_count, 1) + self.assertEquals(mock_update_goal_state.call_count, 0) + + self.assertEquals(HostPluginProtocol.is_default_channel(), False) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.get_goal_state") + @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.get_artifact_request") + def test_get_artifacts_profile_should_use_host_channel_when_direct_channel_fails(self, mock_get_artifact_request, *args): + mock_get_artifact_request.return_value = "dummy_url", "dummy_header" + client = WireClient("foo.bar") + client.ext_conf = ExtensionsConfig(None) + client.ext_conf.artifacts_profile_blob = "testurl" + json_profile = b'{ "onHold": true }' + + HostPluginProtocol.set_default_channel(False) + + mock_failed_response = MockResponse(body=b"", status_code=httpclient.GONE) + mock_successful_response = MockResponse(body=json_profile, status_code=200) + + # Direct channel fails, host channel succeeds. Goal state should not have been updated and host channel + # should have been set as default + with patch("azurelinuxagent.common.utils.restutil._http_request", + side_effect=[mock_failed_response, mock_successful_response]): + with patch("azurelinuxagent.common.protocol.wire.WireClient.update_goal_state") as mock_update_goal_state: + with patch("azurelinuxagent.common.protocol.wire.WireClient.fetch", wraps=client.fetch) as patch_direct: + with patch("azurelinuxagent.common.protocol.wire.WireClient.get_artifacts_profile_through_host", + wraps=client.get_artifacts_profile_through_host) as patch_host: + ret = client.get_artifacts_profile() + self.assertIsInstance(ret, InVMArtifactsProfile) + + self.assertEquals(patch_host.call_count, 1) + # The host channel calls the direct function under the covers + self.assertEquals(patch_direct.call_count, 1 + patch_host.call_count) + self.assertEquals(mock_update_goal_state.call_count, 0) + + self.assertEquals(HostPluginProtocol.is_default_channel(), True) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.get_goal_state") + @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.get_artifact_request") + def test_get_artifacts_profile_should_retry_the_host_channel_after_reloading_goal_state(self, mock_get_artifact_request, *args): + mock_get_artifact_request.return_value = "dummy_url", "dummy_header" + client = WireClient("foo.bar") + client.ext_conf = ExtensionsConfig(None) + client.ext_conf.artifacts_profile_blob = "testurl" + json_profile = b'{ "onHold": true }' + + HostPluginProtocol.set_default_channel(False) + + mock_failed_response = MockResponse(body=b"", status_code=httpclient.GONE) + mock_successful_response = MockResponse(body=json_profile, status_code=200) + + # Direct channel fails, host channel fails due to stale goal state, host channel succeeds after refresh. + # As a consequence, goal state should have been updated and host channel should have been set as default. + with patch("azurelinuxagent.common.utils.restutil._http_request", + side_effect=[mock_failed_response, mock_failed_response, mock_successful_response]): + with patch("azurelinuxagent.common.protocol.wire.WireClient.update_goal_state") as mock_update_goal_state: + with patch("azurelinuxagent.common.protocol.wire.WireClient.fetch", wraps=client.fetch) as patch_direct: + with patch("azurelinuxagent.common.protocol.wire.WireClient.get_artifacts_profile_through_host", + wraps=client.get_artifacts_profile_through_host) as patch_host: + ret = client.get_artifacts_profile() + self.assertIsInstance(ret, InVMArtifactsProfile) + + self.assertEquals(patch_host.call_count, 2) + # The host channel calls the direct function under the covers + self.assertEquals(patch_direct.call_count, 1 + patch_host.call_count) + self.assertEquals(mock_update_goal_state.call_count, 1) + + self.assertEquals(HostPluginProtocol.is_default_channel(), True) + + @patch("azurelinuxagent.common.protocol.wire.WireClient.get_goal_state") + @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.get_artifact_request") + def test_get_artifacts_profile_should_update_goal_state_and_not_change_default_channel_if_host_fails(self, mock_get_artifact_request, *args): + mock_get_artifact_request.return_value = "dummy_url", "dummy_header" + client = WireClient("foo.bar") + client.ext_conf = ExtensionsConfig(None) + client.ext_conf.artifacts_profile_blob = "testurl" + json_profile = b'{ "onHold": true }' + + HostPluginProtocol.set_default_channel(False) + + mock_failed_response = MockResponse(body=b"", status_code=httpclient.GONE) + + # Everything fails. Goal state should have been updated and host channel should not have been set as default. + with patch("azurelinuxagent.common.utils.restutil._http_request", return_value=mock_failed_response): + with patch("azurelinuxagent.common.protocol.wire.WireClient.update_goal_state") as mock_update_goal_state: + with patch("azurelinuxagent.common.protocol.wire.WireClient.fetch", wraps=client.fetch) as patch_direct: + with patch("azurelinuxagent.common.protocol.wire.WireClient.get_artifacts_profile_through_host", + wraps=client.get_artifacts_profile_through_host) as patch_host: + ret = client.get_artifacts_profile() + self.assertEquals(ret, None) + + self.assertEquals(patch_host.call_count, 2) + # The host channel calls the direct function under the covers + self.assertEquals(patch_direct.call_count, 1 + patch_host.call_count) + self.assertEquals(mock_update_goal_state.call_count, 1) + + self.assertEquals(HostPluginProtocol.is_default_channel(), False) + + def test_send_request_using_appropriate_channel_should_not_invoke_host_channel_when_direct_channel_succeeds(self, *args): + xml_text = WireProtocolData(DATA_FILE).goal_state + client = WireClient(wireserver_url) + client.goal_state = GoalState(xml_text) + client.get_host_plugin().set_default_channel(False) + + def direct_func(*args): + direct_func.counter += 1 + return 42 + + def host_func(*args): + host_func.counter += 1 + return None + + direct_func.counter = 0 + host_func.counter = 0 + + # Assert we've only called the direct channel functions and that it succeeded. + ret = client.send_request_using_appropriate_channel(direct_func, host_func) + self.assertEquals(42, ret) + self.assertEquals(1, direct_func.counter) + self.assertEquals(0, host_func.counter) + + def test_send_request_using_appropriate_channel_should_not_use_direct_channel_when_host_channel_is_default(self, *args): + xml_text = WireProtocolData(DATA_FILE).goal_state + client = WireClient(wireserver_url) + client.goal_state = GoalState(xml_text) + client.get_host_plugin().set_default_channel(True) + + def direct_func(*args): + direct_func.counter += 1 + return 42 + + def host_func(*args): + host_func.counter += 1 + return 43 + + direct_func.counter = 0 + host_func.counter = 0 + + # Assert we've only called the host channel function since it's the default channel + ret = client.send_request_using_appropriate_channel(direct_func, host_func) + self.assertEquals(43, ret) + self.assertEquals(0, direct_func.counter) + self.assertEquals(1, host_func.counter) + + def test_send_request_using_appropriate_channel_should_use_host_channel_when_direct_channel_fails(self, *args): + xml_text = WireProtocolData(DATA_FILE).goal_state + client = WireClient(wireserver_url) + client.goal_state = GoalState(xml_text) + host = client.get_host_plugin() + host.set_default_channel(False) + + def direct_func(*args): + direct_func.counter += 1 + raise InvalidContainerError() + + def host_func(*args): + host_func.counter += 1 + return 42 + + direct_func.counter = 0 + host_func.counter = 0 + + # Assert we've called both the direct channel function and the host channel function, which succeeded. + # After the host channel succeeds, the host plugin should have been set as the default channel. + ret = client.send_request_using_appropriate_channel(direct_func, host_func) + self.assertEquals(42, ret) + self.assertEquals(1, direct_func.counter) + self.assertEquals(1, host_func.counter) + self.assertEquals(True, host.is_default_channel()) + + def test_send_request_using_appropriate_channel_should_retry_the_host_channel_after_reloading_goal_state(self, *args): + xml_text = WireProtocolData(DATA_FILE).goal_state + client = WireClient(wireserver_url) + client.goal_state = GoalState(xml_text) + client.get_host_plugin().set_default_channel(False) + + def direct_func(*args): + direct_func.counter += 1 + raise InvalidContainerError() + + def host_func(*args): + host_func.counter += 1 + if host_func.counter == 1: + raise ResourceGoneError("Resource is gone") + return 42 + + direct_func.counter = 0 + host_func.counter = 0 + + # Assert we've called both the direct channel function (once) and the host channel function (twice). + # After the host channel succeeds, the host plugin should have been set as the default channel. + with patch('azurelinuxagent.common.protocol.wire.WireClient.update_goal_state') as mock_update_goal_state: + ret = client.send_request_using_appropriate_channel(direct_func, host_func) + self.assertEquals(42, ret) + self.assertEquals(1, direct_func.counter) + self.assertEquals(2, host_func.counter) + self.assertEquals(1, mock_update_goal_state.call_count) + self.assertEquals(True, client.get_host_plugin().is_default_channel()) + class MockResponse: def __init__(self, body, status_code): self.body = body self.status = status_code - def read(self): + def read(self, *_): return self.body diff -Nru waagent-2.2.34/tests/test_agent.py waagent-2.2.45/tests/test_agent.py --- waagent-2.2.34/tests/test_agent.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/test_agent.py 2019-11-07 00:36:56.000000000 +0000 @@ -36,6 +36,7 @@ HttpProxy.Host = None HttpProxy.Port = None Lib.Dir = /var/lib/waagent +Logs.Console = True Logs.Verbose = False OS.AllowHTTP = False OS.CheckRdmaDriver = False @@ -51,17 +52,16 @@ OS.SudoersDir = /etc/sudoers.d OS.UpdateRdmaDriver = False Pid.File = /var/run/waagent.pid +Provisioning.Agent = auto Provisioning.AllowResetSysUser = False Provisioning.DecodeCustomData = False Provisioning.DeleteRootPassword = True -Provisioning.Enabled = True Provisioning.ExecuteCustomData = False Provisioning.MonitorHostName = True Provisioning.PasswordCryptId = 6 Provisioning.PasswordCryptSaltLength = 10 Provisioning.RegenerateSshHostKeyPair = True Provisioning.SshHostKeyPairType = rsa -Provisioning.UseCloudInit = True ResourceDisk.EnableSwap = False ResourceDisk.EnableSwapEncryption = False ResourceDisk.Filesystem = ext4 @@ -74,13 +74,13 @@ def test_accepts_configuration_path(self): conf_path = os.path.join(data_dir, "test_waagent.conf") - c, f, v, cfp = parse_args(["-configuration-path:" + conf_path]) + c, f, v, d, cfp = parse_args(["-configuration-path:" + conf_path]) self.assertEqual(cfp, conf_path) @patch("os.path.exists", return_value=True) def test_checks_configuration_path(self, mock_exists): conf_path = "/foo/bar-baz/something.conf" - c, f, v, cfp = parse_args(["-configuration-path:"+conf_path]) + c, f, v, d, cfp = parse_args(["-configuration-path:"+conf_path]) self.assertEqual(cfp, conf_path) self.assertEqual(mock_exists.call_count, 1) @@ -89,13 +89,13 @@ @patch("sys.exit", side_effect=Exception) def test_rejects_missing_configuration_path(self, mock_exit, mock_exists, mock_stderr): try: - c, f, v, cfp = parse_args(["-configuration-path:/foo/bar.conf"]) + c, f, v, d, cfp = parse_args(["-configuration-path:/foo/bar.conf"]) self.assertTrue(False) except Exception: self.assertEqual(mock_exit.call_count, 1) def test_configuration_path_defaults_to_none(self): - c, f, v, cfp = parse_args([]) + c, f, v, d, cfp = parse_args([]) self.assertEqual(cfp, None) def test_agent_accepts_configuration_path(self): diff -Nru waagent-2.2.34/tests/tools.py waagent-2.2.45/tests/tools.py --- waagent-2.2.34/tests/tools.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/tools.py 2019-11-07 00:36:56.000000000 +0000 @@ -23,22 +23,28 @@ import pprint import re import shutil +import stat +import sys import tempfile import unittest from functools import wraps import time +from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator import azurelinuxagent.common.event as event import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger +from azurelinuxagent.common.osutil.factory import _get_osutil +from azurelinuxagent.common.osutil.ubuntu import Ubuntu14OSUtil, Ubuntu16OSUtil from azurelinuxagent.common.utils import fileutil - from azurelinuxagent.common.version import PY_VERSION_MAJOR -# Import mock module for Python2 and Python3 try: from unittest.mock import Mock, patch, MagicMock, ANY, DEFAULT, call + + # Import mock module for Python2 and Python3 + from bin.waagent2 import Agent except ImportError: from mock import Mock, patch, MagicMock, ANY, DEFAULT, call @@ -54,14 +60,21 @@ logger.add_logger_appender(logger.AppenderType.STDOUT, logger.LogLevel.VERBOSE) +_MAX_LENGTH = 120 -def _do_nothing(): - pass +_MAX_LENGTH_SAFE_REPR = 80 +# Mock sleep to reduce test execution time +_SLEEP = time.sleep -_MAX_LENGTH = 120 -_MAX_LENGTH_SAFE_REPR = 80 +def mock_sleep(sec=0.01): + """ + Mocks the time.sleep method to reduce unit test time + :param sec: Time to replace the sleep call with, default = 0.01sec + """ + _SLEEP(sec) + def safe_repr(obj, short=False): try: @@ -102,23 +115,80 @@ return result[:_MAX_LENGTH] + ' [truncated]...' +def running_under_travis(): + return 'TRAVIS' in os.environ and os.environ['TRAVIS'] == 'true' + + +def get_osutil_for_travis(): + distro_name = os.environ['_system_name'].lower() + distro_version = os.environ['_system_version'] + + if distro_name == "ubuntu" and distro_version == "14.04": + return Ubuntu14OSUtil() + + if distro_name == "ubuntu" and distro_version == "16.04": + return Ubuntu16OSUtil() + + +def mock_get_osutil(*args): + # It's a known issue that calling platform.linux_distribution() in Travis will result in the wrong info. + # See https://github.com/travis-ci/travis-ci/issues/2755 + # When running in Travis, use manual distro resolution that relies on environment variables. + if running_under_travis(): + return get_osutil_for_travis() + else: + return _get_osutil(*args) + + +def are_cgroups_enabled(): + # We use a function decorator to check if cgroups are enabled in multiple tests, which at some point calls + # get_osutil. The global mock for that function doesn't get executed before the function decorators are imported, + # so we need to specifically mock it beforehand. + mock__get_osutil = patch("azurelinuxagent.common.osutil.factory._get_osutil", mock_get_osutil) + mock__get_osutil.start() + ret = CGroupConfigurator.get_instance().enabled + mock__get_osutil.stop() + return ret + + +def is_trusty_in_travis(): + # In Travis, Trusty (Ubuntu 14.04) is missing the cpuacct.stat file, + # possibly because the accounting is not enabled by default. + if not running_under_travis(): + return False + + return type(get_osutil_for_travis()) == Ubuntu14OSUtil + + +def is_systemd_present(): + return os.path.exists("/run/systemd/system") + + +def i_am_root(): + return os.geteuid() == 0 + + class AgentTestCase(unittest.TestCase): @classmethod def setUpClass(cls): # Setup newer unittest assertions missing in prior versions of Python if not hasattr(cls, "assertRegex"): - cls.assertRegex = cls.assertRegexpMatches if hasattr(cls, "assertRegexpMatches") else _do_nothing + cls.assertRegex = cls.assertRegexpMatches if hasattr(cls, "assertRegexpMatches") else cls.emulate_assertRegexpMatches if not hasattr(cls, "assertNotRegex"): - cls.assertNotRegex = cls.assertNotRegexpMatches if hasattr(cls, "assertNotRegexpMatches") else _do_nothing + cls.assertNotRegex = cls.assertNotRegexpMatches if hasattr(cls, "assertNotRegexpMatches") else cls.emulate_assertNotRegexpMatches if not hasattr(cls, "assertIn"): cls.assertIn = cls.emulate_assertIn if not hasattr(cls, "assertNotIn"): cls.assertNotIn = cls.emulate_assertNotIn if not hasattr(cls, "assertGreater"): cls.assertGreater = cls.emulate_assertGreater + if not hasattr(cls, "assertGreaterEqual"): + cls.assertGreaterEqual = cls.emulate_assertGreaterEqual if not hasattr(cls, "assertLess"): cls.assertLess = cls.emulate_assertLess + if not hasattr(cls, "assertLessEqual"): + cls.assertLessEqual = cls.emulate_assertLessEqual if not hasattr(cls, "assertIsNone"): cls.assertIsNone = cls.emulate_assertIsNone if not hasattr(cls, "assertIsNotNone"): @@ -129,6 +199,17 @@ cls.assertRaisesRegex = cls.emulate_raises_regex if not hasattr(cls, "assertListEqual"): cls.assertListEqual = cls.emulate_assertListEqual + if not hasattr(cls, "assertIsInstance"): + cls.assertIsInstance = cls.emulate_assertIsInstance + if sys.version_info < (2, 7): + # assertRaises does not implement a context manager in 2.6; override it with emulate_assertRaises but + # keep a pointer to the original implementation to use when a context manager is not requested. + cls.original_assertRaises = unittest.TestCase.assertRaises + cls.assertRaises = cls.emulate_assertRaises + + @classmethod + def tearDownClass(cls): + pass def setUp(self): prefix = "{0}_".format(self.__class__.__name__) @@ -147,10 +228,15 @@ event.init_event_status(self.tmp_dir) event.init_event_logger(self.tmp_dir) + self.mock__get_osutil = patch("azurelinuxagent.common.osutil.factory._get_osutil", mock_get_osutil) + self.mock__get_osutil.start() + def tearDown(self): if not debug and self.tmp_dir is not None: shutil.rmtree(self.tmp_dir) + self.mock__get_osutil.stop() + def emulate_assertIn(self, a, b, msg=None): if a not in b: msg = msg if msg is not None else "{0} not found in {1}".format(_safe_repr(a), _safe_repr(b)) @@ -166,11 +252,21 @@ msg = msg if msg is not None else '{0} not greater than {1}'.format(_safe_repr(a), _safe_repr(b)) self.fail(msg) + def emulate_assertGreaterEqual(self, a, b, msg=None): + if not a >= b: + msg = msg if msg is not None else '{0} not greater or equal to {1}'.format(_safe_repr(a), _safe_repr(b)) + self.fail(msg) + def emulate_assertLess(self, a, b, msg=None): if not a < b: msg = msg if msg is not None else '{0} not less than {1}'.format(_safe_repr(a), _safe_repr(b)) self.fail(msg) + def emulate_assertLessEqual(self, a, b, msg=None): + if not a <= b: + msg = msg if msg is not None else '{0} not less or equal to {1}'.format(_safe_repr(a), _safe_repr(b)) + self.fail(msg) + def emulate_assertIsNone(self, x, msg=None): if x is not None: msg = msg if msg is not None else '{0} is not None'.format(_safe_repr(x)) @@ -181,6 +277,50 @@ msg = msg if msg is not None else '{0} is None'.format(_safe_repr(x)) self.fail(msg) + def emulate_assertRegexpMatches(self, text, regexp, msg=None): + if re.search(regexp, text) is not None: + return + msg = msg if msg is not None else "'{0}' does not match '{1}'.".format(text, regexp) + self.fail(msg) + + def emulate_assertNotRegexpMatches(self, text, regexp, msg=None): + if re.search(regexp, text, flags=1) is None: + return + msg = msg if msg is not None else "'{0}' should not match '{1}'.".format(text, regexp) + self.fail(msg) + + class _AssertRaisesContextManager(object): + def __init__(self, expected_exception_type, test_case): + self._expected_exception_type = expected_exception_type + self._test_case = test_case + + def __enter__(self): + return self + + @staticmethod + def _get_type_name(type): + return type.__name__ if hasattr(type, "__name__") else str(type) + + def __exit__(self, exception_type, exception, *_): + if exception_type is None: + expected = AgentTestCase._AssertRaisesContextManager._get_type_name(self._expected_exception_type) + self._test_case.fail("Did not raise an exception; expected '{0}'".format(expected)) + if not issubclass(exception_type, self._expected_exception_type): + raised = AgentTestCase._AssertRaisesContextManager._get_type_name(exception_type) + expected = AgentTestCase._AssertRaisesContextManager._get_type_name(self._expected_exception_type) + self._test_case.fail("Raised '{0}', but expected '{1}'".format(raised, expected)) + + self.exception = exception + return True + + def emulate_assertRaises(self, exception_type, function=None, *args, **kwargs): + # return a context manager only when function is not provided; otherwise use the original assertRaises + if function is None: + return AgentTestCase._AssertRaisesContextManager(exception_type, self) + + self.original_assertRaises(exception_type, function, *args, **kwargs) + + return None def emulate_raises_regex(self, exception_type, regex, function, *args, **kwargs): try: @@ -296,6 +436,12 @@ msg = self._formatMessage(msg, standardMsg) self.fail(msg) + def emulate_assertIsInstance(self, obj, object_type, msg=None): + if not isinstance(obj, object_type): + msg = msg if msg is not None else '{0} is not an instance of {1}'.format(_safe_repr(obj), + _safe_repr(object_type)) + self.fail(msg) + @staticmethod def _create_files(tmp_dir, prefix, suffix, count, with_sleep=0): for i in range(count): @@ -303,6 +449,33 @@ fileutil.write_file(f, "faux content") time.sleep(with_sleep) + def create_script(self, file_name, contents, file_path=None): + """ + Creates an executable script with the given contents. + If file_name ends with ".py", it creates a Python3 script, otherwise it creates a bash script + :param file_name: The name of the file to create the script with + :param contents: Contents of the script file + :param file_path: The path of the file where to create it in (we use /tmp/ by default) + :return: + """ + if not file_path: + file_path = os.path.join(self.tmp_dir, file_name) + + directory = os.path.dirname(file_path) + if not os.path.exists(directory): + os.mkdir(directory) + + with open(file_path, "w") as script: + if file_name.endswith(".py"): + script.write("#!/usr/bin/env python3\n") + else: + script.write("#!/usr/bin/env bash\n") + script.write(contents) + + os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + + return file_name + def load_data(name): """Load test data""" diff -Nru waagent-2.2.34/tests/utils/cgroups_tools.py waagent-2.2.45/tests/utils/cgroups_tools.py --- waagent-2.2.34/tests/utils/cgroups_tools.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/utils/cgroups_tools.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,50 @@ +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# + +import os +from azurelinuxagent.common.cgroupapi import VM_AGENT_CGROUP_NAME +from azurelinuxagent.common.utils import fileutil + +class CGroupsTools(object): + @staticmethod + def create_legacy_agent_cgroup(cgroups_file_system_root, controller, daemon_pid): + """ + Previous versions of the daemon (2.2.31-2.2.40) wrote their PID to /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent; + starting from version 2.2.41 we track the agent service in walinuxagent.service instead of WALinuxAgent/WALinuxAgent. + + This method creates a mock cgroup using the legacy path and adds the given PID to it. + """ + legacy_cgroup = os.path.join(cgroups_file_system_root, controller, "WALinuxAgent", "WALinuxAgent") + if not os.path.exists(legacy_cgroup): + os.makedirs(legacy_cgroup) + fileutil.append_file(os.path.join(legacy_cgroup, "cgroup.procs"), daemon_pid + "\n") + return legacy_cgroup + + @staticmethod + def create_agent_cgroup(cgroups_file_system_root, controller, extension_handler_pid): + """ + Previous versions of the daemon (2.2.31-2.2.40) wrote their PID to /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent; + starting from version 2.2.41 we track the agent service in walinuxagent.service instead of WALinuxAgent/WALinuxAgent. + + This method creates a mock cgroup using the newer path and adds the given PID to it. + """ + new_cgroup = os.path.join(cgroups_file_system_root, controller, VM_AGENT_CGROUP_NAME) + if not os.path.exists(new_cgroup): + os.makedirs(new_cgroup) + fileutil.append_file(os.path.join(new_cgroup, "cgroup.procs"), extension_handler_pid + "\n") + return new_cgroup + diff -Nru waagent-2.2.34/tests/utils/process_target.sh waagent-2.2.45/tests/utils/process_target.sh --- waagent-2.2.34/tests/utils/process_target.sh 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/utils/process_target.sh 1970-01-01 00:00:00.000000000 +0000 @@ -1,41 +0,0 @@ -#!/bin/sh - -count=0 -exitcode=0 - -while [ $# -gt 0 ]; do - case "$1" in - -e) - shift - echo $1 1>&2 - shift - ;; - -o) - shift - echo $1 - shift - ;; - -t) - shift - count=$1 - shift - ;; - -x) - shift - exitcode=$1 - shift - ;; - *) - break - ;; - esac -done - -if [ $count -gt 0 ]; then - for iter in $(seq 1 $count); do - sleep 1 - echo "Iteration $iter" - done -fi - -exit $exitcode diff -Nru waagent-2.2.34/tests/utils/test_crypt_util.py waagent-2.2.45/tests/utils/test_crypt_util.py --- waagent-2.2.34/tests/utils/test_crypt_util.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/utils/test_crypt_util.py 2019-11-07 00:36:56.000000000 +0000 @@ -15,26 +15,9 @@ # Requires Python 2.6+ and Openssl 1.0+ # -import base64 -import binascii -import errno as errno -import glob -import random -import string -import subprocess -import sys -import tempfile -import uuid -import unittest - -import azurelinuxagent.common.conf as conf -import azurelinuxagent.common.utils.shellutil as shellutil -from azurelinuxagent.common.future import ustr -from azurelinuxagent.common.utils.cryptutil import CryptUtil from azurelinuxagent.common.exception import CryptError -from azurelinuxagent.common.version import PY_VERSION_MAJOR +from azurelinuxagent.common.utils.cryptutil import CryptUtil from tests.tools import * -from subprocess import CalledProcessError def is_python_version_26(): @@ -42,6 +25,7 @@ class TestCryptoUtilOperations(AgentTestCase): + def test_decrypt_encrypted_text(self): encrypted_string = load_data("wire/encrypted.enc") prv_key = os.path.join(self.tmp_dir, "TransportPrivate.pem") @@ -75,6 +59,20 @@ crypto = CryptUtil(conf.get_openssl_cmd()) self.assertRaises(CryptError, crypto.decrypt_secret, encrypted_string, prv_key) + def test_get_pubkey_from_crt(self): + crypto = CryptUtil(conf.get_openssl_cmd()) + prv_key = os.path.join(data_dir, "wire", "trans_prv") + expected_pub_key = os.path.join(data_dir, "wire", "trans_pub") + + with open(expected_pub_key) as fh: + self.assertEqual(fh.read(), crypto.get_pubkey_from_prv(prv_key)) + + def test_get_pubkey_from_crt_invalid_file(self): + crypto = CryptUtil(conf.get_openssl_cmd()) + prv_key = os.path.join(data_dir, "wire", "trans_prv_does_not_exist") + + self.assertRaises(IOError, crypto.get_pubkey_from_prv, prv_key) + if __name__ == '__main__': unittest.main() diff -Nru waagent-2.2.34/tests/utils/test_extension_process_util.py waagent-2.2.45/tests/utils/test_extension_process_util.py --- waagent-2.2.34/tests/utils/test_extension_process_util.py 1970-01-01 00:00:00.000000000 +0000 +++ waagent-2.2.45/tests/utils/test_extension_process_util.py 2019-11-07 00:36:56.000000000 +0000 @@ -0,0 +1,260 @@ +# Copyright Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# +from azurelinuxagent.common.exception import ExtensionError, ExtensionErrorCodes +from azurelinuxagent.common.future import ustr +from azurelinuxagent.common.utils.extensionprocessutil import format_stdout_stderr, read_output, \ + wait_for_process_completion_or_timeout, handle_process_completion +from tests.tools import * +import subprocess + + +class TestProcessUtils(AgentTestCase): + def setUp(self): + AgentTestCase.setUp(self) + self.tmp_dir = tempfile.mkdtemp() + self.stdout = tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") + self.stderr = tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") + + self.stdout.write("The quick brown fox jumps over the lazy dog.".encode("utf-8")) + self.stderr.write("The five boxing wizards jump quickly.".encode("utf-8")) + + def tearDown(self): + if self.tmp_dir is not None: + shutil.rmtree(self.tmp_dir) + + def test_wait_for_process_completion_or_timeout_should_terminate_cleanly(self): + process = subprocess.Popen( + "date", + shell=True, + cwd=self.tmp_dir, + env={}, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + timed_out, ret = wait_for_process_completion_or_timeout(process=process, timeout=5) + self.assertEquals(timed_out, False) + self.assertEquals(ret, 0) + + def test_wait_for_process_completion_or_timeout_should_kill_process_on_timeout(self): + timeout = 5 + process = subprocess.Popen( + "sleep 1m", + shell=True, + cwd=self.tmp_dir, + env={}, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + preexec_fn=os.setsid) + + # We don't actually mock the kill, just wrap it so we can assert its call count + with patch('azurelinuxagent.common.utils.extensionprocessutil.os.killpg', wraps=os.killpg) as patch_kill: + with patch('time.sleep') as mock_sleep: + timed_out, ret = wait_for_process_completion_or_timeout(process=process, timeout=timeout) + + # We're mocking sleep to avoid prolonging the test execution time, but we still want to make sure + # we're "waiting" the correct amount of time before killing the process + self.assertEquals(mock_sleep.call_count, timeout) + + self.assertEquals(patch_kill.call_count, 1) + self.assertEquals(timed_out, True) + self.assertEquals(ret, None) + + def test_handle_process_completion_should_return_nonzero_when_process_fails(self): + process = subprocess.Popen( + "ls folder_does_not_exist", + shell=True, + cwd=self.tmp_dir, + env={}, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + timed_out, ret = wait_for_process_completion_or_timeout(process=process, timeout=5) + self.assertEquals(timed_out, False) + self.assertEquals(ret, 2) + + def test_handle_process_completion_should_return_process_output(self): + command = "echo 'dummy stdout' && 1>&2 echo 'dummy stderr'" + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: + process = subprocess.Popen(command, + shell=True, + cwd=self.tmp_dir, + env={}, + stdout=stdout, + stderr=stderr, + preexec_fn=os.setsid) + + process_output = handle_process_completion(process=process, + command=command, + timeout=5, + stdout=stdout, + stderr=stderr, + error_code=42) + + expected_output = "[stdout]\ndummy stdout\n\n\n[stderr]\ndummy stderr\n" + self.assertEquals(process_output, expected_output) + + def test_handle_process_completion_should_raise_on_timeout(self): + command = "sleep 1m" + timeout = 5 + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: + with patch('time.sleep') as mock_sleep: + with self.assertRaises(ExtensionError) as context_manager: + process = subprocess.Popen(command, + shell=True, + cwd=self.tmp_dir, + env={}, + stdout=stdout, + stderr=stderr, + preexec_fn=os.setsid) + + handle_process_completion(process=process, + command=command, + timeout=timeout, + stdout=stdout, + stderr=stderr, + error_code=42) + + # We're mocking sleep to avoid prolonging the test execution time, but we still want to make sure + # we're "waiting" the correct amount of time before killing the process and raising an exception + self.assertEquals(mock_sleep.call_count, timeout) + + self.assertEquals(context_manager.exception.code, ExtensionErrorCodes.PluginHandlerScriptTimedout) + self.assertIn("Timeout({0})".format(timeout), ustr(context_manager.exception)) + + def test_handle_process_completion_should_raise_on_nonzero_exit_code(self): + command = "ls folder_does_not_exist" + error_code = 42 + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: + with self.assertRaises(ExtensionError) as context_manager: + process = subprocess.Popen(command, + shell=True, + cwd=self.tmp_dir, + env={}, + stdout=stdout, + stderr=stderr, + preexec_fn=os.setsid) + + handle_process_completion(process=process, + command=command, + timeout=4, + stdout=stdout, + stderr=stderr, + error_code=error_code) + + self.assertEquals(context_manager.exception.code, error_code) + self.assertIn("Non-zero exit code:", ustr(context_manager.exception)) + + def test_read_output_it_should_return_no_content(self): + with patch('azurelinuxagent.common.utils.extensionprocessutil.TELEMETRY_MESSAGE_MAX_LEN', 0): + expected = "[stdout]\n\n\n[stderr]\n" + actual = read_output(self.stdout, self.stderr) + self.assertEqual(expected, actual) + + def test_read_output_it_should_truncate_the_content(self): + with patch('azurelinuxagent.common.utils.extensionprocessutil.TELEMETRY_MESSAGE_MAX_LEN', 10): + expected = "[stdout]\nThe quick \n\n[stderr]\nThe five b" + actual = read_output(self.stdout, self.stderr) + self.assertEqual(expected, actual) + + def test_read_output_it_should_return_all_content(self): + with patch('azurelinuxagent.common.utils.extensionprocessutil.TELEMETRY_MESSAGE_MAX_LEN', 50): + expected = "[stdout]\nThe quick brown fox jumps over the lazy dog.\n\n" \ + "[stderr]\nThe five boxing wizards jump quickly." + actual = read_output(self.stdout, self.stderr) + self.assertEqual(expected, actual) + + def test_read_output_it_should_handle_exceptions(self): + with patch('azurelinuxagent.common.utils.extensionprocessutil.TELEMETRY_MESSAGE_MAX_LEN', "type error"): + actual = read_output(self.stdout, self.stderr) + self.assertIn("Cannot read stdout/stderr", actual) + + def test_format_stdout_stderr00(self): + """ + If stdout and stderr are both smaller than the max length, + the full representation should be displayed. + """ + stdout = "The quick brown fox jumps over the lazy dog." + stderr = "The five boxing wizards jump quickly." + + expected = "[stdout]\n{0}\n\n[stderr]\n{1}".format(stdout, stderr) + actual = format_stdout_stderr(stdout, stderr, 1000) + self.assertEqual(expected, actual) + + def test_format_stdout_stderr01(self): + """ + If stdout and stderr both exceed the max length, + then both stdout and stderr are trimmed equally. + """ + stdout = "The quick brown fox jumps over the lazy dog." + stderr = "The five boxing wizards jump quickly." + + # noinspection SpellCheckingInspection + expected = '[stdout]\ns over the lazy dog.\n\n[stderr]\nizards jump quickly.' + actual = format_stdout_stderr(stdout, stderr, 60) + self.assertEqual(expected, actual) + self.assertEqual(60, len(actual)) + + def test_format_stdout_stderr02(self): + """ + If stderr is much larger than stdout, stderr is allowed + to borrow space from stdout's quota. + """ + stdout = "empty" + stderr = "The five boxing wizards jump quickly." + + expected = '[stdout]\nempty\n\n[stderr]\ns jump quickly.' + actual = format_stdout_stderr(stdout, stderr, 40) + self.assertEqual(expected, actual) + self.assertEqual(40, len(actual)) + + def test_format_stdout_stderr03(self): + """ + If stdout is much larger than stderr, stdout is allowed + to borrow space from stderr's quota. + """ + stdout = "The quick brown fox jumps over the lazy dog." + stderr = "empty" + + expected = '[stdout]\nr the lazy dog.\n\n[stderr]\nempty' + actual = format_stdout_stderr(stdout, stderr, 40) + self.assertEqual(expected, actual) + self.assertEqual(40, len(actual)) + + def test_format_stdout_stderr04(self): + """ + If the max length is not sufficient to even hold the stdout + and stderr markers an empty string is returned. + """ + stdout = "The quick brown fox jumps over the lazy dog." + stderr = "The five boxing wizards jump quickly." + + expected = '' + actual = format_stdout_stderr(stdout, stderr, 4) + self.assertEqual(expected, actual) + self.assertEqual(0, len(actual)) + + def test_format_stdout_stderr05(self): + """ + If stdout and stderr are empty, an empty template is returned. + """ + + expected = '[stdout]\n\n\n[stderr]\n' + actual = format_stdout_stderr('', '', 1000) + self.assertEqual(expected, actual) diff -Nru waagent-2.2.34/tests/utils/test_process_util.py waagent-2.2.45/tests/utils/test_process_util.py --- waagent-2.2.34/tests/utils/test_process_util.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/utils/test_process_util.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,260 +0,0 @@ -# Copyright Microsoft Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Requires Python 2.6+ and Openssl 1.0+ -# -import datetime -import subprocess - -from azurelinuxagent.common.exception import ExtensionError -from azurelinuxagent.common.utils.processutil \ - import format_stdout_stderr, capture_from_process -from tests.tools import * -import sys - -process_target = "{0}/process_target.sh".format(os.path.abspath(os.path.join(__file__, os.pardir))) -process_cmd_template = "{0} -o '{1}' -e '{2}'" - -EXTENSION_ERROR_CODE = 1000 - -class TestProcessUtils(AgentTestCase): - def test_format_stdout_stderr00(self): - """ - If stdout and stderr are both smaller than the max length, - the full representation should be displayed. - """ - stdout = "The quick brown fox jumps over the lazy dog." - stderr = "The five boxing wizards jump quickly." - - expected = "[stdout]\n{0}\n\n[stderr]\n{1}".format(stdout, stderr) - actual = format_stdout_stderr(stdout, stderr, 1000) - self.assertEqual(expected, actual) - - def test_format_stdout_stderr01(self): - """ - If stdout and stderr both exceed the max length, - then both stdout and stderr are trimmed equally. - """ - stdout = "The quick brown fox jumps over the lazy dog." - stderr = "The five boxing wizards jump quickly." - - # noinspection SpellCheckingInspection - expected = '[stdout]\ns over the lazy dog.\n\n[stderr]\nizards jump quickly.' - actual = format_stdout_stderr(stdout, stderr, 60) - self.assertEqual(expected, actual) - self.assertEqual(60, len(actual)) - - def test_format_stdout_stderr02(self): - """ - If stderr is much larger than stdout, stderr is allowed - to borrow space from stdout's quota. - """ - stdout = "empty" - stderr = "The five boxing wizards jump quickly." - - expected = '[stdout]\nempty\n\n[stderr]\ns jump quickly.' - actual = format_stdout_stderr(stdout, stderr, 40) - self.assertEqual(expected, actual) - self.assertEqual(40, len(actual)) - - def test_format_stdout_stderr03(self): - """ - If stdout is much larger than stderr, stdout is allowed - to borrow space from stderr's quota. - """ - stdout = "The quick brown fox jumps over the lazy dog." - stderr = "empty" - - expected = '[stdout]\nr the lazy dog.\n\n[stderr]\nempty' - actual = format_stdout_stderr(stdout, stderr, 40) - self.assertEqual(expected, actual) - self.assertEqual(40, len(actual)) - - def test_format_stdout_stderr04(self): - """ - If the max length is not sufficient to even hold the stdout - and stderr markers an empty string is returned. - """ - stdout = "The quick brown fox jumps over the lazy dog." - stderr = "The five boxing wizards jump quickly." - - expected = '' - actual = format_stdout_stderr(stdout, stderr, 4) - self.assertEqual(expected, actual) - self.assertEqual(0, len(actual)) - - def test_format_stdout_stderr05(self): - """ - If stdout and stderr are empty, an empty template is returned. - """ - - expected = '[stdout]\n\n\n[stderr]\n' - actual = format_stdout_stderr('', '', 1000) - self.assertEqual(expected, actual) - - def test_process_stdout_stderr(self): - """ - If the command has no timeout, the process need not be the leader of its own process group. - """ - stdout = "The quick brown fox jumps over the lazy dog.\n" - stderr = "The five boxing wizards jump quickly.\n" - - expected = "[stdout]\n{0}\n\n[stderr]\n{1}".format(stdout, stderr) - - cmd = process_cmd_template.format(process_target, stdout, stderr) - process = subprocess.Popen(cmd, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=os.environ) - - actual = capture_from_process(process, cmd) - self.assertEqual(expected, actual) - - def test_process_timeout_non_forked(self): - """ - non-forked process runs for 20 seconds, timeout is 10 seconds - we expect: - - test to run in just over 10 seconds - - exception should be thrown - - output should be collected - """ - cmd = "{0} -t 20".format(process_target) - process = subprocess.Popen(cmd, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=os.environ, - preexec_fn=os.setsid) - - try: - capture_from_process(process, 'sleep 20', 10, EXTENSION_ERROR_CODE) - self.fail('Timeout exception was expected') - except ExtensionError as e: - body = str(e) - self.assertTrue('Timeout(10)' in body) - self.assertTrue('Iteration 9' in body) - self.assertFalse('Iteration 11' in body) - self.assertEqual(EXTENSION_ERROR_CODE, e.code) - except Exception as gen_ex: - self.fail('Unexpected exception: {0}'.format(gen_ex)) - - def test_process_timeout_forked(self): - """ - forked process runs for 20 seconds, timeout is 10 seconds - we expect: - - test to run in less than 3 seconds - - no exception should be thrown - - no output is collected - """ - cmd = "{0} -t 20 &".format(process_target) - process = subprocess.Popen(cmd, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=os.environ, - preexec_fn=os.setsid) - - start = datetime.datetime.utcnow() - try: - cap = capture_from_process(process, 'sleep 20 &', 10) - except Exception as e: - self.fail('No exception should be thrown for a long running process which forks: {0}'.format(e)) - duration = datetime.datetime.utcnow() - start - - self.assertTrue(duration < datetime.timedelta(seconds=3)) - self.assertEqual('[stdout]\ncannot collect stdout\n\n[stderr]\n', cap) - - def test_process_behaved_non_forked(self): - """ - non-forked process runs for 10 seconds, timeout is 20 seconds - we expect: - - test to run in just over 10 seconds - - no exception should be thrown - - output should be collected - """ - cmd = "{0} -t 10".format(process_target) - process = subprocess.Popen(cmd, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=os.environ, - preexec_fn=os.setsid) - - try: - body = capture_from_process(process, 'sleep 10', 20) - except Exception as gen_ex: - self.fail('Unexpected exception: {0}'.format(gen_ex)) - - self.assertFalse('Timeout' in body) - self.assertTrue('Iteration 9' in body) - self.assertTrue('Iteration 10' in body) - - def test_process_behaved_forked(self): - """ - forked process runs for 10 seconds, timeout is 20 seconds - we expect: - - test to run in under 3 seconds - - no exception should be thrown - - output is not collected - """ - cmd = "{0} -t 10 &".format(process_target) - process = subprocess.Popen(cmd, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=os.environ, - preexec_fn=os.setsid) - - start = datetime.datetime.utcnow() - try: - body = capture_from_process(process, 'sleep 10 &', 20) - except Exception as e: - self.fail('No exception should be thrown for a well behaved process which forks: {0}'.format(e)) - duration = datetime.datetime.utcnow() - start - - self.assertTrue(duration < datetime.timedelta(seconds=3)) - self.assertEqual('[stdout]\ncannot collect stdout\n\n[stderr]\n', body) - - def test_process_bad_pgid(self): - """ - If a timeout is requested but the process is not the root of the process group, raise an exception. - """ - stdout = "stdout\n" - stderr = "stderr\n" - - cmd = process_cmd_template.format(process_target, stdout, stderr) - process = subprocess.Popen(cmd, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=os.environ) - - if sys.version_info < (2, 7): - self.assertRaises(ExtensionError, capture_from_process, process, cmd, 10, EXTENSION_ERROR_CODE) - else: - with self.assertRaises(ExtensionError) as ee: - capture_from_process(process, cmd, 10, EXTENSION_ERROR_CODE) - - body = str(ee.exception) - if sys.version_info >= (3, 2): - self.assertRegex(body, "process group") - else: - self.assertRegexpMatches(body, "process group") - - self.assertEqual(EXTENSION_ERROR_CODE, ee.exception.code) - - -if __name__ == '__main__': - unittest.main() diff -Nru waagent-2.2.34/tests/utils/test_rest_util.py waagent-2.2.45/tests/utils/test_rest_util.py --- waagent-2.2.34/tests/utils/test_rest_util.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/utils/test_rest_util.py 2019-11-07 00:36:56.000000000 +0000 @@ -15,14 +15,10 @@ # Requires Python 2.6+ and Openssl 1.0+ # -from azurelinuxagent.common.exception import HttpError, \ - ResourceGoneError - +from azurelinuxagent.common.exception import HttpError, ResourceGoneError, InvalidContainerError import azurelinuxagent.common.utils.restutil as restutil from azurelinuxagent.common.utils.restutil import HTTP_USER_AGENT - from azurelinuxagent.common.future import httpclient, ustr - from tests.tools import * @@ -241,6 +237,89 @@ self.assertEqual("foo.com", h) self.assertEqual(80, p) + def test_get_no_proxy_with_values_set(self): + no_proxy_list = ["foo.com", "www.google.com"] + with patch.dict(os.environ, { + 'no_proxy': ",".join(no_proxy_list) + }): + no_proxy_from_environment = restutil.get_no_proxy() + + self.assertEquals(len(no_proxy_list), len(no_proxy_from_environment)) + + for i, j in zip(no_proxy_from_environment, no_proxy_list): + self.assertEqual(i, j) + + def test_get_no_proxy_with_incorrect_variable_set(self): + no_proxy_list = ["foo.com", "www.google.com", "", ""] + no_proxy_list_cleaned = [entry for entry in no_proxy_list if entry] + + with patch.dict(os.environ, { + 'no_proxy': ",".join(no_proxy_list) + }): + no_proxy_from_environment = restutil.get_no_proxy() + + self.assertEquals(len(no_proxy_list_cleaned), len(no_proxy_from_environment)) + + for i, j in zip(no_proxy_from_environment, no_proxy_list_cleaned): + print(i, j) + self.assertEqual(i, j) + + def test_get_no_proxy_with_ip_addresses_set(self): + no_proxy_var = "10.0.0.1,10.0.0.2,10.0.0.3,10.0.0.4,10.0.0.5,10.0.0.6,10.0.0.7,10.0.0.8,10.0.0.9,10.0.0.10," + no_proxy_list = ['10.0.0.1', '10.0.0.2', '10.0.0.3', '10.0.0.4', '10.0.0.5', + '10.0.0.6', '10.0.0.7', '10.0.0.8', '10.0.0.9', '10.0.0.10'] + + with patch.dict(os.environ, { + 'no_proxy': no_proxy_var + }): + no_proxy_from_environment = restutil.get_no_proxy() + + self.assertEquals(len(no_proxy_list), len(no_proxy_from_environment)) + + for i, j in zip(no_proxy_from_environment, no_proxy_list): + self.assertEqual(i, j) + + def test_get_no_proxy_default(self): + no_proxy_generator = restutil.get_no_proxy() + self.assertIsNone(no_proxy_generator) + + def test_is_ipv4_address(self): + self.assertTrue(restutil.is_ipv4_address('8.8.8.8')) + self.assertFalse(restutil.is_ipv4_address('localhost.localdomain')) + self.assertFalse(restutil.is_ipv4_address('2001:4860:4860::8888')) # ipv6 tests + + def test_is_valid_cidr(self): + self.assertTrue(restutil.is_valid_cidr('192.168.1.0/24')) + self.assertFalse(restutil.is_valid_cidr('8.8.8.8')) + self.assertFalse(restutil.is_valid_cidr('192.168.1.0/a')) + self.assertFalse(restutil.is_valid_cidr('192.168.1.0/128')) + self.assertFalse(restutil.is_valid_cidr('192.168.1.0/-1')) + self.assertFalse(restutil.is_valid_cidr('192.168.1.999/24')) + + def test_address_in_network(self): + self.assertTrue(restutil.address_in_network('192.168.1.1', '192.168.1.0/24')) + self.assertFalse(restutil.address_in_network('172.16.0.1', '192.168.1.0/24')) + + def test_dotted_netmask(self): + self.assertEquals(restutil.dotted_netmask(0), '0.0.0.0') + self.assertEquals(restutil.dotted_netmask(8), '255.0.0.0') + self.assertEquals(restutil.dotted_netmask(16), '255.255.0.0') + self.assertEquals(restutil.dotted_netmask(24), '255.255.255.0') + self.assertEquals(restutil.dotted_netmask(32), '255.255.255.255') + self.assertRaises(ValueError, restutil.dotted_netmask, 33) + + def test_bypass_proxy(self): + no_proxy_list = ["foo.com", "www.google.com", "168.63.129.16", "Microsoft.com"] + with patch.dict(os.environ, { + 'no_proxy': ",".join(no_proxy_list) + }): + self.assertFalse(restutil.bypass_proxy("http://bar.com")) + self.assertTrue(restutil.bypass_proxy("http://foo.com")) + self.assertTrue(restutil.bypass_proxy("http://168.63.129.16")) + self.assertFalse(restutil.bypass_proxy("http://baz.com")) + self.assertFalse(restutil.bypass_proxy("http://10.1.1.1")) + self.assertTrue(restutil.bypass_proxy("http://www.microsoft.com")) + @patch("azurelinuxagent.common.future.httpclient.HTTPSConnection") @patch("azurelinuxagent.common.future.httpclient.HTTPConnection") def test_http_request_direct(self, HTTPConnection, HTTPSConnection): @@ -311,6 +390,114 @@ self.assertNotEquals(None, resp) self.assertEquals("TheResults", resp.read()) + @patch("azurelinuxagent.common.utils.restutil._get_http_proxy") + @patch("time.sleep") + @patch("azurelinuxagent.common.utils.restutil._http_request") + def test_http_request_proxy_with_no_proxy_check(self, _http_request, sleep, mock_get_http_proxy): + mock_http_resp = MagicMock() + mock_http_resp.read = Mock(return_value="hehe") + _http_request.return_value = mock_http_resp + mock_get_http_proxy.return_value = "host", 1234 # Return a host/port combination + + no_proxy_list = ["foo.com", "www.google.com", "168.63.129.16"] + with patch.dict(os.environ, { + 'no_proxy': ",".join(no_proxy_list) + }): + # Test http get + resp = restutil.http_get("http://foo.com", use_proxy=True) + self.assertEquals("hehe", resp.read()) + self.assertEquals(0, mock_get_http_proxy.call_count) + + # Test http get + resp = restutil.http_get("http://bar.com", use_proxy=True) + self.assertEquals("hehe", resp.read()) + self.assertEquals(1, mock_get_http_proxy.call_count) + + def test_proxy_conditions_with_no_proxy(self): + should_use_proxy = True + should_not_use_proxy = False + use_proxy = True + + no_proxy_list = ["foo.com", "www.google.com", "168.63.129.16"] + with patch.dict(os.environ, { + 'no_proxy': ",".join(no_proxy_list) + }): + host = "10.0.0.1" + self.assertEquals(should_use_proxy, use_proxy and not restutil.bypass_proxy(host)) + + host = "foo.com" + self.assertEquals(should_not_use_proxy, use_proxy and not restutil.bypass_proxy(host)) + + host = "www.google.com" + self.assertEquals(should_not_use_proxy, use_proxy and not restutil.bypass_proxy(host)) + + host = "168.63.129.16" + self.assertEquals(should_not_use_proxy, use_proxy and not restutil.bypass_proxy(host)) + + host = "www.bar.com" + self.assertEquals(should_use_proxy, use_proxy and not restutil.bypass_proxy(host)) + + no_proxy_list = ["10.0.0.1/24"] + with patch.dict(os.environ, { + 'no_proxy': ",".join(no_proxy_list) + }): + host = "www.bar.com" + self.assertEquals(should_use_proxy, use_proxy and not restutil.bypass_proxy(host)) + + host = "10.0.0.1" + self.assertEquals(should_not_use_proxy, use_proxy and not restutil.bypass_proxy(host)) + + host = "10.0.1.1" + self.assertEquals(should_use_proxy, use_proxy and not restutil.bypass_proxy(host)) + + # When No_proxy is empty + with patch.dict(os.environ, { + 'no_proxy': "" + }): + host = "10.0.0.1" + self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) + + host = "foo.com" + self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) + + host = "www.google.com" + self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) + + host = "168.63.129.16" + self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) + + host = "www.bar.com" + self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) + + host = "10.0.0.1" + self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) + + host = "10.0.1.1" + self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) + + # When os.environ is empty - No global variables defined. + with patch.dict(os.environ, {}): + host = "10.0.0.1" + self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) + + host = "foo.com" + self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) + + host = "www.google.com" + self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) + + host = "168.63.129.16" + self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) + + host = "www.bar.com" + self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) + + host = "10.0.0.1" + self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) + + host = "10.0.1.1" + self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) + @patch("azurelinuxagent.common.future.httpclient.HTTPSConnection") @patch("azurelinuxagent.common.future.httpclient.HTTPConnection") def test_http_request_proxy_secure(self, HTTPConnection, HTTPSConnection): @@ -470,6 +657,19 @@ Mock(status=httpclient.BAD_REQUEST, reason='Bad Request', read=read) ] + self.assertRaises(InvalidContainerError, restutil.http_get, "https://foo.bar") + self.assertEqual(1, _http_request.call_count) + + @patch("time.sleep") + @patch("azurelinuxagent.common.utils.restutil._http_request") + def test_http_request_raises_for_invalid_role_configuration(self, _http_request, _sleep): + def read(): + return b'{ "errorCode": "RequestRoleConfigFileNotFound", "message": "Invalid request." }' + + _http_request.side_effect = [ + Mock(status=httpclient.GONE, reason='Resource Gone', read=read) + ] + self.assertRaises(ResourceGoneError, restutil.http_get, "https://foo.bar") self.assertEqual(1, _http_request.call_count) diff -Nru waagent-2.2.34/tests/utils/test_shell_util.py waagent-2.2.45/tests/utils/test_shell_util.py --- waagent-2.2.34/tests/utils/test_shell_util.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/utils/test_shell_util.py 2019-11-07 00:36:56.000000000 +0000 @@ -17,13 +17,36 @@ # from tests.tools import * -import uuid +from azurelinuxagent.common.logger import LogLevel import unittest -import os import azurelinuxagent.common.utils.shellutil as shellutil -import test -class TestrunCmd(AgentTestCase): + +class ShellQuoteTestCase(AgentTestCase): + def test_shellquote(self): + self.assertEqual("\'foo\'", shellutil.quote("foo")) + self.assertEqual("\'foo bar\'", shellutil.quote("foo bar")) + self.assertEqual("'foo'\\''bar'", shellutil.quote("foo\'bar")) + + +class RunTestCase(AgentTestCase): + def test_it_should_return_the_exit_code_of_the_command(self): + exit_code = shellutil.run("exit 123") + self.assertEquals(123, exit_code) + + def test_it_should_be_a_pass_thru_to_run_get_output(self): + with patch.object(shellutil, "run_get_output", return_value=(0, "")) as mock_run_get_output: + shellutil.run("echo hello word!", chk_err=False, expected_errors=[1, 2, 3]) + + self.assertEquals(mock_run_get_output.call_count, 1) + + args, kwargs = mock_run_get_output.call_args + self.assertEquals(args[0], "echo hello word!") + self.assertEquals(kwargs["chk_err"], False) + self.assertEquals(kwargs["expected_errors"], [1, 2, 3]) + + +class RunGetOutputTestCase(AgentTestCase): def test_run_get_output(self): output = shellutil.run_get_output(u"ls /") self.assertNotEquals(None, output) @@ -35,10 +58,150 @@ err = shellutil.run_get_output(u"ls 我") self.assertNotEquals(0, err[0]) - def test_shellquote(self): - self.assertEqual("\'foo\'", shellutil.quote("foo")) - self.assertEqual("\'foo bar\'", shellutil.quote("foo bar")) - self.assertEqual("'foo'\\''bar'", shellutil.quote("foo\'bar")) + def test_it_should_log_the_command(self): + command = "echo hello world!" + + with patch("azurelinuxagent.common.utils.shellutil.logger", autospec=True) as mock_logger: + shellutil.run_get_output(command) + + self.assertEquals(mock_logger.verbose.call_count, 1) + + args, kwargs = mock_logger.verbose.call_args + command_in_message = args[1] + self.assertEqual(command_in_message, command) + + def test_it_should_log_command_failures_as_errors(self): + return_code = 99 + command = "exit {0}".format(return_code) + + with patch("azurelinuxagent.common.utils.shellutil.logger", autospec=True) as mock_logger: + shellutil.run_get_output(command, log_cmd=False) + + self.assertEquals(mock_logger.error.call_count, 1) + + args, kwargs = mock_logger.error.call_args + + message = args[0] # message is similar to "Command: [exit 99], return code: [99], result: []" + self.assertIn("[{0}]".format(command), message) + self.assertIn("[{0}]".format(return_code), message) + + self.assertEquals(mock_logger.verbose.call_count, 0) + self.assertEquals(mock_logger.info.call_count, 0) + self.assertEquals(mock_logger.warn.call_count, 0) + + def test_it_should_log_expected_errors_as_info(self): + return_code = 99 + command = "exit {0}".format(return_code) + + with patch("azurelinuxagent.common.utils.shellutil.logger", autospec=True) as mock_logger: + shellutil.run_get_output(command, log_cmd=False, expected_errors=[return_code]) + + self.assertEquals(mock_logger.info.call_count, 1) + + args, kwargs = mock_logger.info.call_args + + message = args[0] # message is similar to "Command: [exit 99], return code: [99], result: []" + self.assertIn("[{0}]".format(command), message) + self.assertIn("[{0}]".format(return_code), message) + + self.assertEquals(mock_logger.verbose.call_count, 0) + self.assertEquals(mock_logger.warn.call_count, 0) + self.assertEquals(mock_logger.error.call_count, 0) + + def test_it_should_log_unexpected_errors_as_errors(self): + return_code = 99 + command = "exit {0}".format(return_code) + + with patch("azurelinuxagent.common.utils.shellutil.logger", autospec=True) as mock_logger: + shellutil.run_get_output(command, log_cmd=False, expected_errors=[return_code + 1]) + + self.assertEquals(mock_logger.error.call_count, 1) + + args, kwargs = mock_logger.error.call_args + + message = args[0] # message is similar to "Command: [exit 99], return code: [99], result: []" + self.assertIn("[{0}]".format(command), message) + self.assertIn("[{0}]".format(return_code), message) + + self.assertEquals(mock_logger.info.call_count, 0) + self.assertEquals(mock_logger.verbose.call_count, 0) + self.assertEquals(mock_logger.warn.call_count, 0) + + +class RunCommandTestCase(AgentTestCase): + def test_run_command_should_execute_the_command(self): + command = ["echo", "-n", "A TEST STRING"] + ret = shellutil.run_command(command) + self.assertEquals(ret, "A TEST STRING") + + def test_run_command_should_raise_an_exception_when_the_command_fails(self): + command = ["ls", "-d", "/etc", "nonexistent_file"] + + with self.assertRaises(shellutil.CommandError) as context_manager: + shellutil.run_command(command) + + exception = context_manager.exception + self.assertEquals(str(exception), "'ls' failed: 2") + self.assertEquals(exception.stdout, "/etc\n") + self.assertIn("No such file or directory", exception.stderr) + self.assertEquals(exception.returncode, 2) + + def test_run_command_should_raise_an_exception_when_it_cannot_execute_the_command(self): + command = "nonexistent_command" + + with self.assertRaises(Exception) as context_manager: + shellutil.run_command(command) + + exception = context_manager.exception + self.assertIn("No such file or directory", str(exception)) + + def test_run_command_it_should_not_log_by_default(self): + + def assert_no_message_logged(command): + try: + shellutil.run_command(command) + except: + pass + + self.assertEquals(mock_logger.info.call_count, 0) + self.assertEquals(mock_logger.verbose.call_count, 0) + self.assertEquals(mock_logger.warn.call_count, 0) + self.assertEquals(mock_logger.error.call_count, 0) + + assert_no_message_logged(["ls", "nonexistent_file"]) + assert_no_message_logged("nonexistent_command") + + def test_run_command_it_should_log_an_error_when_log_error_is_set(self): + command = ["ls", "-d", "/etc", "nonexistent_file"] + + with patch("azurelinuxagent.common.utils.shellutil.logger.error") as mock_log_error: + try: + shellutil.run_command(command, log_error=True) + except: + pass + + self.assertEquals(mock_log_error.call_count, 1) + + args, kwargs = mock_log_error.call_args + self.assertIn("ls -d /etc nonexistent_file", args, msg="The command was not logged") + self.assertIn(2, args, msg="The command's return code was not logged") + self.assertIn("/etc\n", args, msg="The command's stdout was not logged") + self.assertTrue(any("No such file or directory" in str(a) for a in args), msg="The command's stderr was not logged") + + command = "nonexistent_command" + + with patch("azurelinuxagent.common.utils.shellutil.logger.error") as mock_log_error: + try: + shellutil.run_command(command, log_error=True) + except: + pass + + self.assertEquals(mock_log_error.call_count, 1) + + args, kwargs = mock_log_error.call_args + self.assertIn(command, args, msg="The command was not logged") + self.assertTrue(any("No such file or directory" in str(a) for a in args), msg="The command's stderr was not logged") + if __name__ == '__main__': unittest.main() diff -Nru waagent-2.2.34/tests/utils/test_text_util.py waagent-2.2.45/tests/utils/test_text_util.py --- waagent-2.2.34/tests/utils/test_text_util.py 2018-11-22 09:43:55.000000000 +0000 +++ waagent-2.2.45/tests/utils/test_text_util.py 2019-11-07 00:36:56.000000000 +0000 @@ -194,6 +194,18 @@ self.assertNotEqual(textutil.is_str_none_or_whitespace(hex_null_1), textutil.is_str_empty(hex_null_1)) self.assertNotEqual(textutil.is_str_none_or_whitespace(hex_null_2), textutil.is_str_empty(hex_null_2)) + def test_format_memory_value(self): + """ + Test formatting of memory amounts into human-readable units + """ + self.assertEqual(2048, textutil.format_memory_value('kilobytes', 2)) + self.assertEqual(0, textutil.format_memory_value('kilobytes', 0)) + self.assertEqual(2048000, textutil.format_memory_value('kilobytes', 2000)) + self.assertEqual(2048 * 1024, textutil.format_memory_value('megabytes', 2)) + self.assertEqual((1024 + 512) * 1024 * 1024, textutil.format_memory_value('gigabytes', 1.5)) + self.assertRaises(ValueError, textutil.format_memory_value, 'KiloBytes', 1) + self.assertRaises(TypeError, textutil.format_memory_value, 'bytes', None) + if __name__ == '__main__': unittest.main()