Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions doc/install_troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,30 @@ This can be resolved by installing the CLI in a higher directory to prevent reac
See [#1221](https://github.com/Azure/azure-cli/issues/1221#issuecomment-258290204)


Windows - CLI is blocked by Device Guard / WDAC (Windows Defender Application Control) after `az upgrade`
-----------------------------------------------------------------------------------------------------------

On machines that enforce Device Guard / Windows Defender Application Control (WDAC) or similar code
integrity policies (for example, Azure Local/Azure Stack HCI cluster nodes), the bundled `python.exe`
shipped with the Azure CLI MSI may be blocked from running if it doesn't meet the signing requirements
configured by the enforced policy. This can make a previously working Azure CLI installation unusable
immediately after running `az upgrade`.

If you observe Windows Code Integrity events (such as Event ID 3033 or 3077) referencing
`Microsoft SDKs\Azure\CLI2\python.exe` after an upgrade, or `az version`/`az` fails to start with a message
like `'...\python.exe' was blocked by your organization's Device Guard policy`:

* Reinstall the previous, working Azure CLI MSI to restore functionality. Installers for previous versions
are available at https://learn.microsoft.com/cli/azure/release-notes-azure-cli.
* On machines with enforced code integrity policies, test `az upgrade` in a non-production/staging
environment first, and confirm `az version` still runs successfully before rolling the upgrade out more
broadly (for example, across an Azure Local cluster).
* Contact your policy administrator to allow the new binary, or update the enforced policy, if you need to
use the newer version.

See [#33919](https://github.com/Azure/azure-cli/issues/33919)


Ubuntu 12.04 LTS - Known warning
--------------------------------

Expand Down
39 changes: 36 additions & 3 deletions src/azure-cli/azure/cli/command_modules/util/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ def _upgrade_on_windows():

from azure.cli.core.util import rmtree_with_retry

# MSI exit codes that indicate a successful install (3010/1641 = success, restart required).
_MSI_SUCCESS_CODES = {0, 1641, 3010}

if platform.architecture()[0] == '32bit':
msi_url = 'https://aka.ms/installazurecliwindows'
else:
Expand All @@ -231,9 +234,39 @@ def _upgrade_on_windows():

msi_path = _download_from_url(msi_url, msi_dir)

subprocess.Popen(['msiexec.exe', '/i', msi_path])
logger.warning("Installation started. Please complete the upgrade in the opened window.\nTo update extensions, "
"please run `az upgrade` again after completing the upgrade.")
# Run msiexec in passive mode (progress bar, no user interaction) and wait for completion.
# This allows us to verify the new installation afterwards.
logger.warning("Installing Azure CLI MSI. A progress window will appear — please wait for it to finish.")
result = subprocess.run(['msiexec.exe', '/i', msi_path, '/passive'], check=False)

if result.returncode not in _MSI_SUCCESS_CODES:
logger.warning(
"MSI installation failed (exit code %d). The MSI file is saved at '%s'. "
"You can install it manually or re-run `az upgrade`.",
result.returncode, msi_path)
sys.exit(result.returncode)

if result.returncode in (1641, 3010):
logger.warning("The upgrade was applied but a system restart is required before the new CLI is active.")

# Verify that the new installation can be launched. On machines enforcing Device Guard /
# Windows Defender Application Control (WDAC) policies the newly installed python.exe may be
# blocked, leaving the CLI unusable even though the MSI reported success.
try:
subprocess.check_output('az version -o json', shell=True, timeout=30)
logger.warning("Upgrade finished. Run `az upgrade` again to update any installed extensions.")
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError, OSError):
logger.warning(
"The MSI was installed successfully, but running 'az version' failed. "
"This likely means that Device Guard / Windows Defender Application Control (WDAC) "
"or a similar code integrity policy is blocking the newly installed CLI binary.\n"
"To restore the previous working CLI, reinstall the previous MSI version.\n"
"The new MSI is still available at '%s' if you need to retry.\n"
"See https://github.com/Azure/azure-cli/blob/dev/doc/install_troubleshooting.md "
"for troubleshooting steps.",
msi_path)
sys.exit(1)

sys.exit(0)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import subprocess
import unittest
from unittest import mock

from azure.cli.command_modules.util.custom import _upgrade_on_windows

_MSI_PATH = 'C:\\temp\\azure-cli-msi\\azure-cli.msi'


class UpgradeOnWindowsTest(unittest.TestCase):

# ------------------------------------------------------------------
# successful upgrade — az version passes
# ------------------------------------------------------------------

def test_successful_upgrade_exits_zero(self):
"""When msiexec succeeds and az version passes, exit code is 0."""
with mock.patch('platform.architecture', return_value=('64bit', '')), \
mock.patch('azure.cli.command_modules.util.custom._download_from_url',
return_value=_MSI_PATH), \
mock.patch('azure.cli.core.util.rmtree_with_retry'), \
mock.patch('azure.cli.command_modules.util.custom.logger'), \
mock.patch('subprocess.run',
return_value=mock.Mock(returncode=0)) as run_mock, \
mock.patch('subprocess.check_output', return_value=b'{}'):
with self.assertRaises(SystemExit) as cm:
_upgrade_on_windows()

self.assertEqual(cm.exception.code, 0)
run_mock.assert_called_once_with(
['msiexec.exe', '/i', _MSI_PATH, '/passive'], check=False)

# ------------------------------------------------------------------
# WDAC blocking — az version fails after successful msiexec
# ------------------------------------------------------------------

def test_wdac_blocking_exits_nonzero_and_warns(self):
"""When msiexec succeeds but az version fails, exit 1 and warn about WDAC."""
with mock.patch('platform.architecture', return_value=('64bit', '')), \
mock.patch('azure.cli.command_modules.util.custom._download_from_url',
return_value=_MSI_PATH), \
mock.patch('azure.cli.core.util.rmtree_with_retry'), \
mock.patch('azure.cli.command_modules.util.custom.logger') as logger_mock, \
mock.patch('subprocess.run',
return_value=mock.Mock(returncode=0)), \
mock.patch('subprocess.check_output',
side_effect=subprocess.CalledProcessError(1, 'az version -o json')):
with self.assertRaises(SystemExit) as cm:
_upgrade_on_windows()

self.assertEqual(cm.exception.code, 1)
warning_messages = [call_args[0][0] for call_args in logger_mock.warning.call_args_list]
self.assertTrue(
any('Device Guard' in m and 'WDAC' in m for m in warning_messages),
"Expected a WDAC/Device Guard warning; got: {}".format(warning_messages))

# ------------------------------------------------------------------
# msiexec failure
# ------------------------------------------------------------------

def test_msiexec_failure_exits_with_msi_exit_code(self):
"""When msiexec returns a non-success exit code, we exit with that code."""
with mock.patch('platform.architecture', return_value=('64bit', '')), \
mock.patch('azure.cli.command_modules.util.custom._download_from_url',
return_value=_MSI_PATH), \
mock.patch('azure.cli.core.util.rmtree_with_retry'), \
mock.patch('azure.cli.command_modules.util.custom.logger'), \
mock.patch('subprocess.run',
return_value=mock.Mock(returncode=1603)):
with self.assertRaises(SystemExit) as cm:
_upgrade_on_windows()

self.assertEqual(cm.exception.code, 1603)

# ------------------------------------------------------------------
# restart-required exit codes (1641 / 3010)
# ------------------------------------------------------------------

def test_restart_required_warns_and_exits_zero(self):
"""When msiexec returns 3010 (restart required) and az version succeeds, exit 0."""
with mock.patch('platform.architecture', return_value=('64bit', '')), \
mock.patch('azure.cli.command_modules.util.custom._download_from_url',
return_value=_MSI_PATH), \
mock.patch('azure.cli.core.util.rmtree_with_retry'), \
mock.patch('azure.cli.command_modules.util.custom.logger') as logger_mock, \
mock.patch('subprocess.run',
return_value=mock.Mock(returncode=3010)), \
mock.patch('subprocess.check_output', return_value=b'{}'):
with self.assertRaises(SystemExit) as cm:
_upgrade_on_windows()

self.assertEqual(cm.exception.code, 0)
warning_messages = [call_args[0][0] for call_args in logger_mock.warning.call_args_list]
self.assertTrue(
any('restart' in m.lower() for m in warning_messages),
"Expected a restart warning for exit code 3010; got: {}".format(warning_messages))

# ------------------------------------------------------------------
# 32-bit architecture uses 32-bit MSI URL
# ------------------------------------------------------------------

def test_32bit_uses_correct_msi_url(self):
"""On a 32-bit architecture the 32-bit MSI URL is used."""
with mock.patch('platform.architecture', return_value=('32bit', '')), \
mock.patch('azure.cli.command_modules.util.custom._download_from_url',
return_value=_MSI_PATH) as download_mock, \
mock.patch('azure.cli.core.util.rmtree_with_retry'), \
mock.patch('azure.cli.command_modules.util.custom.logger'), \
mock.patch('subprocess.run',
return_value=mock.Mock(returncode=0)), \
mock.patch('subprocess.check_output', return_value=b'{}'):
with self.assertRaises(SystemExit):
_upgrade_on_windows()

url_used = download_mock.call_args[0][0]
self.assertIn('installazurecliwindows', url_used)
self.assertNotIn('x64', url_used)


if __name__ == '__main__':
unittest.main()
Loading