From cceec06bdf07a0f138b0c12414ce0ecc9bd5ae35 Mon Sep 17 00:00:00 2001 From: Mansoor Sarfraz Date: Fri, 31 Jul 2026 16:43:41 +1000 Subject: [PATCH 1/9] azure linux image updated to azl4 --- azure-pipelines.yml | 24 +++++++++++----------- scripts/release/rpm/README.md | 2 +- scripts/release/rpm/azurelinux.dockerfile | 12 +++++------ scripts/release/rpm/pipeline_azurelinux.sh | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 18fedf3969b..c7c4997059f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -352,12 +352,12 @@ jobs: strategy: matrix: ${{ each arch in parameters.architectures }}: - Azure Linux 3.0 ${{ arch.name }}: + Azure Linux 4.0 ${{ arch.name }}: pool: ${{ arch.pool }} - artifactName: docker-azurelinux3.0-${{ arch.value }} + artifactName: docker-azurelinux4.0-${{ arch.value }} dockerfile: azure-linux.dockerfile - packageArtifactName: rpm-azurelinux3.0-${{ arch.value }} - image: mcr.microsoft.com/azurelinux/base/core:3.0 + packageArtifactName: rpm-azurelinux4.0-${{ arch.value }} + image: mcr.microsoft.com/azurelinux-beta/base/core:4.0 pool: name: $(pool) steps: @@ -396,9 +396,9 @@ jobs: strategy: matrix: ${{ each arch in parameters.architectures }}: - Azure Linux 3.0 ${{ arch.name }}: + Azure Linux 4.0 ${{ arch.name }}: pool: ${{ arch.pool }} - artifactName: docker-azurelinux3.0-${{ arch.value }} + artifactName: docker-azurelinux4.0-${{ arch.value }} pool: name: $(pool) steps: @@ -776,9 +776,9 @@ jobs: strategy: matrix: ${{ each arch in parameters.architectures }}: - Azure Linux 3.0 ${{ arch.name }}: - image: mcr.microsoft.com/azurelinux/base/core:3.0 - artifact: rpm-azurelinux3.0-${{ arch.value }} + Azure Linux 4.0 ${{ arch.name }}: + image: mcr.microsoft.com/azurelinux-beta/base/core:4.0 + artifact: rpm-azurelinux4.0-${{ arch.value }} pool: ${{ arch.pool }} pool: name: $(pool) @@ -822,9 +822,9 @@ jobs: strategy: matrix: ${{ each arch in parameters.architectures }}: - Azure Linux 3.0 ${{ arch.name }}: - image: mcr.microsoft.com/azurelinux/base/core:3.0 - artifact: rpm-azurelinux3.0-${{ arch.value }} + Azure Linux 4.0 ${{ arch.name }}: + image: mcr.microsoft.com/azurelinux-beta/base/core:4.0 + artifact: rpm-azurelinux4.0-${{ arch.value }} pool: ${{ arch.pool }} steps: - task: DownloadPipelineArtifact@1 diff --git a/scripts/release/rpm/README.md b/scripts/release/rpm/README.md index 9e263d17140..09388af5301 100644 --- a/scripts/release/rpm/README.md +++ b/scripts/release/rpm/README.md @@ -36,7 +36,7 @@ docker run azure/azure-cli:fedora29-builder cat /root/rpmbuild/RPMS/x86_64/azure _Azure Linux:_ ```bash -docker run azure/azure-cli:azurelinux-builder cat /usr/src/azl/RPMS/x86_64/azure-cli-dev-1.azl3.x86_64.rpm > ./bin/azure-cli-dev-1.azl3.x86_64.rpm +docker run azure/azure-cli:azurelinux-builder cat /root/rpmbuild/RPMS/x86_64/azure-cli-dev-1.azl4.x86_64.rpm > ./bin/azure-cli-dev-1.azl4.x86_64.rpm ``` This launches a container running from the image built and tagged by the previous command, prints the contents of the diff --git a/scripts/release/rpm/azurelinux.dockerfile b/scripts/release/rpm/azurelinux.dockerfile index fba3af87de6..d42138c77a9 100644 --- a/scripts/release/rpm/azurelinux.dockerfile +++ b/scripts/release/rpm/azurelinux.dockerfile @@ -1,4 +1,4 @@ -ARG image=mcr.microsoft.com/azurelinux/base/core:3.0 +ARG image=mcr.microsoft.com/azurelinux-beta/base/core:4.0 FROM ${image} AS build-env ARG cli_version=dev @@ -16,15 +16,15 @@ WORKDIR /azure-cli COPY . . -# Azure Linux 3's python3 is 3.12, the rpm paths are -# /usr/src/azl/RPMS/x86_64/azure-cli-2.63.0-1.azl3.x86_64.rpm -# /usr/src/azl/RPMS/aarch64/azure-cli-2.63.0-1.azl3.aarch64.rpm +# Azure Linux 4's python3 is 3.12, the rpm paths are +# /root/rpmbuild/RPMS/x86_64/azure-cli-2.63.0-1.azl4.x86_64.rpm +# /root/rpmbuild/RPMS/aarch64/azure-cli-2.63.0-1.azl4.aarch64.rpm RUN --mount=type=secret,id=PIP_INDEX_URL export PIP_INDEX_URL=$(cat /run/secrets/PIP_INDEX_URL) && \ dos2unix ./scripts/release/rpm/azure-cli.spec && \ REPO_PATH=$(pwd) CLI_VERSION=$cli_version PYTHON_PACKAGE=python3 PYTHON_CMD=python3 \ rpmbuild -v -bb --clean scripts/release/rpm/azure-cli.spec && \ - cp /usr/src/*/RPMS/*/azure-cli-${cli_version}-1.*.rpm /azure-cli-dev.rpm && \ - mkdir /out && cp /usr/src/*/RPMS/*/azure-cli-${cli_version}-1.*.rpm /out/ + cp /root/rpmbuild/RPMS/*/azure-cli-${cli_version}-1.*.rpm /azure-cli-dev.rpm && \ + mkdir /out && cp /root/rpmbuild/RPMS/*/azure-cli-${cli_version}-1.*.rpm /out/ FROM ${image} AS execution-env diff --git a/scripts/release/rpm/pipeline_azurelinux.sh b/scripts/release/rpm/pipeline_azurelinux.sh index 09bffff50b2..79223952d91 100644 --- a/scripts/release/rpm/pipeline_azurelinux.sh +++ b/scripts/release/rpm/pipeline_azurelinux.sh @@ -6,7 +6,7 @@ set -exv : "${BUILD_STAGINGDIRECTORY:?BUILD_STAGINGDIRECTORY environment variable not set.}" -# IMAGE should be Azure Linux docker image url, such as mcr.microsoft.com/azurelinux/base/core:3.0 +# IMAGE should be Azure Linux docker image url, such as mcr.microsoft.com/azurelinux-beta/base/core:4.0 : "${IMAGE:?IMAGE environment variable not set.}" CLI_VERSION=`cat src/azure-cli/azure/cli/__main__.py | grep __version__ | sed s/' '//g | sed s/'__version__='// | sed s/\"//g` From 62cbc83357ca1404757e00f2c84f065bd8c34352 Mon Sep 17 00:00:00 2001 From: Mansoor Sarfraz Date: Fri, 31 Jul 2026 17:01:31 +1000 Subject: [PATCH 2/9] yml indentation corrected --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c7c4997059f..a29a0e69c37 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -777,7 +777,7 @@ jobs: matrix: ${{ each arch in parameters.architectures }}: Azure Linux 4.0 ${{ arch.name }}: - image: mcr.microsoft.com/azurelinux-beta/base/core:4.0 + image: mcr.microsoft.com/azurelinux-beta/base/core:4.0 artifact: rpm-azurelinux4.0-${{ arch.value }} pool: ${{ arch.pool }} pool: From 58a7a635bcb9a707cd90ab5e9c3856d2acecb012 Mon Sep 17 00:00:00 2001 From: Mansoor Sarfraz Date: Fri, 7 Aug 2026 12:17:44 +1000 Subject: [PATCH 3/9] added support for AZL3 and AZL4 based containers and rpm packages --- azure-pipelines.yml | 60 ++++++++++++------- scripts/release/rpm/README.md | 9 +++ scripts/release/rpm/azurelinux.dockerfile | 18 ++++-- scripts/release/rpm/pipeline_azurelinux.sh | 5 +- .../azure/cli/core/extension/operations.py | 21 ++++++- .../tests/latest/test_extension_commands.py | 39 +++++++++++- 6 files changed, 122 insertions(+), 30 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a29a0e69c37..923729483fd 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -38,6 +38,20 @@ parameters: value: arm64 pool: pool-ubuntu-latest-arm64 +# Azure Linux versions to build in parallel. AZL3 is the current stable release; +# AZL4 is being onboarded (currently beta) to pick up the patched OpenSSL and +# resolve the S360 vulnerability. Both are built and published side-by-side +# until AZL4 reaches GA and AZL3 is deprecated. +- name: azureLinuxVersions + type: object + default: + - name: '3.0' + tag: '3.0' + image: mcr.microsoft.com/azurelinux/base/core:3.0 + - name: '4.0 (Beta)' + tag: '4.0' + image: mcr.microsoft.com/azurelinux-beta/base/core:4.0 + jobs: - job: CheckPullRequest displayName: "Check the Format of Pull Request Title and Content" @@ -351,13 +365,14 @@ jobs: dependsOn: BuildRpmPackagesAzureLinux strategy: matrix: - ${{ each arch in parameters.architectures }}: - Azure Linux 4.0 ${{ arch.name }}: - pool: ${{ arch.pool }} - artifactName: docker-azurelinux4.0-${{ arch.value }} - dockerfile: azure-linux.dockerfile - packageArtifactName: rpm-azurelinux4.0-${{ arch.value }} - image: mcr.microsoft.com/azurelinux-beta/base/core:4.0 + ${{ each azl in parameters.azureLinuxVersions }}: + ${{ each arch in parameters.architectures }}: + Azure Linux ${{ azl.name }} ${{ arch.name }}: + pool: ${{ arch.pool }} + artifactName: docker-azurelinux${{ azl.tag }}-${{ arch.value }} + dockerfile: azure-linux.dockerfile + packageArtifactName: rpm-azurelinux${{ azl.tag }}-${{ arch.value }} + image: ${{ azl.image }} pool: name: $(pool) steps: @@ -395,10 +410,11 @@ jobs: - ExtractMetadata strategy: matrix: - ${{ each arch in parameters.architectures }}: - Azure Linux 4.0 ${{ arch.name }}: - pool: ${{ arch.pool }} - artifactName: docker-azurelinux4.0-${{ arch.value }} + ${{ each azl in parameters.azureLinuxVersions }}: + ${{ each arch in parameters.architectures }}: + Azure Linux ${{ azl.name }} ${{ arch.name }}: + pool: ${{ arch.pool }} + artifactName: docker-azurelinux${{ azl.tag }}-${{ arch.value }} pool: name: $(pool) steps: @@ -775,11 +791,12 @@ jobs: condition: and(succeeded(), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'Manual', 'Schedule')) strategy: matrix: - ${{ each arch in parameters.architectures }}: - Azure Linux 4.0 ${{ arch.name }}: - image: mcr.microsoft.com/azurelinux-beta/base/core:4.0 - artifact: rpm-azurelinux4.0-${{ arch.value }} - pool: ${{ arch.pool }} + ${{ each azl in parameters.azureLinuxVersions }}: + ${{ each arch in parameters.architectures }}: + Azure Linux ${{ azl.name }} ${{ arch.name }}: + image: ${{ azl.image }} + artifact: rpm-azurelinux${{ azl.tag }}-${{ arch.value }} + pool: ${{ arch.pool }} pool: name: $(pool) steps: @@ -821,11 +838,12 @@ jobs: name: $(pool) strategy: matrix: - ${{ each arch in parameters.architectures }}: - Azure Linux 4.0 ${{ arch.name }}: - image: mcr.microsoft.com/azurelinux-beta/base/core:4.0 - artifact: rpm-azurelinux4.0-${{ arch.value }} - pool: ${{ arch.pool }} + ${{ each azl in parameters.azureLinuxVersions }}: + ${{ each arch in parameters.architectures }}: + Azure Linux ${{ azl.name }} ${{ arch.name }}: + image: ${{ azl.image }} + artifact: rpm-azurelinux${{ azl.tag }}-${{ arch.value }} + pool: ${{ arch.pool }} steps: - task: DownloadPipelineArtifact@1 displayName: 'Download Metadata' diff --git a/scripts/release/rpm/README.md b/scripts/release/rpm/README.md index 09388af5301..b5d5dee0cf1 100644 --- a/scripts/release/rpm/README.md +++ b/scripts/release/rpm/README.md @@ -17,7 +17,12 @@ docker build --target build-env -f ./scripts/release/rpm/fedora.dockerfile -t az _Azure Linux:_ ```bash +# Build against Azure Linux 4.0 (Beta) - the default in the dockerfile docker build --target build-env -f ./scripts/release/rpm/azurelinux.dockerfile -t azure/azure-cli:azurelinux-builder . + +# Or build against Azure Linux 3.0 explicitly +docker build --target build-env --build-arg image=mcr.microsoft.com/azurelinux/base/core:3.0 \ + -f ./scripts/release/rpm/azurelinux.dockerfile -t azure/azure-cli:azurelinux3-builder . ``` After several minutes, this will have created a Docker image named `azure/azure-cli:centos7-builder` containing an @@ -36,7 +41,11 @@ docker run azure/azure-cli:fedora29-builder cat /root/rpmbuild/RPMS/x86_64/azure _Azure Linux:_ ```bash +# AZL4 output path (rpmbuild default topdir is /root/rpmbuild) docker run azure/azure-cli:azurelinux-builder cat /root/rpmbuild/RPMS/x86_64/azure-cli-dev-1.azl4.x86_64.rpm > ./bin/azure-cli-dev-1.azl4.x86_64.rpm + +# AZL3 output path (rpmbuild default topdir is /usr/src/azl) +docker run azure/azure-cli:azurelinux3-builder cat /usr/src/azl/RPMS/x86_64/azure-cli-dev-1.azl3.x86_64.rpm > ./bin/azure-cli-dev-1.azl3.x86_64.rpm ``` This launches a container running from the image built and tagged by the previous command, prints the contents of the diff --git a/scripts/release/rpm/azurelinux.dockerfile b/scripts/release/rpm/azurelinux.dockerfile index d42138c77a9..b75d6920400 100644 --- a/scripts/release/rpm/azurelinux.dockerfile +++ b/scripts/release/rpm/azurelinux.dockerfile @@ -1,3 +1,5 @@ +# Default is Azure Linux 4.0 (Beta), but this dockerfile is shared with Azure Linux 3.0 +# builds too -- the CI pipeline always overrides this with --build-arg image=. ARG image=mcr.microsoft.com/azurelinux-beta/base/core:4.0 FROM ${image} AS build-env @@ -16,15 +18,21 @@ WORKDIR /azure-cli COPY . . -# Azure Linux 4's python3 is 3.12, the rpm paths are -# /root/rpmbuild/RPMS/x86_64/azure-cli-2.63.0-1.azl4.x86_64.rpm -# /root/rpmbuild/RPMS/aarch64/azure-cli-2.63.0-1.azl4.aarch64.rpm +# This dockerfile is shared by both Azure Linux 3.0 and Azure Linux 4.0 (Beta) builds +# (the ${image} build-arg selects which base image is used). The two base images use +# different rpmbuild topdir defaults, so the built RPM ends up in different locations: +# AZL3: /usr/src/azl/RPMS/x86_64/azure-cli-2.63.0-1.azl3.x86_64.rpm +# AZL4: /root/rpmbuild/RPMS/x86_64/azure-cli-2.63.0-1.azl4.x86_64.rpm +# Use `find` so the same RUN step works for either base image without needing to know +# which topdir it used. RUN --mount=type=secret,id=PIP_INDEX_URL export PIP_INDEX_URL=$(cat /run/secrets/PIP_INDEX_URL) && \ dos2unix ./scripts/release/rpm/azure-cli.spec && \ REPO_PATH=$(pwd) CLI_VERSION=$cli_version PYTHON_PACKAGE=python3 PYTHON_CMD=python3 \ rpmbuild -v -bb --clean scripts/release/rpm/azure-cli.spec && \ - cp /root/rpmbuild/RPMS/*/azure-cli-${cli_version}-1.*.rpm /azure-cli-dev.rpm && \ - mkdir /out && cp /root/rpmbuild/RPMS/*/azure-cli-${cli_version}-1.*.rpm /out/ + RPM_PATH=$(find / -xdev -type f -name "azure-cli-${cli_version}-1.*.rpm" 2>/dev/null | head -n 1) && \ + test -n "$RPM_PATH" && \ + cp "$RPM_PATH" /azure-cli-dev.rpm && \ + mkdir /out && cp "$RPM_PATH" /out/ FROM ${image} AS execution-env diff --git a/scripts/release/rpm/pipeline_azurelinux.sh b/scripts/release/rpm/pipeline_azurelinux.sh index 79223952d91..e3eb6d7fe8d 100644 --- a/scripts/release/rpm/pipeline_azurelinux.sh +++ b/scripts/release/rpm/pipeline_azurelinux.sh @@ -6,7 +6,10 @@ set -exv : "${BUILD_STAGINGDIRECTORY:?BUILD_STAGINGDIRECTORY environment variable not set.}" -# IMAGE should be Azure Linux docker image url, such as mcr.microsoft.com/azurelinux-beta/base/core:4.0 +# IMAGE should be Azure Linux docker image url. The CI pipeline runs this script once per +# matrix row (Azure Linux 3.0 and Azure Linux 4.0 Beta, each x86_64/aarch64), passing in the +# appropriate image, e.g. mcr.microsoft.com/azurelinux/base/core:3.0 or +# mcr.microsoft.com/azurelinux-beta/base/core:4.0 : "${IMAGE:?IMAGE environment variable not set.}" CLI_VERSION=`cat src/azure-cli/azure/cli/__main__.py | grep __version__ | sed s/' '//g | sed s/'__version__='// | sed s/\"//g` diff --git a/src/azure-cli-core/azure/cli/core/extension/operations.py b/src/azure-cli-core/azure/cli/core/extension/operations.py index e20aed58b03..0d39b3cc1e4 100644 --- a/src/azure-cli-core/azure/cli/core/extension/operations.py +++ b/src/azure-cli-core/azure/cli/core/extension/operations.py @@ -215,8 +215,9 @@ def _install_deps_for_psycopg2(): # pylint: disable=too-many-statements '/usr/local/opt/openssl/lib/' ]) elif system == 'Linux': - distname, _ = get_linux_distro() + distname, distversion = get_linux_distro() distname = distname.lower().strip() + is_azure_linux_4 = 'azure linux' in distname and distversion.startswith('4.') if installer == 'DEB' or any(x in distname for x in ['ubuntu', 'debian']): exit_code = subprocess.call(['dpkg', '-s', 'gcc', 'libpq-dev', 'python3-dev'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if exit_code != 0: @@ -230,7 +231,7 @@ def _install_deps_for_psycopg2(): # pylint: disable=too-many-statements if exit_code == 0: logger.debug("Install dependencies with '%s'", " ".join(apt_install_cmd)) subprocess.call(apt_install_cmd, True) - elif installer == 'RPM' or any(x in distname for x in ['centos', 'rhel', 'red hat', 'fedora', 'opensuse', 'suse', 'sles']): + elif installer == 'RPM' or any(x in distname for x in ['centos', 'rhel', 'red hat', 'fedora', 'opensuse', 'suse', 'sles']) or is_azure_linux_4: if any(x in distname for x in ['centos', 'rhel', 'red hat', 'fedora']): yum_install_cmd = 'yum install -y gcc postgresql-devel python3-devel'.split() if os.geteuid() != 0: # pylint: disable=no-member @@ -249,6 +250,22 @@ def _install_deps_for_psycopg2(): # pylint: disable=too-many-statements if exit_code == 0: logger.debug("Install dependencies with '%s'", " ".join(zypper_install_cmd)) subprocess.call(zypper_install_cmd) + elif is_azure_linux_4: + rpm_packages = ['gcc', 'libpq-devel', 'python3-devel', 'binutils', 'glibc-devel', 'kernel-headers'] + rpm_install_cmd = ['tdnf', 'install', '-y'] + rpm_packages + if os.geteuid() != 0: # pylint: disable=no-member + rpm_install_cmd.insert(0, 'sudo') + logger.debug("Install dependencies with '%s'", " ".join(rpm_install_cmd)) + logger.warning( + 'This extension depends on %s and they will be installed first if not exist.', + ' '.join(rpm_packages) + ) + exit_code = subprocess.call(rpm_install_cmd) + if exit_code != 0: + raise CLIError( + 'Failed to install required system dependencies for psycopg2: {}.' + .format(' '.join(rpm_packages)) + ) elif installer == 'DOCKER' or any(x in distname for x in ['alpine linux']): apk_install_cmd = 'apk add --no-cache libpq-dev'.split() logger.debug("Install dependencies with '%s'", " ".join(apk_install_cmd)) diff --git a/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py b/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py index 2f81268935d..5a1395ef63f 100644 --- a/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py +++ b/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py @@ -14,7 +14,8 @@ from azure.cli.core.extension import get_extension, build_extension_path from azure.cli.core.extension.operations import (add_extension_to_path, list_extensions, add_extension, show_extension, remove_extension, update_extension, - list_available_extensions, OUT_KEY_NAME, OUT_KEY_VERSION, + list_available_extensions, _install_deps_for_psycopg2, + OUT_KEY_NAME, OUT_KEY_VERSION, OUT_KEY_METADATA, OUT_KEY_PATH) from azure.cli.core.extension._resolve import NoExtensionCandidatesError from azure.cli.core.mock import DummyCli @@ -481,6 +482,42 @@ def test_add_extension_azure_to_path(self): self.assertSequenceEqual(old_path_1, list(azure.__path__)) self.assertSequenceEqual(old_path_2, list(azure.mgmt.__path__)) + def test_install_psycopg2_deps_uses_azl4_packages(self): + with mock.patch('azure.cli.core.util.in_cloud_console', return_value=False), \ + mock.patch('platform.system', return_value='Linux'), \ + mock.patch('azure.cli.core.util.get_linux_distro', return_value=('Azure Linux', '4.0')), \ + mock.patch.dict('os.environ', {'AZ_INSTALLER': 'RPM'}), \ + mock.patch('shutil.which', side_effect=lambda cmd: '/usr/bin/tdnf' if cmd == 'tdnf' else None), \ + mock.patch('os.geteuid', return_value=0, create=True), \ + mock.patch('subprocess.call', return_value=0) as subprocess_call: + _install_deps_for_psycopg2() + + azure_linux_packages = ['gcc', 'libpq-devel', 'python3-devel', 'binutils', 'glibc-devel', 'kernel-headers'] + subprocess_call.assert_any_call(['tdnf', 'install', '-y'] + azure_linux_packages) + self.assertNotIn(mock.call(['yum', 'install', '-y'] + azure_linux_packages), + subprocess_call.call_args_list) + + def test_install_psycopg2_deps_preserves_azl3_rpm_behavior(self): + with mock.patch('azure.cli.core.util.in_cloud_console', return_value=False), \ + mock.patch('platform.system', return_value='Linux'), \ + mock.patch('azure.cli.core.util.get_linux_distro', return_value=('Azure Linux', '3.0')), \ + mock.patch.dict('os.environ', {'AZ_INSTALLER': 'RPM'}), \ + mock.patch('subprocess.call') as subprocess_call: + _install_deps_for_psycopg2() + + subprocess_call.assert_not_called() + + def test_install_psycopg2_deps_reports_rpm_install_failure(self): + with mock.patch('azure.cli.core.util.in_cloud_console', return_value=False), \ + mock.patch('platform.system', return_value='Linux'), \ + mock.patch('azure.cli.core.util.get_linux_distro', return_value=('Azure Linux', '4.0')), \ + mock.patch.dict('os.environ', {'AZ_INSTALLER': 'RPM'}), \ + mock.patch('shutil.which', side_effect=lambda cmd: '/usr/bin/tdnf' if cmd == 'tdnf' else None), \ + mock.patch('os.geteuid', return_value=0, create=True), \ + mock.patch('subprocess.call', return_value=1): + with self.assertRaisesRegex(CLIError, 'Failed to install required system dependencies for psycopg2'): + _install_deps_for_psycopg2() + def _setup_cmd(self): cmd = mock.MagicMock() cmd.cli_ctx = DummyCli() From 01045a210cf7f8616600cdb09d0b6a5192e1ffe4 Mon Sep 17 00:00:00 2001 From: Mansoor Sarfraz Date: Fri, 7 Aug 2026 16:05:50 +1000 Subject: [PATCH 4/9] fixed pylint warning --- src/azure-cli-core/azure/cli/core/extension/operations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/extension/operations.py b/src/azure-cli-core/azure/cli/core/extension/operations.py index 0d39b3cc1e4..c52b9ed8f97 100644 --- a/src/azure-cli-core/azure/cli/core/extension/operations.py +++ b/src/azure-cli-core/azure/cli/core/extension/operations.py @@ -180,7 +180,7 @@ def _add_whl_ext(cli_ctx, source, ext_sha256=None, pip_extra_index_urls=None, pi return extension_name -def _install_deps_for_psycopg2(): # pylint: disable=too-many-statements +def _install_deps_for_psycopg2(): # pylint: disable=too-many-statements, too-many-branches # If we are in Cloud Shell, dependencies should have already been installed. from azure.cli.core.util import in_cloud_console if in_cloud_console(): From 47a4a39f89b6899671824ab8c4ef1cc6734b46b8 Mon Sep 17 00:00:00 2001 From: Mansoor Sarfraz Date: Mon, 10 Aug 2026 12:03:41 +1000 Subject: [PATCH 5/9] fixed PR comments --- scripts/release/rpm/azurelinux.dockerfile | 2 +- src/azure-cli-core/azure/cli/core/extension/operations.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/release/rpm/azurelinux.dockerfile b/scripts/release/rpm/azurelinux.dockerfile index b75d6920400..d5d43025890 100644 --- a/scripts/release/rpm/azurelinux.dockerfile +++ b/scripts/release/rpm/azurelinux.dockerfile @@ -29,7 +29,7 @@ RUN --mount=type=secret,id=PIP_INDEX_URL export PIP_INDEX_URL=$(cat /run/secrets dos2unix ./scripts/release/rpm/azure-cli.spec && \ REPO_PATH=$(pwd) CLI_VERSION=$cli_version PYTHON_PACKAGE=python3 PYTHON_CMD=python3 \ rpmbuild -v -bb --clean scripts/release/rpm/azure-cli.spec && \ - RPM_PATH=$(find / -xdev -type f -name "azure-cli-${cli_version}-1.*.rpm" 2>/dev/null | head -n 1) && \ + RPM_PATH=$(find /usr/src/azl/RPMS /root/rpmbuild/RPMS -type f -name "azure-cli-${cli_version}-1.*.rpm" -quit 2>/dev/null) && \ test -n "$RPM_PATH" && \ cp "$RPM_PATH" /azure-cli-dev.rpm && \ mkdir /out && cp "$RPM_PATH" /out/ diff --git a/src/azure-cli-core/azure/cli/core/extension/operations.py b/src/azure-cli-core/azure/cli/core/extension/operations.py index c52b9ed8f97..3115c8aa127 100644 --- a/src/azure-cli-core/azure/cli/core/extension/operations.py +++ b/src/azure-cli-core/azure/cli/core/extension/operations.py @@ -216,7 +216,8 @@ def _install_deps_for_psycopg2(): # pylint: disable=too-many-statements, too-ma ]) elif system == 'Linux': distname, distversion = get_linux_distro() - distname = distname.lower().strip() + distname = (distname or '').lower().strip() + distversion = distversion or '' is_azure_linux_4 = 'azure linux' in distname and distversion.startswith('4.') if installer == 'DEB' or any(x in distname for x in ['ubuntu', 'debian']): exit_code = subprocess.call(['dpkg', '-s', 'gcc', 'libpq-dev', 'python3-dev'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) @@ -257,7 +258,7 @@ def _install_deps_for_psycopg2(): # pylint: disable=too-many-statements, too-ma rpm_install_cmd.insert(0, 'sudo') logger.debug("Install dependencies with '%s'", " ".join(rpm_install_cmd)) logger.warning( - 'This extension depends on %s and they will be installed first if not exist.', + 'This extension depends on %s and they will be installed first if not present.', ' '.join(rpm_packages) ) exit_code = subprocess.call(rpm_install_cmd) From ceabf26a3d04acb06fe4e4e5923b89f8f3a31edb Mon Sep 17 00:00:00 2001 From: Mansoor Sarfraz Date: Mon, 10 Aug 2026 12:18:17 +1000 Subject: [PATCH 6/9] fixed PR comments - error message added --- scripts/release/rpm/azurelinux.dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/release/rpm/azurelinux.dockerfile b/scripts/release/rpm/azurelinux.dockerfile index d5d43025890..ec55638820c 100644 --- a/scripts/release/rpm/azurelinux.dockerfile +++ b/scripts/release/rpm/azurelinux.dockerfile @@ -29,8 +29,8 @@ RUN --mount=type=secret,id=PIP_INDEX_URL export PIP_INDEX_URL=$(cat /run/secrets dos2unix ./scripts/release/rpm/azure-cli.spec && \ REPO_PATH=$(pwd) CLI_VERSION=$cli_version PYTHON_PACKAGE=python3 PYTHON_CMD=python3 \ rpmbuild -v -bb --clean scripts/release/rpm/azure-cli.spec && \ - RPM_PATH=$(find /usr/src/azl/RPMS /root/rpmbuild/RPMS -type f -name "azure-cli-${cli_version}-1.*.rpm" -quit 2>/dev/null) && \ - test -n "$RPM_PATH" && \ + RPM_PATH=$(find /usr/src/azl/RPMS /root/rpmbuild/RPMS -type f -name "azure-cli-${cli_version}-1.*.rpm" 2>/dev/null | head -n 1) || RPM_PATH="" && \ + if [ -z "$RPM_PATH" ]; then echo "ERROR: No RPM found in expected directories. Contents:"; ls -la /usr/src/azl/RPMS 2>/dev/null || echo " /usr/src/azl/RPMS not found"; ls -la /root/rpmbuild/RPMS 2>/dev/null || echo " /root/rpmbuild/RPMS not found"; exit 1; fi && \ cp "$RPM_PATH" /azure-cli-dev.rpm && \ mkdir /out && cp "$RPM_PATH" /out/ From 6419d2edfa16edf70ce6ed56e0e435e0020f8bae Mon Sep 17 00:00:00 2001 From: Mansoor Sarfraz Date: Mon, 10 Aug 2026 17:12:52 +1000 Subject: [PATCH 7/9] fixed PR comments - tdnf handled --- .../azure/cli/core/extension/operations.py | 7 +++++++ .../extension/tests/latest/test_extension_commands.py | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/src/azure-cli-core/azure/cli/core/extension/operations.py b/src/azure-cli-core/azure/cli/core/extension/operations.py index 3115c8aa127..4c3a702699a 100644 --- a/src/azure-cli-core/azure/cli/core/extension/operations.py +++ b/src/azure-cli-core/azure/cli/core/extension/operations.py @@ -252,6 +252,13 @@ def _install_deps_for_psycopg2(): # pylint: disable=too-many-statements, too-ma logger.debug("Install dependencies with '%s'", " ".join(zypper_install_cmd)) subprocess.call(zypper_install_cmd) elif is_azure_linux_4: + from shutil import which + if which('tdnf') is None: + raise CLIError( + 'tdnf package manager not found. This extension requires Azure Linux 4 with tdnf ' + 'to install required system dependencies: gcc, libpq-devel, python3-devel, binutils, ' + 'glibc-devel, kernel-headers.' + ) rpm_packages = ['gcc', 'libpq-devel', 'python3-devel', 'binutils', 'glibc-devel', 'kernel-headers'] rpm_install_cmd = ['tdnf', 'install', '-y'] + rpm_packages if os.geteuid() != 0: # pylint: disable=no-member diff --git a/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py b/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py index 5a1395ef63f..02747d55e79 100644 --- a/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py +++ b/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py @@ -518,6 +518,15 @@ def test_install_psycopg2_deps_reports_rpm_install_failure(self): with self.assertRaisesRegex(CLIError, 'Failed to install required system dependencies for psycopg2'): _install_deps_for_psycopg2() + def test_install_psycopg2_deps_fails_if_tdnf_missing(self): + with mock.patch('azure.cli.core.util.in_cloud_console', return_value=False), \ + mock.patch('platform.system', return_value='Linux'), \ + mock.patch('azure.cli.core.util.get_linux_distro', return_value=('Azure Linux', '4.0')), \ + mock.patch.dict('os.environ', {'AZ_INSTALLER': 'RPM'}), \ + mock.patch('shutil.which', return_value=None): + with self.assertRaisesRegex(CLIError, 'tdnf package manager not found'): + _install_deps_for_psycopg2() + def _setup_cmd(self): cmd = mock.MagicMock() cmd.cli_ctx = DummyCli() From caafeb403f50b8556a5694a9f092d643b2be7b94 Mon Sep 17 00:00:00 2001 From: Mansoor Sarfraz Date: Mon, 10 Aug 2026 17:35:37 +1000 Subject: [PATCH 8/9] fixed PR comments --- .../azure/cli/core/extension/operations.py | 13 +++++++++--- .../tests/latest/test_extension_commands.py | 21 +++++++++++++++++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/extension/operations.py b/src/azure-cli-core/azure/cli/core/extension/operations.py index 4c3a702699a..617f8d86a26 100644 --- a/src/azure-cli-core/azure/cli/core/extension/operations.py +++ b/src/azure-cli-core/azure/cli/core/extension/operations.py @@ -217,8 +217,15 @@ def _install_deps_for_psycopg2(): # pylint: disable=too-many-statements, too-ma elif system == 'Linux': distname, distversion = get_linux_distro() distname = (distname or '').lower().strip() - distversion = distversion or '' - is_azure_linux_4 = 'azure linux' in distname and distversion.startswith('4.') + distversion = (distversion or '').strip() + # Extract major version from VERSION_ID (e.g., "4", "4.0", "4.0.1" all map to major=4) + azl_major_version = None + if 'azure linux' in distname and distversion: + try: + azl_major_version = int(distversion.partition('.')[0]) + except ValueError: + pass + is_azure_linux_4 = azl_major_version == 4 if installer == 'DEB' or any(x in distname for x in ['ubuntu', 'debian']): exit_code = subprocess.call(['dpkg', '-s', 'gcc', 'libpq-dev', 'python3-dev'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if exit_code != 0: @@ -265,7 +272,7 @@ def _install_deps_for_psycopg2(): # pylint: disable=too-many-statements, too-ma rpm_install_cmd.insert(0, 'sudo') logger.debug("Install dependencies with '%s'", " ".join(rpm_install_cmd)) logger.warning( - 'This extension depends on %s and they will be installed first if not present.', + 'This extension depends on %s and will now be installed.', ' '.join(rpm_packages) ) exit_code = subprocess.call(rpm_install_cmd) diff --git a/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py b/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py index 02747d55e79..13563d5cab9 100644 --- a/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py +++ b/src/azure-cli-core/azure/cli/core/extension/tests/latest/test_extension_commands.py @@ -494,8 +494,11 @@ def test_install_psycopg2_deps_uses_azl4_packages(self): azure_linux_packages = ['gcc', 'libpq-devel', 'python3-devel', 'binutils', 'glibc-devel', 'kernel-headers'] subprocess_call.assert_any_call(['tdnf', 'install', '-y'] + azure_linux_packages) - self.assertNotIn(mock.call(['yum', 'install', '-y'] + azure_linux_packages), - subprocess_call.call_args_list) + # Ensure no yum/sudo yum calls were made + for call in subprocess_call.call_args_list: + args = call.args[0] if call.args else [] + self.assertFalse(any(cmd in args for cmd in ['yum', 'zypper']), + f"Unexpected package manager call: {args}") def test_install_psycopg2_deps_preserves_azl3_rpm_behavior(self): with mock.patch('azure.cli.core.util.in_cloud_console', return_value=False), \ @@ -527,6 +530,20 @@ def test_install_psycopg2_deps_fails_if_tdnf_missing(self): with self.assertRaisesRegex(CLIError, 'tdnf package manager not found'): _install_deps_for_psycopg2() + def test_install_psycopg2_deps_recognizes_azl4_without_dot_version(self): + # Regression test: VERSION_ID may be "4" (no dot) instead of "4.0" + with mock.patch('azure.cli.core.util.in_cloud_console', return_value=False), \ + mock.patch('platform.system', return_value='Linux'), \ + mock.patch('azure.cli.core.util.get_linux_distro', return_value=('Azure Linux', '4')), \ + mock.patch.dict('os.environ', {'AZ_INSTALLER': 'RPM'}), \ + mock.patch('shutil.which', side_effect=lambda cmd: '/usr/bin/tdnf' if cmd == 'tdnf' else None), \ + mock.patch('os.geteuid', return_value=0, create=True), \ + mock.patch('subprocess.call', return_value=0) as subprocess_call: + _install_deps_for_psycopg2() + + azure_linux_packages = ['gcc', 'libpq-devel', 'python3-devel', 'binutils', 'glibc-devel', 'kernel-headers'] + subprocess_call.assert_any_call(['tdnf', 'install', '-y'] + azure_linux_packages) + def _setup_cmd(self): cmd = mock.MagicMock() cmd.cli_ctx = DummyCli() From e610a1098fb44c9e8ba5b597c1bc5a9584c0ceb3 Mon Sep 17 00:00:00 2001 From: Mansoor Sarfraz Date: Mon, 10 Aug 2026 18:03:55 +1000 Subject: [PATCH 9/9] fixed PR comments --- scripts/release/rpm/azurelinux.dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release/rpm/azurelinux.dockerfile b/scripts/release/rpm/azurelinux.dockerfile index ec55638820c..2e7e65a8ff8 100644 --- a/scripts/release/rpm/azurelinux.dockerfile +++ b/scripts/release/rpm/azurelinux.dockerfile @@ -29,7 +29,7 @@ RUN --mount=type=secret,id=PIP_INDEX_URL export PIP_INDEX_URL=$(cat /run/secrets dos2unix ./scripts/release/rpm/azure-cli.spec && \ REPO_PATH=$(pwd) CLI_VERSION=$cli_version PYTHON_PACKAGE=python3 PYTHON_CMD=python3 \ rpmbuild -v -bb --clean scripts/release/rpm/azure-cli.spec && \ - RPM_PATH=$(find /usr/src/azl/RPMS /root/rpmbuild/RPMS -type f -name "azure-cli-${cli_version}-1.*.rpm" 2>/dev/null | head -n 1) || RPM_PATH="" && \ + RPM_PATH=$(find /usr/src/azl/RPMS /root/rpmbuild/RPMS -type f -name "azure-cli-${cli_version}-1.*.rpm" 2>/dev/null | head -n 1) && \ if [ -z "$RPM_PATH" ]; then echo "ERROR: No RPM found in expected directories. Contents:"; ls -la /usr/src/azl/RPMS 2>/dev/null || echo " /usr/src/azl/RPMS not found"; ls -la /root/rpmbuild/RPMS 2>/dev/null || echo " /root/rpmbuild/RPMS not found"; exit 1; fi && \ cp "$RPM_PATH" /azure-cli-dev.rpm && \ mkdir /out && cp "$RPM_PATH" /out/