pax_global_header00006660000000000000000000000064152723631030014514gustar00rootroot0000000000000052 comment=f65a76676430d351e8d5d202bb2a013e0fee82b0 azure-vm-utils-0.8.0/000077500000000000000000000000001527236310300144255ustar00rootroot00000000000000azure-vm-utils-0.8.0/.clang-format000066400000000000000000000000301527236310300167710ustar00rootroot00000000000000BasedOnStyle: Microsoft azure-vm-utils-0.8.0/.github/000077500000000000000000000000001527236310300157655ustar00rootroot00000000000000azure-vm-utils-0.8.0/.github/dependabot.yml000066400000000000000000000003241527236310300206140ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" groups: github-actions: patterns: ["*"] schedule: interval: "weekly" cooldown: default-days: 7 azure-vm-utils-0.8.0/.github/workflows/000077500000000000000000000000001527236310300200225ustar00rootroot00000000000000azure-vm-utils-0.8.0/.github/workflows/codeql.yml000066400000000000000000000106061527236310300220170ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ "main" ] pull_request: schedule: - cron: '44 19 * * 3' jobs: analyze: name: Analyze (${{ matrix.language }}) # Runner size impacts CodeQL analysis time. To learn more, please see: # - https://gh.io/recommended-hardware-resources-for-running-codeql # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} permissions: # required for all workflows security-events: write # required to fetch internal or private CodeQL packs packages: read # only required for workflows in private repositories actions: read contents: read strategy: fail-fast: false matrix: include: - language: c-cpp build-mode: autobuild # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # If the analyze step fails for one of the languages you are analyzing with # "We were unable to automatically build your code", modify the matrix above # to set the build mode to "manual" for that language. Then modify this step # to build your code. # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - if: matrix.build-mode == 'manual' shell: bash run: | echo 'If you are using a "manual" build mode for one or more of the' \ 'languages you are analyzing, replace this with the commands to build' \ 'your code, for example:' echo ' make bootstrap' echo ' make release' exit 1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: category: "/language:${{matrix.language}}" azure-vm-utils-0.8.0/.github/workflows/debs.yml000066400000000000000000000040271527236310300214650ustar00rootroot00000000000000name: Debian Packaging CI on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: container: - ubuntu:20.04 - ubuntu:22.04 - ubuntu:24.04 - debian:12 - debian:sid defaults: run: shell: bash steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.sha }} fetch-depth: 0 fetch-tags: true - name: Get tags from upstream, if needed if: github.repository != 'Azure/azure-vm-utils' run: | git remote add upstream https://github.com/Azure/azure-vm-utils.git git fetch upstream --tags - name: Build debs run: | ./scripts/build-deb.sh - name: Lintian check run: | lintian --fail-on error,warning out/*.deb - name: Install debs run: | sudo apt-get install -y mdadm sudo dpkg -i out/*.deb - name: Verify installation run: | set -x dpkg -L azure-vm-utils test -f /etc/azure-ephemeral-disk-setup.conf test -f /usr/lib/systemd/network/01-azure-unmanaged-sriov.network test -f /usr/lib/systemd/system/azure-ephemeral-disk-setup.service test -f /usr/lib/udev/rules.d/10-azure-unmanaged-sriov.rules test -f /usr/lib/udev/rules.d/80-azure-disk.rules test -x /usr/sbin/azure-ephemeral-disk-setup test -x /usr/sbin/azure-nvme-id test -x /usr/sbin/azure-vm-utils-selftest test -f /usr/share/initramfs-tools/hooks/azure-disk test -f /usr/share/initramfs-tools/hooks/azure-unmanaged-sriov test -f /usr/share/man/man8/azure-ephemeral-disk-setup.8.gz test -f /usr/share/man/man8/azure-nvme-id.8.gz test -f /usr/share/man/man8/azure-vm-utils-selftest.8.gz azure-ephemeral-disk-setup --version azure-nvme-id --version sudo azure-vm-utils-selftest --skip-imds-validation --skip-udev-validation azure-vm-utils-0.8.0/.github/workflows/main.yml000066400000000000000000000064711527236310300215010ustar00rootroot00000000000000name: Main CI on: [push, pull_request] jobs: build: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-22.04, ubuntu-24.04] defaults: run: shell: bash steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.sha }} fetch-depth: 0 fetch-tags: true - name: Get tags from upstream, if needed if: github.repository != 'Azure/azure-vm-utils' run: | git remote add upstream https://github.com/Azure/azure-vm-utils.git git fetch upstream --tags - name: Setup run: | sudo apt update sudo apt install gcc pandoc cmake libjson-c-dev mdadm -y - name: Build & install project with cmake with tests disabled run: | cmake -B build -S . -DENABLE_TESTS=0 make -C build sudo make -C build install - name: Verify installation run: | set -x test -f /usr/local/lib/systemd/network/01-azure-unmanaged-sriov.network test -f /usr/local/lib/systemd/system/azure-ephemeral-disk-setup.service test -f /usr/local/lib/udev/rules.d/10-azure-unmanaged-sriov.rules test -f /usr/local/lib/udev/rules.d/80-azure-disk.rules test -x /usr/local/sbin/azure-ephemeral-disk-setup test -x /usr/local/sbin/azure-nvme-id test -x /usr/local/sbin/azure-vm-utils-selftest test -f /usr/local/share/initramfs-tools/hooks/azure-disk test -f /usr/local/share/initramfs-tools/hooks/azure-unmanaged-sriov test -f /usr/local/share/man/man8/azure-ephemeral-disk-setup.8 test -f /usr/local/share/man/man8/azure-nvme-id.8 test -f /usr/local/share/man/man8/azure-vm-utils-selftest.8 azure-ephemeral-disk-setup --version azure-nvme-id --version sudo azure-vm-utils-selftest --skip-imds-validation --skip-udev-validation - name: Verify manpages can be generated run: | make -C build generate-manpages - name: Rebuild with tests enabled and run tests run: | set -x sudo apt install -y libcmocka-dev cppcheck clang-format rm -rf build cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug cd build make # ctest should invoke all tests, but to be sure let's run them individually test -x ./debug_tests ./debug_tests test -x ./identify_disks_tests ./identify_disks_tests test -x ./identify_udev_tests ./identify_udev_tests test -x ./nvme_tests ./nvme_tests ctest --verbose -j - name: Check source formatting with clang-format run: | make -C build check-clang-format || (echo "Run 'make clang-format' to fix formatting issues" && exit 1) - name: Check cppcheck run: | make -C build cppcheck || (echo "Run 'make cppcheck' to fix cppcheck issues" && exit 1) - name: Check shell scripts run: | sudo snap install shellcheck make -C build shellcheck || (echo "Run 'make shellcheck' to fix shell script issues" && exit 1) - name: Check python scripts run: | python -m venv venv source venv/bin/activate pip install -r selftest/test-requirements-frozen.txt make -C build python-lint azure-vm-utils-0.8.0/.github/workflows/rpms.yml000066400000000000000000000070131527236310300215270ustar00rootroot00000000000000name: RPM Packaging CI on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: container: # EOL May 13th, 2026 - fedora:42 - fedora:43 - fedora:44 - fedora:rawhide - ghcr.io/almalinux/10-base:latest - ghcr.io/almalinux/9-base:latest - ghcr.io/almalinux/8-base:latest - mcr.microsoft.com/cbl-mariner/base/core:2.0 - mcr.microsoft.com/azurelinux/base/core:3.0 container: image: ${{ matrix.container }} options: --user root --privileged defaults: run: shell: bash steps: - name: Identify OS run: cat /etc/os-release - name: Install dependencies run: | set -x id case "${{ matrix.container }}" in *almalinux/8*) dnf install -y 'dnf-command(config-manager)' epel-release dnf config-manager --set-enabled powertools dnf install -y python39 alternatives --set python3 /usr/bin/python3.9 /usr/bin/env python3 --version ;; *almalinux/9*) dnf install -y 'dnf-command(config-manager)' dnf config-manager --set-enabled crb ;; *azurelinux*|*cbl-mariner*) export HOME=/root # tdnf requires HOME to be set to /root tdnf install -y awk ca-certificates dnf --verbose ;; *fedora*) dnf install -y awk ;; esac dnf install -y git sudo - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.sha }} fetch-depth: 0 fetch-tags: true - name: Get tags from upstream, if needed if: github.repository != 'Azure/azure-vm-utils' run: | git remote add upstream https://github.com/Azure/azure-vm-utils.git git fetch upstream --tags - name: Build RPMs run: | set -x git config --global --add safe.directory "$(pwd)" ./scripts/build-rpm.sh - name: Install rpms run: | dnf install -y mdadm util-linux rpm -Uvh -i --nosignature out/*.rpm - name: Verify installation run: | set -x rpm -qa azure-vm-utils test -f /etc/azure-ephemeral-disk-setup.conf test -f /usr/lib/dracut/modules.d/97azure-disk/module-setup.sh test -f /usr/lib/dracut/modules.d/97azure-unmanaged-sriov/module-setup.sh test -f /usr/lib/systemd/network/01-azure-unmanaged-sriov.network test -f /usr/lib/systemd/system/azure-ephemeral-disk-setup.service test -f /usr/lib/udev/rules.d/10-azure-unmanaged-sriov.rules test -f /usr/lib/udev/rules.d/80-azure-disk.rules test -x /usr/sbin/azure-ephemeral-disk-setup || test -x /usr/bin/azure-ephemeral-disk-setup test -x /usr/sbin/azure-nvme-id || test -x /usr/bin/azure-nvme-id test -x /usr/sbin/azure-vm-utils-selftest || test -x /usr/bin/azure-vm-utils-selftest test -f /usr/share/man/man8/azure-ephemeral-disk-setup.8.gz test -f /usr/share/man/man8/azure-nvme-id.8.gz test -f /usr/share/man/man8/azure-vm-utils-selftest.8.gz azure-ephemeral-disk-setup --version azure-nvme-id --version azure-vm-utils-selftest --skip-imds-validation --skip-udev-validation azure-vm-utils-0.8.0/.gitignore000066400000000000000000000013661527236310300164230ustar00rootroot00000000000000# Object files *.o *.ko *.obj *.elf # Linker output *.ilk *.map *.exp # Precompiled Headers *.gch *.pch # Libraries *.lib *.a *.la *.lo # Shared objects (inc. Windows DLLs) *.dll *.so *.so.* *.dylib # Executables *.exe *.out *.app *.i*86 *.x86_64 *.hex # Debug files *.dSYM/ *.su *.idb *.pdb # Kernel Module Compile Results *.mod* *.cmd .tmp_versions/ modules.order Module.symvers Mkfile.old dkms.conf # CMake CMakeLists.txt.user CMakeCache.txt CMakeFiles CMakeScripts CPackConfig.cmake CPackSourceConfig.cmake DartConfiguration.tcl Testing Makefile cmake_install.cmake install_manifest.txt compile_commands.json CTestTestfile.cmake _deps # VS Code .vscode # Compiled artifacts azure-nvme-id obj-* /build/ /doc/azure-nvme-id.8 /src/version.h /out/ azure-vm-utils-0.8.0/CMakeLists.txt000066400000000000000000000052671527236310300171770ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.5) project(azure-vm-utils VERSION 0.1.0 LANGUAGES C) if(NOT DEFINED VERSION) execute_process( COMMAND git describe --tags --always --dirty WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE VERSION OUTPUT_STRIP_TRAILING_WHITESPACE ) endif() set(AZURE_NVME_ID_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/sbin" CACHE PATH "azure-nvme-id installation directory") set(AZURE_EPHEMERAL_DISK_SETUP_CONF_INSTALL_DIR "/etc" CACHE PATH "azure-ephemeral-disk-setup.conf installation directory") set(AZURE_EPHEMERAL_DISK_SETUP_INSTALL_DIR "${AZURE_NVME_ID_INSTALL_DIR}" CACHE PATH "azure-ephemeral-disk-setup installation directory") set(AZURE_EPHEMERAL_DISK_SETUP_SERVICE_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/lib/systemd/system" CACHE PATH "azure-ephemeral-disk-setup.service installation directory") set(DRACUT_MODULES_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/lib/dracut/modules.d" CACHE PATH "dracut modules installation directory") set(INITRAMFS_HOOKS_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/share/initramfs-tools/hooks" CACHE PATH "initramfs-tools hooks installation directory") set(NETWORKD_CONFIGS_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/lib/systemd/network" CACHE PATH "networkd configs installation directory") set(UDEV_RULES_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/lib/udev/rules.d" CACHE PATH "udev rules installation directory") include(cmake/cppcheck.cmake) include(cmake/clangformat.cmake) include(cmake/doc.cmake) include(cmake/ephemeral-disk-setup.cmake) include(cmake/initramfs.cmake) include(cmake/networkd.cmake) include(cmake/python.cmake) include(cmake/shellcheck.cmake) include(cmake/udev.cmake) include(CTest) enable_testing() find_package(PkgConfig REQUIRED) pkg_check_modules(JSON_C REQUIRED json-c) include_directories( ${CMAKE_SOURCE_DIR}/src ${JSON_C_INCLUDE_DIRS} ) add_compile_options(-Wextra -Wall $<$:-Werror> -std=gnu11 -D_GNU_SOURCE=1) add_executable(azure-nvme-id src/debug.c src/identify_disks.c src/identify_udev.c src/main.c src/nvme.c) target_link_libraries(azure-nvme-id ${JSON_C_LIBRARIES}) configure_file( "${CMAKE_SOURCE_DIR}/src/version.h.in" "${CMAKE_SOURCE_DIR}/src/version.h" @ONLY ) install(TARGETS azure-nvme-id DESTINATION ${AZURE_NVME_ID_INSTALL_DIR}) install(FILES ${CMAKE_SOURCE_DIR}/selftest/selftest.py DESTINATION ${AZURE_NVME_ID_INSTALL_DIR} RENAME azure-vm-utils-selftest PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ) set(CPACK_PROJECT_NAME ${PROJECT_NAME}) set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) include(CPack) option(ENABLE_TESTS "Enable unit tests" ON) if(ENABLE_TESTS) include(cmake/tests.cmake) endif() azure-vm-utils-0.8.0/CODE_OF_CONDUCT.md000066400000000000000000000006741527236310300172330ustar00rootroot00000000000000# Microsoft Open Source Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). Resources: - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns azure-vm-utils-0.8.0/LICENSE000066400000000000000000000021651527236310300154360ustar00rootroot00000000000000 MIT License Copyright (c) Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE azure-vm-utils-0.8.0/README.md000066400000000000000000000072731527236310300157150ustar00rootroot00000000000000# azure-vm-utils A collection of utilities and udev rules to make the most of the Linux experience on Azure. ## Quick Start To build: ``` cmake . make ``` To install: ``` sudo make install ``` # Executables ## azure-nvme-id `azure-nvme-id` is a utility to help identify Azure NVMe devices. To run: ``` sudo azure-nvme-id ``` To run in udev mode: ``` DEVNAME=/dev/nvme0n1 azure-nvme-id --udev ``` # Rules for udev ## 80-azure-disk.rules Provides helpful symlinks in /dev/disk/azure for local, data, and os disks. # Testing ## unit tests There are unit test executables provided to validate functionality at build-time. They are automatically enabled, but can be disabled with -DENABLE_TESTS=0 passed to cmake. To build without tests: ``` mkdir build && cd build cmake .. -DENABLE_TESTS=0 make ``` To build and execute tests: ``` mkdir build && cd build cmake .. make ctest --output-on-failure ``` Optionally, ctest can be skipped in favor of running tests individually: ``` mkdir build && cd build cmake .. make for test in *_tests; do ./$test; done ``` ## selfcheck selfcheck is provided to validate the runtime environment of a VM. With azure-vm-utils installed in a VM on Azure, simply copy the selfcheck.py executable to the target and execute with sudo. ``` scp selfcheck/selftest.py $ip: && ssh $ip -- sudo ./selftest.py ``` Selftest uses strict current-behavior checks by default. Use `--compat` to allow for features not present in older distro-packaged versions. Image tests enable compatibility for packaged utilities and keep strict checks when installing from source. ## updating requirements To refresh the frozen selftest dependency list from [selftest/test-requirements.txt](selftest/test-requirements.txt), run: ```sh ./scripts/update-selftest-requirements.sh ``` This requires Docker and uses a Python 3.10 container to generate updated requirements. ## test_images Image tests create Azure VMs from marketplace and community images and run selftests over SSH. They require Azure CLI, `ssh`, and `ssh-keygen`. Tests use utilities installed in each image. Set `SELFTEST_INSTALL_FROM_SOURCE=1` to build and install from the working tree before testing. See [test_images.py](selftest/test_images.py) for example runs, environment variables, and the test matrix. # Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. azure-vm-utils-0.8.0/SECURITY.md000066400000000000000000000051401527236310300162160ustar00rootroot00000000000000 ## Security Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. ## Preferred Languages We prefer all communications to be in English. ## Policy Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). azure-vm-utils-0.8.0/SUPPORT.md000066400000000000000000000023341527236310300161250ustar00rootroot00000000000000# TODO: The maintainer of this repo has not yet edited this file **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? - **No CSS support:** Fill out this template with information about how to file issues and get help. - **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. - **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* # Support ## How to file issues and get help This project uses GitHub Issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new Issue. For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER CHANNEL. WHERE WILL YOU HELP PEOPLE?**. ## Microsoft Support Policy Support for this **PROJECT or PRODUCT** is limited to the resources listed above. azure-vm-utils-0.8.0/cmake/000077500000000000000000000000001527236310300155055ustar00rootroot00000000000000azure-vm-utils-0.8.0/cmake/clangformat.cmake000066400000000000000000000005321527236310300210040ustar00rootroot00000000000000file(GLOB ALL_SOURCE_FILES src/*.[ch] tests/*.[ch]) add_custom_target(clang-format COMMAND clang-format -i ${ALL_SOURCE_FILES} COMMENT "Running clang-format on source files" ) add_custom_target(check-clang-format COMMAND clang-format --dry-run --Werror ${ALL_SOURCE_FILES} COMMENT "Running clang-format check on source files" ) azure-vm-utils-0.8.0/cmake/cppcheck.cmake000066400000000000000000000004611527236310300202700ustar00rootroot00000000000000file(GLOB ALL_SOURCE_FILES src/*.[ch] tests/*.[ch]) set(CPPCHECK_COMMAND cppcheck --enable=all --suppress=missingIncludeSystem -I${CMAKE_SOURCE_DIR}/src ${ALL_SOURCE_FILES} ) add_custom_target(cppcheck COMMAND ${CPPCHECK_COMMAND} COMMENT "Running cppcheck on source files" ) azure-vm-utils-0.8.0/cmake/doc.cmake000066400000000000000000000033551527236310300172620ustar00rootroot00000000000000function(generate_manpage manpage_name) set(manpage_in "${CMAKE_SOURCE_DIR}/doc/${manpage_name}.8.in") set(manpage_output "${CMAKE_CURRENT_BINARY_DIR}/doc/${manpage_name}.8") configure_file( "${manpage_in}" "${manpage_output}" @ONLY ) set(MANPAGES_OUTPUT ${MANPAGES_OUTPUT} ${manpage_output} PARENT_SCOPE) endfunction() string(TIMESTAMP TODAY "%B %d, %Y") set(PANDOC_GENERATE_COMMAND pandoc --standalone --from markdown --to man --template "${CMAKE_SOURCE_DIR}/doc/pandoc.template" --variable footer="${manpage_name} ${VERSION}" --variable date="${TODAY}" ) set(PANDOC_FIXUPS_COMMAND sed -i -e 's/f\\[C]/f[CR]/g' -e 's/f\\[V]/f[CB]/g') add_custom_target( generate-manpages COMMAND ${PANDOC_GENERATE_COMMAND} "${CMAKE_SOURCE_DIR}/doc/azure-ephemeral-disk-setup.md" -o "${CMAKE_SOURCE_DIR}/doc/azure-ephemeral-disk-setup.8.in" COMMAND ${PANDOC_FIXUPS_COMMAND} "${CMAKE_SOURCE_DIR}/doc/azure-ephemeral-disk-setup.8.in" COMMAND ${PANDOC_GENERATE_COMMAND} "${CMAKE_SOURCE_DIR}/doc/azure-nvme-id.md" -o "${CMAKE_SOURCE_DIR}/doc/azure-nvme-id.8.in" COMMAND ${PANDOC_FIXUPS_COMMAND} "${CMAKE_SOURCE_DIR}/doc/azure-nvme-id.8.in" COMMAND ${PANDOC_GENERATE_COMMAND} "${CMAKE_SOURCE_DIR}/doc/azure-vm-utils-selftest.md" -o "${CMAKE_SOURCE_DIR}/doc/azure-vm-utils-selftest.8.in" COMMAND ${PANDOC_FIXUPS_COMMAND} "${CMAKE_SOURCE_DIR}/doc/azure-vm-utils-selftest.8.in" ) generate_manpage("azure-ephemeral-disk-setup") generate_manpage("azure-nvme-id") generate_manpage("azure-vm-utils-selftest") add_custom_target( doc DEPENDS ${MANPAGES_OUTPUT} ) set(MANPAGES_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/share/man") install(FILES ${MANPAGES_OUTPUT} DESTINATION ${MANPAGES_INSTALL_DIR}/man8) azure-vm-utils-0.8.0/cmake/ephemeral-disk-setup.cmake000066400000000000000000000020531527236310300225370ustar00rootroot00000000000000configure_file( "${CMAKE_SOURCE_DIR}/ephemeral-disk-setup/azure-ephemeral-disk-setup" "${CMAKE_BINARY_DIR}/ephemeral-disk-setup/azure-ephemeral-disk-setup" @ONLY ) configure_file( "${CMAKE_SOURCE_DIR}/ephemeral-disk-setup/azure-ephemeral-disk-setup.service" "${CMAKE_BINARY_DIR}/ephemeral-disk-setup/azure-ephemeral-disk-setup.service" @ONLY ) install(FILES ${CMAKE_SOURCE_DIR}/ephemeral-disk-setup/azure-ephemeral-disk-setup.conf DESTINATION ${AZURE_EPHEMERAL_DISK_SETUP_CONF_INSTALL_DIR} PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) install(FILES ${CMAKE_BINARY_DIR}/ephemeral-disk-setup/azure-ephemeral-disk-setup.service DESTINATION ${AZURE_EPHEMERAL_DISK_SETUP_SERVICE_INSTALL_DIR} PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) install(FILES ${CMAKE_BINARY_DIR}/ephemeral-disk-setup/azure-ephemeral-disk-setup DESTINATION ${AZURE_EPHEMERAL_DISK_SETUP_INSTALL_DIR} PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ) azure-vm-utils-0.8.0/cmake/initramfs.cmake000066400000000000000000000024111527236310300205010ustar00rootroot00000000000000find_program(INITRAMFS_TOOLS update-initramfs) find_program(DRACUT dracut) if(INITRAMFS_TOOLS AND INITRAMFS_HOOKS_INSTALL_DIR) message(STATUS "initramfs-tools found, installing initramfs hooks") install(FILES ${CMAKE_SOURCE_DIR}/initramfs/initramfs-tools/hooks/azure-disk ${CMAKE_SOURCE_DIR}/initramfs/initramfs-tools/hooks/azure-unmanaged-sriov DESTINATION ${INITRAMFS_HOOKS_INSTALL_DIR} PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ OWNER_EXECUTE GROUP_EXECUTE WORLD_EXECUTE) elseif(DRACUT AND DRACUT_MODULES_INSTALL_DIR) message(STATUS "dracut found, installing dracut modules") install(FILES ${CMAKE_SOURCE_DIR}/initramfs/dracut/modules.d/97azure-disk/module-setup.sh DESTINATION ${DRACUT_MODULES_INSTALL_DIR}/97azure-disk PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ OWNER_EXECUTE GROUP_EXECUTE WORLD_EXECUTE) install(FILES ${CMAKE_SOURCE_DIR}/initramfs/dracut/modules.d/97azure-unmanaged-sriov/module-setup.sh DESTINATION ${DRACUT_MODULES_INSTALL_DIR}/97azure-unmanaged-sriov PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ OWNER_EXECUTE GROUP_EXECUTE WORLD_EXECUTE) else() message(STATUS "initramfs-tools and dracut not found, skipping installation of azure-disk hook") endif() azure-vm-utils-0.8.0/cmake/networkd.cmake000066400000000000000000000001711527236310300203430ustar00rootroot00000000000000install(FILES ${CMAKE_SOURCE_DIR}/networkd/01-azure-unmanaged-sriov.network DESTINATION ${NETWORKD_CONFIGS_INSTALL_DIR}) azure-vm-utils-0.8.0/cmake/python.cmake000066400000000000000000000012601527236310300200270ustar00rootroot00000000000000file(GLOB PYTHON_SOURCES */*.py) add_custom_target( python-autoformat COMMAND isort ${PYTHON_SOURCES} COMMAND black ${PYTHON_SOURCES} COMMAND autoflake -r --in-place --remove-unused-variables --remove-all-unused-imports --ignore-init-module-imports ${PYTHON_SOURCES} COMMENT "Running autoformatting tools" ) add_custom_target( python-lint COMMAND isort --check-only ${PYTHON_SOURCES} COMMAND black --check --diff ${PYTHON_SOURCES} COMMAND mypy --ignore-missing-imports ${PYTHON_SOURCES} COMMAND flake8 --ignore=W503,E501,E402 ${PYTHON_SOURCES} COMMAND pylint ${PYTHON_SOURCES} COMMENT "Running Python lint checks and formatting checks" ) azure-vm-utils-0.8.0/cmake/shellcheck.cmake000066400000000000000000000004001527236310300206060ustar00rootroot00000000000000file(GLOB_RECURSE SHELL_SOURCES *.sh ephemeral-disk-setup/azure-ephemeral-disk-setup ) add_custom_target(shellcheck COMMAND shellcheck ${SHELL_SOURCES} COMMENT "Running shellcheck on source files" ) azure-vm-utils-0.8.0/cmake/tests.cmake000066400000000000000000000041151527236310300176520ustar00rootroot00000000000000find_package(PkgConfig REQUIRED) pkg_check_modules(CMOCKA REQUIRED cmocka) function(add_test_executable test_name) set(multiValueArgs WRAPPED_FUNCTIONS SOURCES CFLAGS) cmake_parse_arguments(TEST "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) add_executable(${test_name} ${TEST_SOURCES} tests/capture.c) add_test(NAME ${test_name} COMMAND ${test_name}) # LTO may be enabled by packaging, but it must be disabled for --wrap to work. target_compile_options(${test_name} PRIVATE -g -O0 -fno-lto ${CMOCKA_CFLAGS_OTHER} ${TEST_CFLAGS}) target_link_options(${test_name} PRIVATE -flto=n) target_include_directories(${test_name} PRIVATE ${CMOCKA_INCLUDE_DIRS} src tests) target_link_directories(${test_name} PRIVATE ${CMOCKA_LIBRARY_DIRS}) target_link_libraries(${test_name} PRIVATE ${JSON_C_LIBRARIES} ${CMOCKA_LIBRARIES} dl) if(TEST_WRAPPED_FUNCTIONS) foreach(func ${TEST_WRAPPED_FUNCTIONS}) target_link_options(${test_name} PRIVATE -Wl,--wrap=${func}) endforeach() endif() endfunction() add_test_executable(checked_tests WRAPPED_FUNCTIONS strdup strndup json_object_new_array json_object_new_object json_object_new_string json_object_new_int json_object_object_add json_object_array_add json_object_to_json_string_ext SOURCES tests/checked_tests.c ) add_test_executable(debug_tests WRAPPED_FUNCTIONS SOURCES src/debug.c tests/debug_tests.c ) add_test_executable(identify_disks_tests WRAPPED_FUNCTIONS nvme_identify_namespace_vs get_nsid_from_namespace_device_path SOURCES src/debug.c src/identify_disks.c src/nvme.c tests/identify_disks_tests.c CFLAGS -DUNIT_TESTING_SYS_CLASS_NVME=1 ) add_test_executable(identify_udev_tests WRAPPED_FUNCTIONS nvme_identify_namespace_vs_for_namespace_device strdup SOURCES src/debug.c src/identify_udev.c src/nvme.c tests/identify_udev_tests.c ) add_test_executable(nvme_tests WRAPPED_FUNCTIONS open open64 posix_memalign ioctl __ioctl_time64 close strndup sysconf SOURCES src/identify_disks.c src/identify_udev.c src/nvme.c tests/nvme_tests.c ) azure-vm-utils-0.8.0/cmake/udev.cmake000066400000000000000000000011501527236310300174470ustar00rootroot00000000000000# Install Azure unmanaged SR-IOV rules and configuration. install(FILES ${CMAKE_SOURCE_DIR}/udev/10-azure-unmanaged-sriov.rules DESTINATION ${UDEV_RULES_INSTALL_DIR}) # Install udev rules after updating "/usr/sbin/azure-nvme-id" with installed path $AZURE_NVME_ID_INSTALL_DIR. file(READ ${CMAKE_SOURCE_DIR}/udev/80-azure-disk.rules CONTENT) string(REPLACE "/usr/sbin/azure-nvme-id" "${AZURE_NVME_ID_INSTALL_DIR}/azure-nvme-id" CONTENT "${CONTENT}") file(WRITE ${CMAKE_BINARY_DIR}/udev/80-azure-disk.rules ${CONTENT}) install(FILES ${CMAKE_BINARY_DIR}/udev/80-azure-disk.rules DESTINATION ${UDEV_RULES_INSTALL_DIR}) azure-vm-utils-0.8.0/doc/000077500000000000000000000000001527236310300151725ustar00rootroot00000000000000azure-vm-utils-0.8.0/doc/azure-ephemeral-disk-setup.8.in000066400000000000000000000275331527236310300230560ustar00rootroot00000000000000.TH "azure-ephemeral-disk-setup" "8" "October\ 29,\ 2026" "azure-ephemeral-disk-setup @VERSION@" "User Manual" .hy .SH NAME .PP \f[B]azure-ephemeral-disk-setup\f[R] \[em] detect, validate, format, and mount Azure ephemeral local disks (NVMe or SCSI) with optional RAID aggregation .SH SYNOPSIS .IP .nf \f[CR] azure-ephemeral-disk-setup \f[R] .fi .SH DESCRIPTION .PP \f[B]azure-ephemeral-disk-setup\f[R] is a systemd-managed one-shot initialization service for Microsoft Azure virtual machines that prepares ephemeral (temporary) disks for use. It supports both NVMe and SCSI ephemeral disks and is capable of aggregating multiple NVMe devices into a RAID-0 array using \f[CB]mdadm\f[R]. .PP The service is opt-in and should be configured through \f[CB]/etc/azure-ephemeral-disk-setup.conf\f[R]. It runs early in the boot process and aims to be idempotent, safe, and compatible with existing tools such as \f[CB]cloud-init\f[R] and \f[CB]WALinuxAgent\f[R]. .PP Logs are printed to stderr and are accessible via journalctl if using the systemd service: .IP .nf \f[CR] journalctl -u azure-ephemeral-disk-setup.service \f[R] .fi .SH CONFIGURATION .PP Configuration is loaded from: .IP \[bu] 2 \f[CB]/etc/azure-ephemeral-disk-setup.conf\f[R] (preferred) .IP \[bu] 2 Environment variables (fallback) .PP The following environment variables are supported: .IP \[bu] 2 \f[B]AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION\f[R] Supported values: \f[CB]mdadm\f[R], \f[CB]none\f[R], \f[CB]auto\f[R] \f[CB]mdadm\f[R]: require \f[CB]mdadm\f[R] and use it for aggregation if multiple disks are found, exiting with error if \f[CB]mdadm\f[R] is not installed (regardless of number of disks) \f[CB]auto\f[R]: use \f[CB]mdadm\f[R] for aggregation only if installed, otherwise fall back to use a single disk \f[CB]none\f[R]: never aggregate disks, only use one Default: \f[CB]auto\f[R] .IP \[bu] 2 \f[B]AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE\f[R] Supported values: \f[CB]ext4\f[R], \f[CB]xfs\f[R] Default: \f[CB]ext4\f[R] (universally available; \f[CB]xfs\f[R] requires \f[CB]xfsprogs\f[R]) .IP \[bu] 2 \f[B]AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK\f[R] Supported values: Any valid mdadm chunk size Default: \f[CB]512K\f[R] (mdadm default) .IP \[bu] 2 \f[B]AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME\f[R] Supported values: Any valid mdadm name Default: \f[CB]azure-ephemeral-md\f[R] .IP \[bu] 2 \f[B]AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT\f[R] Supported values: Any valid mount point path Default: \f[CB]/mnt\f[R] (matches cloud-init default mount point) .IP \[bu] 2 \f[B]AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE\f[R] Supported values: \f[CB]true\f[R], \f[CB]false\f[R] Default: \f[CB]false\f[R] (minimizes risk of conflict with cloud-init and WALinuxAgent) .IP \[bu] 2 \f[B]AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS\f[R] Supported values: positive integer (>=1) Default: 60 .IP \[bu] 2 \f[B]AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS\f[R] Supported values: positive integation (>=1) Default: 60 .SH OPERATION .PP The service performs the following steps: .SS 1. Load and Validate Configuration .IP \[bu] 2 Configuration is read via filtered \f[CB]source /etc/azure-ephemeral-disk-setup.conf\f[R], allowing only safe environment-style assignments. .IP \[bu] 2 If invalid values are detected, the service exits with error. .IP \[bu] 2 Required tools (\f[CB]mkfs.${AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE}\f[R], \f[CB]mdadm\f[R]) must be available. .SS 2. Check for Existing Mounts .PP If the mount point is already configured: .IP \[bu] 2 with a matching \f[CB]/etc/fstab\f[R] line containing the comment \f[CB]comment=azure-ephemeral-disk-setup\f[R], the service will wait for mount to complete. .RS 2 .IP \[bu] 2 if mount succeeds, service exits with success. .IP \[bu] 2 if mount does not complete, this service will check if the ephemeral disks have been reallocated and reconfigure them via the standard detection/validation process below. .RE .IP \[bu] 2 by cloud-init and source is \f[CB]/dev/disk/cloud/azure_resource-part1\f[R]: .RS 2 .IP \[bu] 2 if \f[CB]AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=false\f[R], the service exits with success allowing cloud-init to manage the disk. .IP \[bu] 2 if \f[CB]AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true\f[R], the service exits with error as the entry is either stale or conflicting. .RE .IP \[bu] 2 by other tools/services/configuration, the service exits with error. .SS 3. Detect and Validate Disks .PP \f[B]NVMe\f[R] disks are detected via: .IP \[bu] 2 Model names: .RS 2 .IP \[bu] 2 Microsoft NVMe Direct Disk .IP \[bu] 2 Microsoft NVMe Direct Disk v2 .RE .PP \f[B]SCSI resource disk\f[R] is detected and validated if \f[CB]AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true\f[R] and no ephemeral NVMe disks are detected. .PP See \f[B]SAFETY AND VALIDATION\f[R] section for more details on how disks are validated to ensure no data is lost. .SS 4. Disk Setup .PP Each candidate disk is checked to ensure: .IP \[bu] 2 It is a block device .IP \[bu] 2 It is not mounted .IP \[bu] 2 It is not partitioned (unless NTFS resource disk) .IP \[bu] 2 It has no existing filesystem (unless NTFS resource disk) .PP If any disk fails these checks, the service aborts with a detailed error. .SS 5. Aggregation with RAID-0 For VMs with Multiple local NVMe Disks .PP If more than one NVMe disk is found and aggregation is enabled with mdadm: .IP .nf \f[CR] mdadm --create \[dq]/dev/md/${AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME}_0\[dq] \[rs] --level=0 \[rs] --raid-devices=N \[rs] --metadata=1.2 \[rs] --chunk=\[dq]$AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK\[dq] \[rs] --name=\[dq]$AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME\[dq] \[rs] ${DISKS[\[at]]} \f[R] .fi .PP Configuration is \f[B]not\f[R] written to \f[CB]/etc/mdadm/mdadm.conf\f[R] in favor of kernel auto-detection. .SS 6. Format Filesystem .IP \[bu] 2 Formats target device (either RAID device or single disk) directly \f[B]without partitioning\f[R] .IP \[bu] 2 Uses \f[CB]mkfs.ext4 [-F] -L AzureEphmDsk ...\f[R] or \f[CB]mkfs.xfs [-f] -L AzureEphmDsk ...\f[R] with the force flag only if it is reformatting the SCSI resource disk .SS 7. Persistenting Mount .PP If \f[CB]/etc/fstab\f[R] is writable: .IP \[bu] 2 Previous entries with \f[CB]comment=azure-ephemeral-disk-setup\f[R] are removed .IP \[bu] 2 New entry added: .RS 2 .IP .nf \f[CR] LABEL=AzureEphmDsk defaults,nofail,comment=azure-ephemeral-disk-setup 0 2 \f[R] .fi .RE .PP If \f[CB]/etc/fstab\f[R] is read-only or does not exist, it will not be updated. It will be mounted by this service on every boot. .SS 8. Mounting .PP For persistent mounts: .IP \[bu] 2 Tries to mount using systemd (\f[CB]systemctl start \f[R]) .IP \[bu] 2 Falls back to direct \f[CB]mount\f[R] call if mount unit fails to start .PP For non-persitent mounts, a \f[CB]mount\f[R] call is used. .SH SAFETY AND VALIDATION .PP This service only formats NVMe disks that are: .IP \[bu] 2 Not mounted .IP \[bu] 2 Unpartitioned .IP \[bu] 2 Unformatted .PP SCSI resource disks are only reformatted if it: .IP \[bu] 2 Has a single NTFS partition labeled \f[CB]Temporary Storage\f[R] .IP \[bu] 2 Is empty except for \f[CB]dataloss_warning_readme.txt\f[R] and \f[CB]System Volume Information\f[R] .RS 2 .IP \[bu] 2 To check if disk is empty, this service will attempt to mount the resource disk partition read-only via \f[CB]ntfs3\f[R], with fallback to \f[CB]ntfs\f[R]. If both fail due to lack of driver, it is assumed safe to reformat. .RE .PP Calls to mkfs and mdadm do not use \f[CB]--force\f[R] for formatting or mdadm array creation unless reformatting validated NTFS SCSI disk. .SH EXIT STATUS .IP \[bu] 2 \f[CB]0\f[R] \[em] Success .IP \[bu] 2 \f[CB]1\f[R] \[em] Any error (invalid config, conflicting mount, unsafe disk, etc.) .SH FSTAB STRATEGY .PP The following values are used when generating an \f[CB]/etc/fstab\f[R] entry for the ephemeral disk: .IP \[bu] 2 \f[B]fs_spec\f[R]: \f[CB]LABEL=AzureEphmDsk\f[R] Refers to the disk label. This assumes the script has ownership of the \f[CB]AzureEphmDsk\f[R] label. .IP \[bu] 2 \f[B]fs_vfstype\f[R]: \f[CB]$AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE\f[R] The filesystem type, determined by configuration (e.g., \f[CB]ext4\f[R] or \f[CB]xfs\f[R]). .IP \[bu] 2 \f[B]fs_mntops\f[R]: \f[CB]defaults,nofail,comment=azure-ephemeral-disk-setup\f[R] .RS 2 .IP \[bu] 2 \f[CB]defaults\f[R]: standard mount options .IP \[bu] 2 \f[CB]nofail\f[R]: ensures the VM boots even if the ephemeral disk is missing or has been re-allocated .IP \[bu] 2 \f[CB]comment=azure-ephemeral-disk-setup\f[R]: tags the entry to show it\[cq]s managed by this setup .RE .IP \[bu] 2 \f[B]fs_freq\f[R]: \f[CB]0\f[R] Disables \f[CB]dump\f[R] backups for the filesystem. .IP \[bu] 2 \f[B]fs_passno\f[R]: \f[CB]2\f[R] Sets the order for \f[CB]fsck\f[R] during boot. Non-root filesystems typically use \f[CB]2\f[R]. .SH AVOIDING CONFLICTS WITH CLOUD-INIT .PP If \f[CB]AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=false\f[R], this service will exit with success if cloud-init is managing the SCSI resource disk at the same mount point. Given the widespread usage of cloud-init for managing the SCSI resource disk, this is the default case. .PP If \f[CB]AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true\f[R], cloud-init management of the resource disk must be disabled. There are two options for disabling cloud-init\[cq]s management of SCSI resource disk: .SS Disabling cloud-init management of SCSI resource disk via udev rules .PP Disable udev rules responsible for creating the /dev/disk/cloud symlinks: .IP .nf \f[CR] ln -sf /dev/null /etc/udev/rules.d/66-azure-ephemeral.rules \f[R] .fi .PP If cloud-init doesn\[cq]t detect the device, it will not attempt to format/mount it. .SS Disabling cloud-init management of SCSI resource disk via cloud-init configuration (cloud-init >= 25.3) .PP Work is ongoing to introduce a flag to the Azure datasource to control behavior. See pull request (https://github.com/canonical/cloud-init/pull/6323). .PP This is expected to be available in cloud-init 25.3. .SH AVOIDING CONFLICTS WITH WALINUXAGENT .PP WALinuxAgent must be configured with \f[CB]ResourceDisk.Format=n\f[R]. .PP If \f[CB]AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true\f[R], this service will exit with error if \f[CB]/etc/waagent.conf\f[R] sets \f[CB]ResourceDisk.Format=y\f[R]. .SH EXAMPLES .PP Basic usage: .IP .nf \f[CR] sudo azure-ephemeral-disk-setup \f[R] .fi .PP Enable management of SCSI resource disk: .IP .nf \f[CR] # /etc/azure-ephemeral-disk-setup.conf AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true \f[R] .fi .SH FILES .IP \[bu] 2 \f[CB]/etc/azure-ephemeral-disk-setup.conf\f[R] \[em] user configuration .IP \[bu] 2 \f[CB]/etc/fstab\f[R] \[em] mount persistence .SH SERVICE ORDERING .PP \f[CB]DefaultDependencies=no\f[R] to avoid typical service dependencies as we want to start early. .PP After: .IP \[bu] 2 \f[CB]cloud-init.service\f[R] (a.k.a. \f[CB]cloud-init-network.service\f[R]) allows users to customize service configuration using cloud-config .IP \[bu] 2 \f[CB]local-fs.target\f[R] ensures local filesystems are mounted (necessary if no cloud-init) .PP Before: .IP \[bu] 2 \f[CB]cloud-init.target\f[R] ensures we start before cloud-init completes which services may use to determine if system is fully configured .IP \[bu] 2 \f[CB]walinuxagent.service waagent.service\f[R] ensures we start prior to walinuxagent and avoid conflicts .IP \[bu] 2 \f[CB]network-online.target\f[R] to ensure we start before networking is considered online .IP \[bu] 2 \f[CB]sshd.service\f[R] to ensure we start before sshd starts accepting connections .IP \[bu] 2 \f[CB]systemd-user-sessions.service\f[R] to ensure we start before user sessions are enabled .IP \[bu] 2 \f[CB]sysinit.target\f[R] to ensure we run before most services requiring mounts (Debian/Ubuntu only) .SH DEBUGGING .PP All logs are written to stderr and accessible with: .IP .nf \f[CR] journalctl -u azure-ephemeral-disk-setup.service \f[R] .fi .SH SEE ALSO .PP \f[B]mdadm(8)\f[R], \f[B]mount(8)\f[R], \f[B]cloud-init(8)\f[R], \f[B]waagent(8)\f[R] .PP Project Home: azure-vm-utils-0.8.0/doc/azure-ephemeral-disk-setup.md000066400000000000000000000243771527236310300227050ustar00rootroot00000000000000--- title: azure-ephemeral-disk-setup section: 8 header: User Manual footer: azure-ephemeral-disk-setup __VERSION__ date: __DATE__ --- # NAME **azure-ephemeral-disk-setup** — detect, validate, format, and mount Azure ephemeral local disks (NVMe or SCSI) with optional RAID aggregation # SYNOPSIS ```bash azure-ephemeral-disk-setup ```` # DESCRIPTION **azure-ephemeral-disk-setup** is a systemd-managed one-shot initialization service for Microsoft Azure virtual machines that prepares ephemeral (temporary) disks for use. It supports both NVMe and SCSI ephemeral disks and is capable of aggregating multiple NVMe devices into a RAID-0 array using `mdadm`. The service is opt-in and should be configured through `/etc/azure-ephemeral-disk-setup.conf`. It runs early in the boot process and aims to be idempotent, safe, and compatible with existing tools such as `cloud-init` and `WALinuxAgent`. Logs are printed to stderr and are accessible via journalctl if using the systemd service: ```bash journalctl -u azure-ephemeral-disk-setup.service ``` # CONFIGURATION Configuration is loaded from: * `/etc/azure-ephemeral-disk-setup.conf` (preferred) * Environment variables (fallback) The following environment variables are supported: * **AZURE\_EPHEMERAL\_DISK\_SETUP\_AGGREGATION** Supported values: `mdadm`, `none`, `auto` `mdadm`: require `mdadm` and use it for aggregation if multiple disks are found, exiting with error if `mdadm` is not installed (regardless of number of disks) `auto`: use `mdadm` for aggregation only if installed, otherwise fall back to use a single disk `none`: never aggregate disks, only use one Default: `auto` * **AZURE\_EPHEMERAL\_DISK\_SETUP\_FS\_TYPE** Supported values: `ext4`, `xfs` Default: `ext4` (universally available; `xfs` requires `xfsprogs`) * **AZURE\_EPHEMERAL\_DISK\_SETUP\_MDADM\_CHUNK** Supported values: Any valid mdadm chunk size Default: `512K` (mdadm default) * **AZURE\_EPHEMERAL\_DISK\_SETUP\_MDADM\_NAME** Supported values: Any valid mdadm name Default: `azure-ephemeral-md` * **AZURE\_EPHEMERAL\_DISK\_SETUP\_MOUNT\_POINT** Supported values: Any valid mount point path Default: `/mnt` (matches cloud-init default mount point) * **AZURE\_EPHEMERAL\_DISK\_SETUP\_SCSI\_RESOURCE** Supported values: `true`, `false` Default: `false` (minimizes risk of conflict with cloud-init and WALinuxAgent) * **AZURE\_EPHEMERAL\_DISK\_SETUP\_SYSTEMD\_UNIT\_TIMEOUT\_SECS** Supported values: positive integer (>=1) Default: 60 * **AZURE\_EPHEMERAL\_DISK\_SETUP\_UDEVADM\_SETTLE\_TIMEOUT\_SECS** Supported values: positive integation (>=1) Default: 60 # OPERATION The service performs the following steps: ### 1. Load and Validate Configuration * Configuration is read via filtered `source /etc/azure-ephemeral-disk-setup.conf`, allowing only safe environment-style assignments. * If invalid values are detected, the service exits with error. * Required tools (`mkfs.${AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE}`, `mdadm`) must be available. ### 2. Check for Existing Mounts If the mount point is already configured: * with a matching `/etc/fstab` line containing the comment `comment=azure-ephemeral-disk-setup`, the service will wait for mount to complete. * if mount succeeds, service exits with success. * if mount does not complete, this service will check if the ephemeral disks have been reallocated and reconfigure them via the standard detection/validation process below. * by cloud-init and source is `/dev/disk/cloud/azure_resource-part1`: * if `AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=false`, the service exits with success allowing cloud-init to manage the disk. * if `AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true`, the service exits with error as the entry is either stale or conflicting. * by other tools/services/configuration, the service exits with error. ### 3. Detect and Validate Disks **NVMe** disks are detected via: * Model names: * Microsoft NVMe Direct Disk * Microsoft NVMe Direct Disk v2 **SCSI resource disk** is detected and validated if `AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true` and no ephemeral NVMe disks are detected. See **SAFETY AND VALIDATION** section for more details on how disks are validated to ensure no data is lost. ### 4. Disk Setup Each candidate disk is checked to ensure: * It is a block device * It is not mounted * It is not partitioned (unless NTFS resource disk) * It has no existing filesystem (unless NTFS resource disk) If any disk fails these checks, the service aborts with a detailed error. ### 5. Aggregation with RAID-0 For VMs with Multiple local NVMe Disks If more than one NVMe disk is found and aggregation is enabled with mdadm: ```bash mdadm --create "/dev/md/${AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME}_0" \ --level=0 \ --raid-devices=N \ --metadata=1.2 \ --chunk="$AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK" \ --name="$AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME" \ ${DISKS[@]} ``` Configuration is **not** written to `/etc/mdadm/mdadm.conf` in favor of kernel auto-detection. ### 6. Format Filesystem * Formats target device (either RAID device or single disk) directly **without partitioning** * Uses `mkfs.ext4 [-F] -L AzureEphmDsk ...` or `mkfs.xfs [-f] -L AzureEphmDsk ...` with the force flag only if it is reformatting the SCSI resource disk ### 7. Persistenting Mount If `/etc/fstab` is writable: * Previous entries with `comment=azure-ephemeral-disk-setup` are removed * New entry added: ```fstab LABEL=AzureEphmDsk defaults,nofail,comment=azure-ephemeral-disk-setup 0 2 ``` If `/etc/fstab` is read-only or does not exist, it will not be updated. It will be mounted by this service on every boot. ### 8. Mounting For persistent mounts: * Tries to mount using systemd (`systemctl start `) * Falls back to direct `mount` call if mount unit fails to start For non-persitent mounts, a `mount` call is used. # SAFETY AND VALIDATION This service only formats NVMe disks that are: * Not mounted * Unpartitioned * Unformatted SCSI resource disks are only reformatted if it: * Has a single NTFS partition labeled `Temporary Storage` * Is empty except for `dataloss_warning_readme.txt` and `System Volume Information` * To check if disk is empty, this service will attempt to mount the resource disk partition read-only via `ntfs3`, with fallback to `ntfs`. If both fail due to lack of driver, it is assumed safe to reformat. Calls to mkfs and mdadm do not use `--force` for formatting or mdadm array creation unless reformatting validated NTFS SCSI disk. # EXIT STATUS * `0` — Success * `1` — Any error (invalid config, conflicting mount, unsafe disk, etc.) # FSTAB STRATEGY The following values are used when generating an `/etc/fstab` entry for the ephemeral disk: * **fs\_spec**: `LABEL=AzureEphmDsk` Refers to the disk label. This assumes the script has ownership of the `AzureEphmDsk` label. * **fs\_vfstype**: `$AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE` The filesystem type, determined by configuration (e.g., `ext4` or `xfs`). * **fs\_mntops**: `defaults,nofail,comment=azure-ephemeral-disk-setup` * `defaults`: standard mount options * `nofail`: ensures the VM boots even if the ephemeral disk is missing or has been re-allocated * `comment=azure-ephemeral-disk-setup`: tags the entry to show it's managed by this setup * **fs\_freq**: `0` Disables `dump` backups for the filesystem. * **fs\_passno**: `2` Sets the order for `fsck` during boot. Non-root filesystems typically use `2`. # AVOIDING CONFLICTS WITH CLOUD-INIT If `AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=false`, this service will exit with success if cloud-init is managing the SCSI resource disk at the same mount point. Given the widespread usage of cloud-init for managing the SCSI resource disk, this is the default case. If `AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true`, cloud-init management of the resource disk must be disabled. There are two options for disabling cloud-init's management of SCSI resource disk: ## Disabling cloud-init management of SCSI resource disk via udev rules Disable udev rules responsible for creating the /dev/disk/cloud symlinks: ```bash ln -sf /dev/null /etc/udev/rules.d/66-azure-ephemeral.rules ``` If cloud-init doesn't detect the device, it will not attempt to format/mount it. ## Disabling cloud-init management of SCSI resource disk via cloud-init configuration (cloud-init >= 25.3) Work is ongoing to introduce a flag to the Azure datasource to control behavior. See [pull request](https://github.com/canonical/cloud-init/pull/6323). This is expected to be available in cloud-init 25.3. # AVOIDING CONFLICTS WITH WALINUXAGENT WALinuxAgent must be configured with `ResourceDisk.Format=n`. If `AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true`, this service will exit with error if `/etc/waagent.conf` sets `ResourceDisk.Format=y`. # EXAMPLES Basic usage: ```bash sudo azure-ephemeral-disk-setup ``` Enable management of SCSI resource disk: ```conf # /etc/azure-ephemeral-disk-setup.conf AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true ``` # FILES * `/etc/azure-ephemeral-disk-setup.conf` — user configuration * `/etc/fstab` — mount persistence # SERVICE ORDERING `DefaultDependencies=no` to avoid typical service dependencies as we want to start early. After: - `cloud-init.service` (a.k.a. `cloud-init-network.service`) allows users to customize service configuration using cloud-config - `local-fs.target` ensures local filesystems are mounted (necessary if no cloud-init) Before: - `cloud-init.target` ensures we start before cloud-init completes which services may use to determine if system is fully configured - `walinuxagent.service waagent.service` ensures we start prior to walinuxagent and avoid conflicts - `network-online.target` to ensure we start before networking is considered online - `sshd.service` to ensure we start before sshd starts accepting connections - `systemd-user-sessions.service` to ensure we start before user sessions are enabled - `sysinit.target` to ensure we run before most services requiring mounts (Debian/Ubuntu only) # DEBUGGING All logs are written to stderr and accessible with: ```bash journalctl -u azure-ephemeral-disk-setup.service ``` # SEE ALSO **mdadm(8)**, **mount(8)**, **cloud-init(8)**, **waagent(8)** Project Home: [https://github.com/Azure/azure-vm-utils](https://github.com/Azure/azure-vm-utils) azure-vm-utils-0.8.0/doc/azure-nvme-id.8.in000066400000000000000000000111651527236310300203570ustar00rootroot00000000000000.TH "azure-nvme-id" "8" "October 30, 2026" "azure-nvme-id @VERSION@" "User Manual" .hy .SH NAME .PP azure-nvme-id - identify Azure NVMe devices .SH SYNOPSIS .PP \f[B]azure-nvme-id\f[R] [--debug] [--format {plain|json}] [--help | --version | --udev] .SH DESCRIPTION .PP \f[B]azure-nvme-id\f[R] provides identification metadata in the response to Identify Namespace command for some models of NVMe devices. This is found in vendor-specific (vs) field which contains various identification details with a comma-separated, key=value format. .PP \f[B]azure-nvme-id\f[R] combines this metadata with the make and model of NVMe device namespaces to identify a device by type, name, index, etc. .PP Diagnostics, including underlying system errors, are written to stderr. .SH DEVICE ENUMERATION .PP Without \f[CB]--udev\f[R], \f[B]azure-nvme-id\f[R] enumerates Microsoft NVMe controllers under \f[CB]/sys/class/nvme\f[R] and identifies their namespaces. Output options are plain or json. .PP JSON output includes \f[CB]path\f[R], \f[CB]nsid\f[R], \f[CB]model\f[R], \f[CB]properties\f[R], and \f[CB]vs\f[R] for each namespace. .PP These errors are reported to stderr but do not stop enumeration: .IP \[bu] 2 Missing \f[CB]/sys/class/nvme\f[R] means no controllers. A missing controller directory means no namespaces for that controller. .IP \[bu] 2 An unreadable \f[CB]device/vendor\f[R] or \f[CB]model\f[R] file, or an invalid vendor ID, skips the controller. .IP \[bu] 2 A failed model reread during fallback identification leaves properties empty. .IP \[bu] 2 \f[CB]lun\f[R] or \f[CB]index\f[R] values that cannot be converted to integers stay strings in JSON output. .PP Exit status \f[CB]0\f[R] does not guarantee that every device was identified. .SH UDEV DEVICE IDENTIFICATION .PP The \f[CB]--udev\f[R] option identifies a single NVMe namespace device specified by the required \f[CB]DEVNAME\f[R] environment variable instead of enumerating controllers. It queries that device directly, without sysfs vendor filtering or model-based fallback identification. .PP Output is a set of \f[CB]=\f[R] variables for consumption by udev rules. \f[CB]AZURE_DISK_VS\f[R] contains the vendor-specific data. Each property in that data is also printed with its key converted to uppercase and prefixed with \f[CB]AZURE_DISK_\f[R], such as \f[CB]AZURE_DISK_TYPE\f[R], \f[CB]AZURE_DISK_INDEX\f[R], and \f[CB]AZURE_DISK_NAME\f[R]. .SH OPTIONS .TP \f[CB]--debug\f[R] Debug mode with additional logging. .TP \f[CB]--format {plain|json}\f[R] Output format, default is plain. .TP \f[CB]--help\f[R] Show usage information and exit. .TP \f[CB]--udev\f[R] Run in udev mode. See UDEV DEVICE IDENTIFICATION. .TP \f[CB]--version\f[R] Show version information and exit. .SH EXIT STATUS .TP \f[CB]0\f[R] Success, including \f[CB]--help\f[R], \f[CB]--version\f[R], or finding no matching devices. .TP \f[CB]1\f[R] Invalid command-line arguments. .TP \f[CB]2\f[R] Failure to scan \f[CB]/sys/class/nvme\f[R], except for \f[CB]ENOENT\f[R], which is treated as success. .TP \f[CB]3\f[R] Failure to scan \f[CB]/sys/class/nvme/\f[R] for namespaces, except for \f[CB]ENOENT\f[R], which is treated as success. .TP \f[CB]4\f[R] Memory allocation failure in namespace identification, udev property processing, or JSON processing, including JSON construction and serialization. .TP \f[CB]5\f[R] Failure to open an NVMe namespace device, either to obtain its namespace ID or to execute Identify Namespace. .TP \f[CB]6\f[R] An ioctl error executing the NVMe Identify Namespace command. .TP \f[CB]7\f[R] The NVMe Identify Namespace command returned a nonzero NVMe completion status. The status is printed in hexadecimal on stderr; it is not used directly as the process exit status. .TP \f[CB]8\f[R] An ioctl error obtaining a namespace ID with \f[CB]NVME_IOCTL_ID\f[R]. .TP \f[CB]9\f[R] Failure to obtain the page size with \f[CB]sysconf()\f[R] for Identify Namespace. .TP \f[CB]10\f[R] Malformed vendor-specific key-value properties in udev mode. .TP \f[CB]11\f[R] The \f[CB]DEVNAME\f[R] environment variable is not set in udev mode. .SH EXAMPLES .PP Identify NVMe namespaces: .IP .nf \f[CR] $ sudo azure-nvme-id /dev/nvme0n1: /dev/nvme1n1: type=local,index=1,name=nvme-110G-1 \f[R] .fi .PP Parse device identifiers for udev consumption: .IP .nf \f[CR] $ sudo env DEVNAME=/dev/nvme1n1 azure-nvme-id --udev AZURE_DISK_VS=type=local,index=1,name=nvme-110G-1 AZURE_DISK_TYPE=local AZURE_DISK_INDEX=1 AZURE_DISK_NAME=nvme-110G-1 \f[R] .fi .PP Check \f[CB]azure-nvme-id\f[R] version: .IP .nf \f[CR] $ azure-nvme-id --version azure-nvme-id 0.1.2 \f[R] .fi .SH SEE ALSO .PP Source and documentation available at: azure-vm-utils-0.8.0/doc/azure-nvme-id.md000066400000000000000000000101401527236310300201730ustar00rootroot00000000000000--- title: azure-nvme-id section: 8 header: User Manual footer: azure-nvme-id __VERSION__ date: __DATE__ --- # NAME azure-nvme-id - identify Azure NVMe devices # SYNOPSIS **azure-nvme-id** [\-\-debug] [\-\-format {plain|json}] [\-\-help | \-\-version | \-\-udev] # DESCRIPTION **azure-nvme-id** provides identification metadata in the response to Identify Namespace command for some models of NVMe devices. This is found in vendor-specific (vs) field which contains various identification details with a comma-separated, key=value format. **azure-nvme-id** combines this metadata with the make and model of NVMe device namespaces to identify a device by type, name, index, etc. Diagnostics, including underlying system errors, are written to stderr. # DEVICE ENUMERATION Without `--udev`, **azure-nvme-id** enumerates Microsoft NVMe controllers under `/sys/class/nvme` and identifies their namespaces. Output options are plain or json. JSON output includes `path`, `nsid`, `model`, `properties`, and `vs` for each namespace. These errors are reported to stderr but do not stop enumeration: * Missing `/sys/class/nvme` means no controllers. A missing controller directory means no namespaces for that controller. * An unreadable `device/vendor` or `model` file, or an invalid vendor ID, skips the controller. * A failed model reread during fallback identification leaves properties empty. * `lun` or `index` values that cannot be converted to integers stay strings in JSON output. Exit status `0` does not guarantee that every device was identified. # UDEV DEVICE IDENTIFICATION The `--udev` option identifies a single NVMe namespace device specified by the required `DEVNAME` environment variable instead of enumerating controllers. It queries that device directly, without sysfs vendor filtering or model-based fallback identification. Output is a set of `=` variables for consumption by udev rules. `AZURE_DISK_VS` contains the vendor-specific data. Each property in that data is also printed with its key converted to uppercase and prefixed with `AZURE_DISK_`, such as `AZURE_DISK_TYPE`, `AZURE_DISK_INDEX`, and `AZURE_DISK_NAME`. # OPTIONS `--debug` : Debug mode with additional logging. `--format {plain|json}` : Output format, default is plain. `--help` : Show usage information and exit. `--udev` : Run in udev mode. See UDEV DEVICE IDENTIFICATION. `--version` : Show version information and exit. # EXIT STATUS `0` : Success, including `--help`, `--version`, or finding no matching devices. `1` : Invalid command-line arguments. `2` : Failure to scan `/sys/class/nvme`, except for `ENOENT`, which is treated as success. `3` : Failure to scan `/sys/class/nvme/` for namespaces, except for `ENOENT`, which is treated as success. `4` : Memory allocation failure in namespace identification, udev property processing, or JSON processing, including JSON construction and serialization. `5` : Failure to open an NVMe namespace device, either to obtain its namespace ID or to execute Identify Namespace. `6` : An ioctl error executing the NVMe Identify Namespace command. `7` : The NVMe Identify Namespace command returned a nonzero NVMe completion status. The status is printed in hexadecimal on stderr; it is not used directly as the process exit status. `8` : An ioctl error obtaining a namespace ID with `NVME_IOCTL_ID`. `9` : Failure to obtain the page size with `sysconf()` for Identify Namespace. `10` : Malformed vendor-specific key-value properties in udev mode. `11` : The `DEVNAME` environment variable is not set in udev mode. # EXAMPLES Identify NVMe namespaces: ```bash $ sudo azure-nvme-id /dev/nvme0n1: /dev/nvme1n1: type=local,index=1,name=nvme-110G-1 ``` Parse device identifiers for udev consumption: ```bash $ sudo env DEVNAME=/dev/nvme1n1 azure-nvme-id --udev AZURE_DISK_VS=type=local,index=1,name=nvme-110G-1 AZURE_DISK_TYPE=local AZURE_DISK_INDEX=1 AZURE_DISK_NAME=nvme-110G-1 ``` Check `azure-nvme-id` version: ```bash $ azure-nvme-id --version azure-nvme-id 0.1.2 ``` # SEE ALSO Source and documentation available at: azure-vm-utils-0.8.0/doc/azure-vm-utils-selftest.8.in000066400000000000000000000017051527236310300224260ustar00rootroot00000000000000.TH "azure-vm-utils-selftest" "8" "October\ 29,\ 2026" "azure-vm-utils-selftest @VERSION@" "User Manual" .hy .SH NAME .PP azure-vm-utils-selftest - packaging self-tests .SH SYNOPSIS .PP \f[B]azure-vm-utils-selftest [--skip-imds-validation] [--skip-udev-validation]\f[R] .SH DESCRIPTION .PP \f[B]azure-vm-utils-selftest\f[R] validates functionailty related to azure-vm-utils package. .PP These self-tests validate azure-nvme-id outputs and /dev/disk/azure symlinks. .SH EXAMPLES .PP Running self tests requires root: .IP .nf \f[CR] $ sudo azure-vm-utils-selftest \f[R] .fi .PP To run inside Azure VM without a reboot after install: .IP .nf \f[CR] azure-vm-utils-selftest --skip-udev-validation \f[R] .fi .PP To run outside of Azure VM: .IP .nf \f[CR] azure-vm-utils-selftest --skip-imds-validation --skip-udev-validation \f[R] .fi .PP Hopefully it exits successfully! .SH SEE ALSO .PP Source and documentation available at: azure-vm-utils-0.8.0/doc/azure-vm-utils-selftest.md000066400000000000000000000016171527236310300222540ustar00rootroot00000000000000--- title: azure-vm-utils-selftest section: 8 header: User Manual footer: azure-vm-utils-selftest __VERSION__ date: __DATE__ --- # NAME azure-vm-utils-selftest - packaging self-tests # SYNOPSIS **azure-vm-utils-selftest [\-\-skip-imds-validation] [\-\-skip-udev-validation]** # DESCRIPTION **azure-vm-utils-selftest** validates functionailty related to azure-vm-utils package. These self-tests validate azure\-nvme\-id outputs and /dev/disk/azure symlinks. # EXAMPLES Running self tests requires root: ```bash $ sudo azure-vm-utils-selftest ``` To run inside Azure VM without a reboot after install: ```bash azure-vm-utils-selftest --skip-udev-validation ``` To run outside of Azure VM: ```bash azure-vm-utils-selftest --skip-imds-validation --skip-udev-validation ``` Hopefully it exits successfully! # SEE ALSO Source and documentation available at: azure-vm-utils-0.8.0/doc/disk-identification.md000066400000000000000000000251151527236310300214410ustar00rootroot00000000000000# Azure VM Utils - Disk Identification ## Introduction azure-vm-utils is a new package to consolidate tools and udev rules which are critical to the Linux on Azure experience. The initial focus for azure-vm-utils is to provide the tools and configuration to help the customer identify the various NVMe disk devices. ## Background There are number of udev rules critical to the Linux on Azure experience, to assist managing devices including: SCSI, NVMe, MANA, Mellanox. Today these rules are spread out among cloud-init, WALinuxAgent, azure-nvme-utils, and vendor-specific image customization. ### azure-nvme-utils The precursor to azure-vm-utils, renamed to azure-vm-utils to support increased scope without requiring additional packages. ### WALinuxAgent The WALinuxAgent team is working to decouple their guest agent from the provisioning agent. The provisioning agent is being deprecated and will be removed in a future release. The long-term plan is to migrate the udev rules and configuration found in WALinuxAgent into azure-vm-utils so they can be maintained and updated independently of the WALinuxAgent package. Today, WALinuxAgent includes udev rules to provide several symlinks for SCSI disks, including: - /dev/disk/azure/resource - /dev/disk/azure/root - /dev/disk/azure/scsi0/lun\ - /dev/disk/azure/scsi1/lun\ ## Azure NVMe Controllers Azure currently supports three NVMe disk controllers: - MSFT NVMe Accelerator v1 - Provides remote NVMe disks attached to the VM including OS and data disks. - OS and data disks may share a controller or use separate controllers, depending on the VM size. - Microsoft NVMe Direct Disk v1 - Earlist generation of VMs with directly-attached NVMe local disks, e.g. LSv2/LSv3. - Each local disk has its own controller. - Microsoft NVMe Direct Disk v2 - Second generation of NVMe directly-attached local disks used in v6 and newer VM sizes. - Each local disk has its own controller. ## NVMe Identify Namespace: Vendor-Specific Reserved NVMe' identify namespace command returns a structure with a vendor-specific (VS) field found at offset 384 and is 3712 bytes long. Azure utilizes this space to include some metadata about the device, including the disk's: - type: specifies the type of NVMe device (os, data, local) - index: indicates the specific device's index within a set, if applicable (local) - lun: customer-configured "lun" for data disks - name: customer-configured name for data disks, or designated name for local disks The metadata is formatted as comma-separated key-value pairs truncated by a null-byte. Example VS metadata includes: - type=local,index=1,name=nvme-300G-1\0 - type=os\0 - type=data,lun=4,name=data-db\0 If VS starts with a null-terminator, the protocol is unsupported. This metadata is currently supported by Microsoft NVMe Direct Disk v2 and will be extended in the future. ## Disk Controller Identification For NVMe Disks without VS-based Identification For NVMe devices which do not (yet) support identification via metadata in Identify Namespace, some identifiers are derived from a combination of NVMe controller identifier and namespace identifier. The following are assumed to be local disks: - Microsoft NVMe Direct Disk v1 - Microsoft NVMe Direct Disk v2 The following is assumed to be OS disk: - MSFT NVMe Accelerator v1, namespace id = 1 The following are assumed to be data disks: - MSFT NVMe Accelerator v1, namespace id = 2+. The "lun" for a data disk can be calculated by subtracting 2 from the namespace id. This calculation may be different for Windows VMs. ## Disk Device Identification on Linux The rules found in WALinuxAgent are being extended with azure-vm-utils to add identification support for NVMe devices. New symlinks that will be provided for all instances with NVMe disks include: - /dev/disk/azure/data/by-lun/\ - /dev/disk/azure/local/by-serial/\ - /dev/disk/azure/os For v6 and newer VM sizes with local NVMe disks supporting namespace identifiers, additional links will be available: - /dev/disk/azure/local/by-index/\ - /dev/disk/azure/local/by-name/\ For future VM sizes with remote NVMe disks supporting namespace identifiers, additional links will be available: - /dev/disk/azure/data/by-name/\ ### SCSI Compatibility For compatibility reasons, azure-vm-utils will ensure SCSI disks support the following links: - /dev/disk/azure/os - /dev/disk/azure/resource Note that some VM sizes come with both NVMe "local" disks in addition to a SCSI "temp" resource disk. These temp resource disks are considered to be separate from "local" disks which are dedicated to local NVMe disks to avoid confusion. ### Disk Identification Examples for Various VM Sizes & Configurations #### Standard_D2s_v4 + 4 Data Disks ```bash $ find /dev/disk/azure -type l | sort /dev/disk/azure/data/by-lun/0 /dev/disk/azure/data/by-lun/1 /dev/disk/azure/data/by-lun/2 /dev/disk/azure/data/by-lun/3 /dev/disk/azure/os /dev/disk/azure/os-part1 /dev/disk/azure/os-part14 /dev/disk/azure/os-part15 /dev/disk/azure/root /dev/disk/azure/root-part1 /dev/disk/azure/root-part14 /dev/disk/azure/root-part15 /dev/disk/azure/scsi1/lun0 /dev/disk/azure/scsi1/lun1 /dev/disk/azure/scsi1/lun2 /dev/disk/azure/scsi1/lun3 $ sudo azure-nvme-id ``` #### Standard_D2ds_v4 configured with 4 data disks ```bash $ find /dev/disk/azure -type l | sort /dev/disk/azure/data/by-lun/0 /dev/disk/azure/data/by-lun/1 /dev/disk/azure/data/by-lun/2 /dev/disk/azure/data/by-lun/3 /dev/disk/azure/os /dev/disk/azure/os-part1 /dev/disk/azure/os-part14 /dev/disk/azure/os-part15 /dev/disk/azure/resource /dev/disk/azure/resource-part1 /dev/disk/azure/root /dev/disk/azure/root-part1 /dev/disk/azure/root-part14 /dev/disk/azure/root-part15 /dev/disk/azure/scsi1/lun0 /dev/disk/azure/scsi1/lun1 /dev/disk/azure/scsi1/lun2 /dev/disk/azure/scsi1/lun3 $ sudo azure-nvme-id ``` #### Standard_E2bds_v5 configured with SCSI disk controller and 4 data disks ```bash $ find /dev/disk/azure -type l | sort /dev/disk/azure/data/by-lun/0 /dev/disk/azure/data/by-lun/1 /dev/disk/azure/data/by-lun/2 /dev/disk/azure/data/by-lun/3 /dev/disk/azure/os /dev/disk/azure/os-part1 /dev/disk/azure/os-part14 /dev/disk/azure/os-part15 /dev/disk/azure/resource /dev/disk/azure/resource-part1 /dev/disk/azure/root /dev/disk/azure/root-part1 /dev/disk/azure/root-part14 /dev/disk/azure/root-part15 /dev/disk/azure/scsi1/lun0 /dev/disk/azure/scsi1/lun1 /dev/disk/azure/scsi1/lun2 /dev/disk/azure/scsi1/lun3 $ sudo azure-nvme-id ``` #### Standard_E2bds_v5 configured with NVMe disk controller and 4 data disks ```bash $ find /dev/disk/azure -type l | sort /dev/disk/azure/data/by-lun/0 /dev/disk/azure/data/by-lun/1 /dev/disk/azure/data/by-lun/2 /dev/disk/azure/data/by-lun/3 /dev/disk/azure/os /dev/disk/azure/os-part1 /dev/disk/azure/os-part14 /dev/disk/azure/os-part15 /dev/disk/azure/resource /dev/disk/azure/resource-part1 $ sudo azure-nvme-id /dev/nvme0n1: /dev/nvme0n2: /dev/nvme0n3: /dev/nvme0n4: /dev/nvme0n5: ``` #### Standard_E2bs_v5 configured with NVMe disk controller and 4 data disks ```bash $ find /dev/disk/azure -type l | sort /dev/disk/azure/data/by-lun/0 /dev/disk/azure/data/by-lun/1 /dev/disk/azure/data/by-lun/2 /dev/disk/azure/data/by-lun/3 /dev/disk/azure/os /dev/disk/azure/os-part1 /dev/disk/azure/os-part14 /dev/disk/azure/os-part15 $ sudo azure-nvme-id /dev/nvme0n1: /dev/nvme0n2: /dev/nvme0n3: /dev/nvme0n4: /dev/nvme0n5: ``` #### Standard_L8s_v3 configured with 4 data disks (1 local NVMe disk + 1 temp SCSI disk) ```bash $ find /dev/disk/azure -type l | sort /dev/disk/azure/data/by-lun/0 /dev/disk/azure/data/by-lun/1 /dev/disk/azure/data/by-lun/2 /dev/disk/azure/data/by-lun/3 /dev/disk/azure/local/by-serial/70307ea5392400000001 /dev/disk/azure/os /dev/disk/azure/os-part1 /dev/disk/azure/os-part14 /dev/disk/azure/os-part15 /dev/disk/azure/resource /dev/disk/azure/resource-part1 /dev/disk/azure/root /dev/disk/azure/root-part1 /dev/disk/azure/root-part14 /dev/disk/azure/root-part15 /dev/disk/azure/scsi1/lun0 /dev/disk/azure/scsi1/lun1 /dev/disk/azure/scsi1/lun2 /dev/disk/azure/scsi1/lun3 $ sudo azure-nvme-id /dev/nvme0n1: ``` #### Standard_L8s_v3 configured with 4 data disks (4 local NVMe disks + 1 temp SCSI disk) ```bash $ find /dev/disk/azure -type l | sort /dev/disk/azure/data/by-lun/0 /dev/disk/azure/data/by-lun/1 /dev/disk/azure/data/by-lun/2 /dev/disk/azure/data/by-lun/3 /dev/disk/azure/local/by-serial/f0451a1ba53a00000001 /dev/disk/azure/local/by-serial/f0451a1ba53a00000002 /dev/disk/azure/local/by-serial/f0451a1ba53a00000003 /dev/disk/azure/local/by-serial/f0451a1ba53a00000004 /dev/disk/azure/os /dev/disk/azure/os-part1 /dev/disk/azure/os-part14 /dev/disk/azure/os-part15 /dev/disk/azure/resource /dev/disk/azure/resource-part1 /dev/disk/azure/root /dev/disk/azure/root-part1 /dev/disk/azure/root-part14 /dev/disk/azure/root-part15 /dev/disk/azure/scsi1/lun0 /dev/disk/azure/scsi1/lun1 /dev/disk/azure/scsi1/lun2 /dev/disk/azure/scsi1/lun3 $ sudo azure-nvme-id /dev/nvme0n1: /dev/nvme1n1: /dev/nvme2n1: /dev/nvme3n1: ``` #### Standard_D2alds_v6 configured with 2 data disks (1 local NVMe disk) ```bash $ find /dev/disk/azure -type l | sort /dev/disk/azure/data/by-lun/0 /dev/disk/azure/data/by-lun/1 /dev/disk/azure/local/by-index/1 /dev/disk/azure/local/by-name/nvme-440G-1 /dev/disk/azure/local/by-serial/ebeb91bd841bceb90001 /dev/disk/azure/os /dev/disk/azure/os-part1 /dev/disk/azure/os-part14 /dev/disk/azure/os-part15 $ sudo azure-nvme-id /dev/nvme0n1: /dev/nvme0n2: /dev/nvme0n3: /dev/nvme1n1: type=local,index=1,name=nvme-110G-1 ``` #### Standard_D16alds_v6 configured with 4 data disks (4 local NVMe disks) ```bash $ find /dev/disk/azure -type l | sort /dev/disk/azure/data/by-lun/0 /dev/disk/azure/data/by-lun/1 /dev/disk/azure/data/by-lun/2 /dev/disk/azure/data/by-lun/3 /dev/disk/azure/local/by-index/1 /dev/disk/azure/local/by-index/2 /dev/disk/azure/local/by-index/3 /dev/disk/azure/local/by-index/4 /dev/disk/azure/local/by-name/nvme-440G-1 /dev/disk/azure/local/by-name/nvme-440G-2 /dev/disk/azure/local/by-name/nvme-440G-3 /dev/disk/azure/local/by-name/nvme-440G-4 /dev/disk/azure/local/by-serial/351ea1d05ea261d50001 /dev/disk/azure/local/by-serial/351ea1d05ea261d50002 /dev/disk/azure/local/by-serial/351ea1d05ea261d50003 /dev/disk/azure/local/by-serial/351ea1d05ea261d50004 /dev/disk/azure/os /dev/disk/azure/os-part1 /dev/disk/azure/os-part14 /dev/disk/azure/os-part15 $ sudo azure-nvme-id /dev/nvme0n1: /dev/nvme0n2: /dev/nvme0n3: /dev/nvme0n4: /dev/nvme0n5: /dev/nvme1n1: type=local,index=2,name=nvme-440G-2 /dev/nvme2n1: type=local,index=3,name=nvme-440G-3 /dev/nvme3n1: type=local,index=1,name=nvme-440G-1 /dev/nvme4n1: type=local,index=4,name=nvme-440G-4 ``` azure-vm-utils-0.8.0/doc/pandoc.template000066400000000000000000000007151527236310300201760ustar00rootroot00000000000000$if(has-tables)$ .\"t $endif$ $if(pandoc-version)$ $endif$ $if(adjusting)$ .ad $adjusting$ $endif$ .TH "$title/nowrap$" "$section/nowrap$" "$date/nowrap$" "$title/nowrap$ @VERSION@" "$header/nowrap$" $if(hyphenate)$ .hy $else$ .nh $endif$ $for(header-includes)$ $header-includes$ $endfor$ $for(include-before)$ $include-before$ $endfor$ $body$ $for(include-after)$ $include-after$ $endfor$ $if(author)$ .SH AUTHORS $for(author)$$author$$sep$; $endfor$. $endif$ azure-vm-utils-0.8.0/ephemeral-disk-setup/000077500000000000000000000000001527236310300204555ustar00rootroot00000000000000azure-vm-utils-0.8.0/ephemeral-disk-setup/azure-ephemeral-disk-setup000077500000000000000000000513761527236310300255730ustar00rootroot00000000000000#!/bin/bash VERSION='@VERSION@' if [[ "$1" == "--version" ]]; then echo "$VERSION" exit 0 fi set -xeuo pipefail CONFIGURATION_FILE="@AZURE_EPHEMERAL_DISK_SETUP_CONF_INSTALL_DIR@/azure-ephemeral-disk-setup.conf" FSTAB_COMMENT="comment=azure-ephemeral-disk-setup" SCSI_RESOURCE_DISK_PATH="/dev/disk/azure/resource" SCSI_RESOURCE_DISK_FORCE_REFORMAT="false" FILESYSTEM_LABEL="AzureEphmDsk" DISK_BY_LABEL_PATH="/dev/disk/by-label/$FILESYSTEM_LABEL" log() { echo "$1" >&2 } exit_failure() { echo "$1" exit 1 } exit_success() { echo "$1" exit 0 } check_if_block_device_needs_setup() { local resolved resolved="$(readlink -f "$disk")" log "Checking disk: $disk -> $resolved" if [[ ! -b "$resolved" ]]; then exit_failure "Not a valid block device: $disk" fi # Check if disk is currently mounted. if findmnt --noheadings --output SOURCE,TARGET,FSTYPE,OPTIONS --source "$disk" >&2; then exit_failure "Device $disk is already mounted or in use" fi # Check if it has partitions. local partitions partition_count mapfile -t partitions < <(lsblk --list --paths -o NAME,TYPE "$disk" 2>/dev/null | awk '$2 == "part" {print $1}') partition_count=${#partitions[@]} # Special handling for SCSI resource disk. if [[ "$disk" == "$SCSI_RESOURCE_DISK_PATH" && "$partition_count" -eq 1 ]]; then check_if_scsi_resource_device_needs_setup "$disk" "${partitions[0]}" SCSI_RESOURCE_DISK_FORCE_REFORMAT="true" return fi # If disk has partitions, fail setup. if [[ "$partition_count" -gt 0 ]]; then exit_failure "Device $disk has $partition_count partition(s)" fi # Check if disk is formatted (has a filesystem). if blkid "$disk" >&2; then exit_failure "Device $disk contains a partition table or is already formatted" fi # Check if disk is partitioned/formatted with parted. if parted -s "$disk" print >&2; then exit_failure "Device $disk is partitioned or formatted" fi log "Device $disk is not partitioned or formatted, proceeding with setup" } # Check if SCSI resource disk is safe to use. Only return it if is. check_if_scsi_resource_device_needs_setup() { local disk="$1" local partition="$2" local existing_mount existing_mount="$(findmnt --noheadings --output SOURCE,TARGET,FSTYPE,OPTIONS --source "$partition" || true)" if [[ -n "$existing_mount" ]]; then exit_failure "Resource disk partition $partition is already mounted or in use" fi local type type="$(blkid -o value -s TYPE "$partition" 2>/dev/null)" if [[ "$type" != "ntfs" ]]; then exit_failure "Resource disk partition $partition has type=$type, expected type=ntfs" fi local label label="$(blkid -o value -s LABEL "$partition" 2>/dev/null)" if [[ "$label" != "Temporary Storage" ]]; then exit_failure "Resource disk partition $partition has label=$label, expected label=Temporary Storage" fi # Some distributions only have ntfs3 and some have neither (e.g. RHEL 9.4). # If we can't mount it as ntfs3 or ntfs, we assume it is safe to reformat. local temp_mount_path temp_mount_path="$(mktemp -d)" local mount_rc=0 local mount_output mount_output=$(mount -t ntfs3 "$partition" "$temp_mount_path" 2>&1) || mount_rc=$? log "Mount (ntfs3) exited with $mount_rc: $mount_output" if [[ $mount_rc -ne 0 ]]; then if [[ "$mount_output" == *"unknown filesystem type 'ntfs3'"* ]]; then mount_rc=0 mount_output=$(mount -t ntfs "$partition" "$temp_mount_path" 2>&1) || mount_rc=$? log "Mount (ntfs) exited with code=$mount_rc: $mount_output" if [[ $mount_rc -ne 0 ]]; then if [[ "$mount_output" == *"unknown filesystem type 'ntfs'"* ]]; then log "WARNING: failed to mount $partition due to lack of ntfs support, assuming it is empty and safe for reformat" return 0 fi exit_failure "Mounting $partition as ntfs failed" fi else exit_failure "Mounting $partition as ntfs3 failed" fi fi local unexpected_files mapfile -t unexpected_files < <(find "$temp_mount_path" -mindepth 1 -maxdepth 1 \ ! -iname 'dataloss_warning_readme.txt' \ ! -iname 'system volume information' \ -printf '%P\n') umount "$temp_mount_path" || exit_failure "Unmounting NTFS $partition failed" rmdir "$temp_mount_path" if [[ ${#unexpected_files[@]} -gt 0 ]]; then exit_failure "SCSI resource disk $disk is NTFS formatted but contains unexpected files or folders: ${unexpected_files[*]}" fi log "SCSI resource disk $disk is NTFS formatted and safe to reformat" } # Read configuration file and export key=value pairs. load_config() { if [ ! -r "${CONFIGURATION_FILE}" ]; then echo "Configuration file not found or readable: $CONFIGURATION_FILE" return 1 fi while IFS='=' read -r key value; do # Skip empty lines and comments. [[ -z "$key" || "$key" =~ ^[[:space:]]*# ]] && continue # Remove leading and trailing spaces. key="${key#"${key%%[![:space:]]*}"}" value="${value#"${value%%[![:space:]]*}"}" key="${key%"${key##*[![:space:]]}"}" value="${value%"${value##*[![:space:]]}"}" # Remove surrounding double and single quotes on values, if fully quoted. if [[ "$value" =~ ^\".*\"$ ]]; then value="${value:1:-1}" elif [[ "$value" =~ ^\'.*\'$ ]]; then value="${value:1:-1}" fi # Ensure key is valid. case "$key" in AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION|\ AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE|\ AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK|\ AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME|\ AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT|\ AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE|\ AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS|\ AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS) ;; *) echo "WARNING: ignoring invalid config key=$key" >&2 continue ;; esac # Value will be checked later, just assume it is correct and export it. echo "Configuration file set key=$key with value=$value" >&2 export "$key=$value" done < "$CONFIGURATION_FILE" } # Wait for a systemd unit to become active for up to 60 (AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS) seconds. start_and_wait_for_systemd_unit() { local unit="$1" local deadline=$((SECONDS + AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS)) if ! systemctl is-enabled "$unit" >&2; then log "systemd unit $unit is not enabled, skipping wait" return 1 fi # systemctl --wait may unexpectedly hang in some scenarios, so wait for up to 1s and loop as needed. while [[ $SECONDS -lt $deadline ]]; do log "Waiting for systemd unit $unit to become active... (remaining: $((deadline - SECONDS)) seconds)" timeout 1s systemctl start "$unit" --wait >&2 || log "Failed to start systemd unit $unit: $?" if systemctl is-active "$unit" >&2; then log "Started systemd unit $unit successfully" return 0 fi done log "Timed out waiting for systemd unit $unit to become active: $(systemctl status "$unit" 2>&1 || true)" return 1 } wait_for_udev_to_settle() { log "Waiting for udev to settle (timeout=${AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS}s)..." udevadm settle --timeout "$AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS" || true } AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION="${AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION:-auto}" AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME="${AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME:-azure-ephemeral-md}" AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK="${AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK:-512K}" AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE="${AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE:-ext4}" AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT="${AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT:-/mnt}" AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE="${AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE:-false}" AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS="${AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS:-60}" AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS="${AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS:-60}" load_config if [[ ! "$AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION" =~ ^(mdadm|none|auto)$ ]]; then exit_failure "AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION must be either 'auto', 'mdadm' or 'none'." fi if [[ ! "$AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then exit_failure "AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME must be a valid name (alphanumeric, underscores, or hyphens)." fi if [[ ! "$AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK" =~ ^[1-9][0-9]*[KMGT]$ ]]; then exit_failure "AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK must be a positive integer followed by K, M, G, or T (e.g., 512K, 1M, 2G)." fi if [[ ! "$AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE" =~ ^(ext4|xfs)$ ]]; then exit_failure "AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE must be either 'ext4' or 'xfs'." fi if [[ ! "$AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT" =~ ^/[a-zA-Z0-9_/-]+$ ]]; then exit_failure "AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT must be an absolute path and can only contain alphanumeric characters, underscores, hyphens, and slashes." fi if [[ -L "$AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT" ]] || [[ -e "$AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT" && ! -d "$AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT" ]]; then exit_failure "Mount point $AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT exists, but is not a directory" fi if [[ ! "$AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE" =~ ^(true|false)$ ]]; then exit_failure "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE must be either 'true' or 'false'" fi if [[ ! "$AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS" =~ ^[0-9]+$ || "$AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS" == 0 ]]; then exit_failure "AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS must be a positive integer" fi if [[ ! "$AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS" =~ ^[0-9]+$ || "$AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS" == 0 ]]; then exit_failure "AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS must be a positive integer" fi MKFS="mkfs.${AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE}" if ! command -v "$MKFS" &>/dev/null; then exit_failure "$MKFS is not installed and is required for formatting" fi if [[ "$AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION" == "auto" ]]; then if command -v mdadm &>/dev/null ; then log "mdadm is available, setting AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION to 'mdadm'" AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION="mdadm" else log "mdadm is not available, setting AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION to 'none'" AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION="none" fi elif [[ "$AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION" == "mdadm" ]] && ! command -v mdadm &>/dev/null ; then exit_failure "mdadm is not installed and is required for disk aggregation" fi # Check for existing mount point fstab entry. EXISTING_FSTAB_MOUNT="$(findmnt --fstab --noheadings --output SOURCE,FSTYPE,OPTIONS --mountpoint "$AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT" || true)" SYSTEMD_MOUNT_NAME=$(systemd-escape -p --suffix=mount "$AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT") # Ensure there are no conflicts with WALinuxAgent if AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true. # WALinuxAgent does not write fstab entry for us to check so we check if ResourceDisk.Format=y in waagent.conf. if [[ "$AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE" == "true" ]]; then if grep -Eq '^ResourceDisk\.Format\s*=\s*y' /etc/waagent.conf; then exit_failure "/etc/waagent.conf has ResourceDisk.Format=y which may conflict with this service" fi fi # Ensure there are no conflicts with cloud-init (assuming matching mount point). if [[ "$EXISTING_FSTAB_MOUNT" == "/dev/disk/cloud/azure_resource-part1"*"comment=cloudconfig"* ]]; then if [[ "$AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE" == "true" ]]; then exit_failure "Mount point $AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT is already configured by cloud-init, but AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" fi exit_success "Mount point $AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT is already configured by cloud-init and AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=false, nothing to do" fi if [[ -n "$EXISTING_FSTAB_MOUNT" ]]; then # If this service didn't configure the mount point, report error. if [[ "$EXISTING_FSTAB_MOUNT" != *"$FSTAB_COMMENT"* ]]; then DETAILS="$(awk '{print "source="$1" fstype="$2" options="$3}' <<<"$EXISTING_FSTAB_MOUNT")" exit_failure "Aborting due to conflicting fstab entry for $AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT with $DETAILS" fi # Check case where we already have an fstab entry configured by this service, but it is not yet mounted. # This can happen if the service starts before systemd has mounted the device, or perhaps the mount failed. # Try to start the systemd mount unit and wait, falling back to an explicit mount call. If it fails here, # we likely have freshly allocated disks that need to be setup. EXISTING_MOUNT="$(findmnt --noheadings --output SOURCE,TARGET,FSTYPE,OPTIONS --mountpoint "$AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT" || true)" if [[ -z "$EXISTING_MOUNT" ]]; then log "Mount point $AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT is configured in /etc/fstab but not mounted, waiting for $SYSTEMD_MOUNT_NAME..." if ! start_and_wait_for_systemd_unit "$SYSTEMD_MOUNT_NAME"; then log "Mount point $AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT is not yet mounted, falling back to explicit mount" mount --target "$AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT" >&2 || log "Failed to start existing mount for $AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT" fi fi fi wait_for_udev_to_settle EXISTING_LABEL_MOUNT="$(findmnt --noheadings --output SOURCE,TARGET,FSTYPE,OPTIONS,LABEL --source "LABEL=$FILESYSTEM_LABEL" || true)" EXISTING_MOUNT="$(findmnt --noheadings --output SOURCE,TARGET,FSTYPE,OPTIONS,LABEL --mountpoint "$AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT" || true)" if [[ -n "$EXISTING_MOUNT" ]]; then log "Mount point $AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT is mounted: $EXISTING_MOUNT" if [[ "$EXISTING_MOUNT" == *"$FILESYSTEM_LABEL" ]]; then exit_success "Mount point $AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT is mounted and ready for use" fi exit_failure "Mount exists at $AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT, but with a different label: $EXISTING_MOUNT" elif [[ -z "$EXISTING_FSTAB_MOUNT" && -z "$EXISTING_LABEL_MOUNT" && -b "$DISK_BY_LABEL_PATH" ]]; then # There may be cases where we cannot persist the mount with an fstab entry (read-only, or no /etc/fstab). # If a filesystem with the expected label is available from prior boot and currently unmounted, mount it. # If the user removed the fstab entry manually for some reason, we can still mount it here. The user # should disable this service if they do not want to use the ephemeral disk as intended. We just make note # of the label and missing fstab entry in the output and exit with success after mounted. DISK_BY_LABEL_PATH="$(readlink -f "$DISK_BY_LABEL_PATH")" log "WARNING: found existing filesystem with label=$FILESYSTEM_LABEL but no fstab entry configured: $DISK_BY_LABEL_PATH" mount -L "$FILESYSTEM_LABEL" --target "$AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT" >&2 || exit_failure "Mounting existing block device with label $FILESYSTEM_LABEL failed" wait_for_udev_to_settle exit_success "Mounted existing filesystem with label=$FILESYSTEM_LABEL at $AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT without fstab entry" elif [[ -z "$EXISTING_FSTAB_MOUNT" && -n "$EXISTING_LABEL_MOUNT" ]]; then # The first block checked the case where it's already mounted in the right location with -n $EXISTING_MOUNT, # so if we end up here with an existing label mount, it means the filesystem is mounted somewhere else. # This can happen if the user manually mounted it or perhaps changed their target mount point config. # In this case the user should fixup their fstab entry. Future work could update fstab automatically. exit_failure "Filesystem with label=$FILESYSTEM_LABEL is unexpectedly mounted: $EXISTING_LABEL_MOUNT" fi # Detect local NVMe disks using device model name. mapfile -t DISKS < <(lsblk --paths --noheadings -nodeps -o NAME,MODEL | awk '$0 ~ /Microsoft NVMe Direct Disk( v2)?[[:space:]]*$/ {print $1}' | sort -V || true) if [[ ${#DISKS[@]} -eq 0 ]]; then if [[ "$AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE" != "true" ]]; then if [[ -L "$SCSI_RESOURCE_DISK_PATH" ]]; then exit_success "No local NVMe disks detected and AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=$AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE, exiting without action" fi exit_success "No local NVMe disks detected, exiting without action" fi if [[ ! -L "$SCSI_RESOURCE_DISK_PATH" ]]; then exit_success "No local NVMe or SCSI resource disks detected, exiting without action" fi log "No local NVMe disks detected, using SCSI resource disk at $SCSI_RESOURCE_DISK_PATH" DISKS=("$SCSI_RESOURCE_DISK_PATH") else log "Detected local NVMe disks: ${DISKS[*]}" fi for disk in "${DISKS[@]}"; do check_if_block_device_needs_setup "$disk" done # If we have multiple disks, aggregate them using mdadm, if enabled. if [[ ${#DISKS[@]} -gt 1 && "$AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION" == "mdadm" ]]; then md="/dev/md/${AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME}_0" mdadm --create "$md" \ --level=0 \ --raid-devices=${#DISKS[@]} \ --metadata=1.2 \ --chunk="$AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK" \ --name="$AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME" \ "${DISKS[@]}" >&2 || exit_failure "Failed to create mdadm array with devices: ${DISKS[*]}" TARGET_DEVICE="$md" EXTRAS=" chunk=$AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK count=${#DISKS[@]}" # mdadm has a fairly short timeout to wait for device so we must wait for udev to settle. # [ 11.467473] + mdadm --create /dev/md/azure-ephemeral-md_0 --level=0 --raid-devices=2 --metadata=1.2 --chunk=512K --name=azure-ephemeral-md /dev/disk/azure/local/by-serial/95fcd1d0adcd35f70001 /dev/disk/azure/local/by-serial/95fcd1d0adcd35f70002 # [ 11.516972] mdadm: array /dev/md/azure-ephemeral-md_0 started. # [ 15.307223] mdadm: timeout waiting for /dev/md/azure-ephemeral-md_0 wait_for_udev_to_settle else TARGET_DEVICE="${DISKS[0]}" EXTRAS="" if [[ ${#DISKS[@]} -gt 1 ]]; then log "Multiple disks found but aggregation is disabled. Only using $TARGET_DEVICE" fi fi # Only force reformat if explicitly configured for SCSI resource disk (with some safety checks). MKFS_FORCE_FLAG="" if [[ "$SCSI_RESOURCE_DISK_FORCE_REFORMAT" == "true" && ${#DISKS[@]} -eq 1 && ${DISKS[0]} == "$SCSI_RESOURCE_DISK_PATH" ]]; then if [[ "$AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE" == "xfs" ]]; then MKFS_FORCE_FLAG="-f" else MKFS_FORCE_FLAG="-F" fi fi log "Formatting $TARGET_DEVICE with $AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE filesystem" $MKFS -L "$FILESYSTEM_LABEL" $MKFS_FORCE_FLAG "$TARGET_DEVICE" >&2 || exit_failure "Formatting $TARGET_DEVICE failed" # Ensure the partition table is updated and udev rules are applied. partprobe "$TARGET_DEVICE" || true wait_for_udev_to_settle mkdir -p "$AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT" # Update /etc/fstab to persist the mount and cleanup any old entries. if [[ ! -w /etc/fstab ]]; then log "WARNING: unable to persist mount to /etc/fstab as it is not writable" mount -L "$FILESYSTEM_LABEL" "$AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT" >&2 || exit_failure "Mounting $TARGET_DEVICE failed" else # Clean old fstab entries managed by this service. if grep -q "$FSTAB_COMMENT" /etc/fstab; then log "Cleaning old fstab entries with comment: $FSTAB_COMMENT" sed -i.bak "/$FSTAB_COMMENT/d" /etc/fstab fi log "Adding fstab entry for $TARGET_DEVICE at $AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT" echo "LABEL=$FILESYSTEM_LABEL $AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT $AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE defaults,nofail,$FSTAB_COMMENT 0 2" | tee -a /etc/fstab >&2 # Ensure systemd is aware of the mount point. systemctl daemon-reload if ! start_and_wait_for_systemd_unit "$SYSTEMD_MOUNT_NAME"; then if ! mount --verbose --target "$AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT" >&2; then exit_failure "Mounting $TARGET_DEVICE failed" fi fi fi wait_for_udev_to_settle exit_success "Mounted $TARGET_DEVICE at $AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT with fs=${AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE}${EXTRAS}" azure-vm-utils-0.8.0/ephemeral-disk-setup/azure-ephemeral-disk-setup.conf000066400000000000000000000016731527236310300265070ustar00rootroot00000000000000###### # Configuration for azure-ephemeral-disk-setup.service ## # Aggregation mode to use if multiple NVMe disks are found: {mdadm, none} # If set to 'mdadm', multiple local NVMe disks will be aggregated into a single raid0 array. # If set to 'none', at most one disk will be used. # If set to 'auto', at most one disk will be used if mdadm is not installed. AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION=auto # Filesystem type: {ext4, xfs} AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE=ext4 # mdadm chunk size: must be a positive integer followed by K, M, G, or T (e.g., 512K, 1M, 2G) AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK=512K # mdadm array name: must be alphanumeric, underscores, or hyphens AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME=azure-ephemeral-md # Target mount point for ephemeral disks: must be absolute path with no spaces AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT=/mnt # Manage SCSI resource disk: {true, false} AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=false azure-vm-utils-0.8.0/ephemeral-disk-setup/azure-ephemeral-disk-setup.service000066400000000000000000000012441527236310300272140ustar00rootroot00000000000000[Unit] Description=Format and mount local ephemeral NVMe disks After=cloud-init.service cloud-init-network.service After=local-fs.target DefaultDependencies=no Before=cloud-init.target Before=waagent.service walinuxagent.service Before=network-online.target Before=sshd.service Before=systemd-user-sessions.service # It would be ideal to start before sysinit.target, but only works on Debian/Ubuntu. # This causes an ordering cycle on Fedora. # Before=sysinit.target [Service] Type=oneshot ExecStart=@AZURE_NVME_ID_INSTALL_DIR@/azure-ephemeral-disk-setup RemainAfterExit=yes StandardOutput=journal+console StandardError=journal+console [Install] WantedBy=multi-user.target azure-vm-utils-0.8.0/ephemeral-disk-setup/test-cloud-config.yaml000066400000000000000000000006151527236310300246710ustar00rootroot00000000000000#cloud-config bootcmd: # Force delay in boot to ensure service starts after and we aren't racing. - sleep 60 write_files: - path: /etc/azure-ephemeral-disk-setup.conf permissions: '0644' content: | # Override fs and mount point so we can verify configs apply. AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE=xfs AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT=/mnt/azure-ephemeral-disk azure-vm-utils-0.8.0/ephemeral-disk-setup/test-scenarios.sh000077500000000000000000001360121527236310300237620ustar00rootroot00000000000000#!/bin/bash ########################################################################### # Test scenarios for azure-ephemeral-disk-setup service. # # This script is designed to test the behavior of the azure-ephemeral-disk-setup # script under various conditions, simulating different disk configurations # and ensuring that it behaves correctly in each case. # # This is meant to be run on Standard_L32s_v3 which features SCSI resource and # local NVMe disks (v1). # # Dependencies: # sudo dnf install -y mdadm xfsprogs parted ntfsprogs ntfs-3g # ########################################################################### set -euo pipefail shopt -s nullglob RUN_LOG_STDOUT="/tmp/azure-ephemeral-test.0.stdout" RUN_LOG_STDERR="/tmp/azure-ephemeral-test.0.stderr" # Ensure the script is run as root if [[ $EUID -ne 0 ]]; then echo "This script must be run as root" >&2 exit 1 fi # Ensure disks are symlinked correctly if previous runs deleted them. rm -f /etc/udev/rules.d/66-azure-ephemeral.rules # Scan NVMe disks. mapfile -t NVME_DISKS < <(lsblk --paths --noheadings -nodeps -o NAME,MODEL | awk '$0 ~ /Microsoft NVMe Direct Disk( v2)?[[:space:]]*$/ {print $1}' | sort -V || true) # Scan NVMe PCI addresses corresponding to the disks. mapfile -t NVME_DISKS_PCI_ADDR < <( for disk in "${NVME_DISKS[@]}"; do # Resolve symlink to actual device actual_device="$(readlink -f "$disk")" # Extract nvme device name (e.g., nvme0 from /dev/nvme0n1) nvme_device="$(basename "$actual_device" | sed 's/n[0-9]*$//')" # Get PCI address from sysfs [[ -e "/sys/class/nvme/$nvme_device/device" ]] || exit 1 pci_addr="$(basename "$(readlink -f "/sys/class/nvme/$nvme_device/device")")" echo "$pci_addr" done ) # Ensure we have at least 2 local NVMe disks. if [[ ${#NVME_DISKS[@]} -lt 2 ]]; then echo "Tests require at least 2 local NVMe disks. Found: ${#NVME_DISKS[@]}" exit 1 fi echo "Detected ${#NVME_DISKS[@]} local NVMe disks" # Ensure we have a SCSI resource disk. RESOURCE_DISK="/dev/disk/azure/resource" if [[ ! -b "$RESOURCE_DISK" ]]; then echo "Tests require SCSI resource disk at $RESOURCE_DISK" exit 1 fi RESOURCE_DISK_RESOLVED="$(readlink -f "$RESOURCE_DISK")" RESOURCE_DISK_PART1_RESOLVED="${RESOURCE_DISK_RESOLVED}1" echo "Detected SCSI resource disk: $RESOURCE_DISK_PART1_RESOLVED" # Scrub /etc/fstab to remove any lines with 'comment=' then save a backup. chattr -i /etc/fstab sed -i "/comment=/d" /etc/fstab FSTAB_SAVED_PATH="/etc/fstab.saved" cp /etc/fstab "$FSTAB_SAVED_PATH" WAAGENT_CONF_SAVED_PATH="/etc/waagent.conf.saved" cp /etc/waagent.conf "$WAAGENT_CONF_SAVED_PATH" # Allow for hiding mdadm/mkfs.xfs tools to test failure cases. MDADM_PATH="$(command -v mdadm)" MDADM_SAVED_PATH="$MDADM_PATH.saved" MKFS_XFS_PATH="$(command -v mkfs.xfs)" MKFS_XFS_SAVED_PATH="MKFS_XFS_PATH.saved" # Create a temporary directory for fake binaries to mock tools. FAKE_BIN_PATH="$(mktemp -d)" PATH="$FAKE_BIN_PATH:$PATH" # Use full path to systemctl to avoid conflicts with faked systemctl under some tests. SYSTEMCTL="$(command -v systemctl)" MOUNT="$(command -v mount)" configure_conf() { # Populates /etc/azure-ephemeral-disk-setup.conf with default settings, overriding # with KEY=VALUE arguments if provided. local key_value_pairs=("$@") local config_file="/etc/azure-ephemeral-disk-setup.conf" truncate -s 0 "$config_file" local pair for pair in "${key_value_pairs[@]}"; do echo "$pair" >>$config_file done } configure_nvme_disks() { # Configures NVMe local disks by creating symlinks in /dev/disk/azure/local/by-serial. # Takes optional # of disks to expose. If unspecified, exposes all available local disks. # Dynamically binds/unbinds NVMe devices via PCI to control which disks are available. local nvme_count=${#NVME_DISKS[@]} local desired_count="${1:-$nvme_count}" local i for i in $(seq 0 $(("${#NVME_DISKS[@]}" - 1))); do local pci_addr="${NVME_DISKS_PCI_ADDR[$i]}" if [[ $i -ge $desired_count ]]; then # Unbind the NVMe device to hide it if [[ -d "/sys/bus/pci/drivers/nvme/$pci_addr" ]]; then echo "$pci_addr" > /sys/bus/pci/drivers/nvme/unbind 2>/dev/null || true udevadm settle 2>/dev/null || true fi # Remove symlink if it exists rm -f "${NVME_DISKS[$i]}" else # Bind the NVMe device to make it available if [[ ! -d "/sys/bus/pci/drivers/nvme/$pci_addr" ]]; then echo "$pci_addr" > /sys/bus/pci/drivers/nvme/bind 2>/dev/null || true udevadm settle 2>/dev/null || true fi # Verify symlink exists after binding if [[ ! -b "${NVME_DISKS[$i]}" ]]; then echo "configure_nvme_disks: unexpected missing disk symlink ${NVME_DISKS[$i]} after binding $pci_addr" exit 1 fi fi done } configure_scsi_resource_disk() { # Configures SCSI resource disk by managing /dev/disk/azure/resource symlink. # Takes argument of 0 (disabled) or 1 (enabled). local count="$1" if [[ $count -eq 0 ]]; then rm -f "$RESOURCE_DISK" return fi if [[ ! -b "$RESOURCE_DISK" ]]; then echo "configure_scsi_resource_disk: unexpectedly missing resource disk symlink" exit 1 fi } disable_write_fstab() { chattr +i /etc/fstab } hide_mdadm() { mv "$MDADM_PATH" "$MDADM_SAVED_PATH" } hide_mkfs_xfs() { mv "$MKFS_XFS_PATH" "$MKFS_XFS_SAVED_PATH" } partprobe_safe() { # Sometimes a sync seems to be needed after partprobe. It may just be a # coincidence that the delay from sync is enough. # WARNING: partprobe will trigger symlinks to be re-created, make sure to # run configure_nvme_disks and configure_scsi_resource_disk AFTER running # this. partprobe >/dev/null 2>&1 udevadm settle } fstab_add_entry() { echo "$1" >>/etc/fstab $SYSTEMCTL daemon-reload } mount_safe() { local device="$1" local target="$2" local systemd_mount_name systemd_mount_name="$(systemd-escape -p --suffix=mount "$target")" #echo "Mounting $device at $target with fs=$(blkid -o value -s TYPE "$device")" >&2 $MOUNT "$device" "$target" >&2 $SYSTEMCTL daemon-reload $SYSTEMCTL reset-failed "$systemd_mount_name" >&2 } unmount_safe() { local target="$1" local systemd_mount_name systemd_mount_name="$(systemd-escape -p --suffix=mount "$target")" #echo "Unmounting $target with fs=$(findmnt --noheadings --output FSTYPE --target "$target")" >&2 local umount_target="true" if $SYSTEMCTL status "$systemd_mount_name" >/dev/null 2>&1; then $SYSTEMCTL stop "$systemd_mount_name" >/dev/null 2>&1 && umount_target="false" fi if [[ "$umount_target" == "true" ]]; then umount "$target" >&2 fi $SYSTEMCTL daemon-reload } reset_disk() { wipefs -q -a "$1" >/dev/null 2>&1 || true } reset_disks() { if findmnt /mnt >/dev/null 2>&1; then unmount_safe /mnt fi local disk for disk in /dev/md/*; do local target target="$(findmnt --noheadings --output TARGET --source "$disk" || true)" if [[ -n "$target" ]]; then unmount_safe "$target" fi reset_disk "$disk" mdadm -q --stop "$disk" done for disk in "${NVME_DISKS[@]}" "$RESOURCE_DISK" "${RESOURCE_DISK_PART1_RESOLVED}"; do if [[ ! -b "$disk" ]]; then continue fi local target target="$(findmnt --noheadings --output TARGET --source "$disk" || true)" if [[ -n "$target" ]]; then unmount_safe "$target" fi reset_disk "$disk" done partprobe_safe configure_nvme_disks ${#NVME_DISKS[@]} configure_scsi_resource_disk 1 if [[ -b "$RESOURCE_DISK" ]]; then reset_ntfs_resource_disk fi $SYSTEMCTL daemon-reload $SYSTEMCTL reset-failed || true } reset_fake_bin_dir() { rm -rf "${FAKE_BIN_PATH:?}"/* } reset_fstab() { chattr -i /etc/fstab cp "$FSTAB_SAVED_PATH" /etc/fstab } reset_waagent_conf() { cp "$WAAGENT_CONF_SAVED_PATH" /etc/waagent.conf } reset_mdadm() { if [[ -f "$MDADM_SAVED_PATH" ]]; then mv "$MDADM_SAVED_PATH" "$MDADM_PATH" fi } reset_mkfs_xfs() { if [[ -f "$MKFS_XFS_SAVED_PATH" ]]; then mv "$MKFS_XFS_SAVED_PATH" "$MKFS_XFS_PATH" fi } reset_ntfs_resource_disk() { parted -s "$RESOURCE_DISK" mklabel gpt mkpart primary ntfs 0% 100% partprobe_safe mkfs.ntfs -q --quick "${RESOURCE_DISK_PART1_RESOLVED}" --label "Temporary Storage" mount_safe "${RESOURCE_DISK_PART1_RESOLVED}" /mnt echo "This is a test NTFS file" >/mnt/dataloss_warning_readme.txt unmount_safe /mnt } reset_all() { #echo "Resetting all state..." >&2 reset_fake_bin_dir reset_fstab reset_mdadm reset_mkfs_xfs reset_disks reset_waagent_conf configure_conf chmod 755 /mnt } on_exit() { echo "Cleaning up for exit..." if findmnt /mnt >/dev/null 2>&1; then unmount_safe /mnt fi reset_all partprobe_safe } trap on_exit EXIT INT TERM assert_in_stderr() { local expected_string="$1" if ! grep -q -F "$expected_string" "$RUN_LOG_STDERR"; then echo "❌ $TEST_COUNT $TEST LOG ASSERTION FAILED: Expected \"$expected_string\"" exit 1 fi } assert_regex_in_stderr() { local expected_regex="$1" if ! grep -q "$expected_regex" "$RUN_LOG_STDERR"; then echo "❌ $TEST_COUNT $TEST LOG ASSERTION FAILED: Expected \"$expected_regex\"" exit 1 fi } run_and_assert() { local expected_output="$1" local expected_code="$2" local regex="${3:-false}" local actual_output local actual_code=0 RUN_LOG_STDOUT="/tmp/azure-ephemeral-test.$TEST_COUNT.stdout" RUN_LOG_STDERR="/tmp/azure-ephemeral-test.$TEST_COUNT.stderr" azure-ephemeral-disk-setup >"$RUN_LOG_STDOUT" 2>"$RUN_LOG_STDERR" || actual_code=$? actual_output="$(cat "$RUN_LOG_STDOUT")" if [[ "$regex" == "true" && ! "$actual_output" =~ $expected_output ]]; then echo "❌ $TEST_COUNT $TEST OUTPUT FAILED: Actual \"$actual_output\" !~ Expected \"$expected_output\"" exit 1 elif [[ "$regex" == "false" && "$actual_output" != "$expected_output" ]]; then echo "❌ $TEST_COUNT $TEST OUTPUT FAILED: Actual \"$actual_output\" != Expected \"$expected_output\"" exit 1 fi if [[ $actual_code -ne $expected_code ]]; then echo "❌ $TEST_COUNT $TEST EXIT CODE FAILED: Actual code=$actual_code != Expected code=$expected_code" exit 1 fi echo "✅ $TEST_COUNT $TEST: $actual_output (code=$actual_code)" } fake_mount_that_fails_first_call() { # Creates a fake mount command that fails the first call, simulating a mount failure, then passes through subsequent calls. cat < "$FAKE_BIN_PATH/mount" #!/bin/bash -x called_path="$FAKE_BIN_PATH/mount_called" [[ -f \$called_path ]] && exec /usr/bin/mount "\$@" touch "\$called_path" exit 1 EOF chmod +x "$FAKE_BIN_PATH/mount" hash -r } fake_mount_that_fails_ntfs_and_ntfs3_with_unknown_filesystem() { # Creates a fake mount command that fails with "unknown filesystem type" for ntfs and ntfs3. cat < "$FAKE_BIN_PATH/mount" #!/bin/bash -x [[ "\$2" == "ntfs" || "\$2" == "ntfs3" ]] || exec /usr/bin/mount "\$@" echo "mount: unknown filesystem type '\$2'" >&2 exit 32 EOF chmod +x "$FAKE_BIN_PATH/mount" hash -r } fake_systemctl_that_hangs_on_start() { # Create a systemctl that hangs on "start" calls but passes through other commands. cat < "$FAKE_BIN_PATH/systemctl" #!/bin/bash echo "fake systemctl called with args: \$@" >&2 [[ "\$1" == "start" ]] || exec /usr/bin/systemctl "\$@" sleep 1000 EOF chmod +x "$FAKE_BIN_PATH/systemctl" hash -r } fake_systemctl_that_fails_on_start() { # Create a systemctl that fails on "start" calls but passes through other commands. cat < "$FAKE_BIN_PATH/systemctl" #!/bin/bash echo "fake systemctl called with args: \$@" >&2 [[ "\$1" == "start" ]] || exec /usr/bin/systemctl "\$@" exit 1 EOF chmod +x "$FAKE_BIN_PATH/systemctl" hash -r } run_and_assert_success() { local expected_output="$1" run_and_assert "$expected_output" 0 } run_and_assert_failure() { local expected_output="$1" run_and_assert "$expected_output" 1 } run_and_assert_failure_regex() { local expected_output="$1" run_and_assert "$expected_output" 1 true } test_load_config() { configure_nvme_disks 0 configure_scsi_resource_disk 0 cat >/etc/azure-ephemeral-disk-setup.conf <>/etc/waagent.conf run_and_assert_failure "/etc/waagent.conf has ResourceDisk.Format=y which may conflict with this service" # Check with spaces. reset_waagent_conf echo "ResourceDisk.Format = y" >>/etc/waagent.conf run_and_assert_failure "/etc/waagent.conf has ResourceDisk.Format=y which may conflict with this service" } test_reboot_systemctl_wait_single_nvme() { configure_nvme_disks 1 configure_scsi_resource_disk 0 run_and_assert_success "Mounted ${NVME_DISKS[0]} at /mnt with fs=ext4" # Simulate a reboot by resetting unmounting /mnt via systemd. $SYSTEMCTL stop mnt.mount run_and_assert_success "Mount point /mnt is mounted and ready for use" assert_in_stderr "Mount point /mnt is configured in /etc/fstab but not mounted, waiting for mnt.mount..." } test_reboot_systemctl_wait_aggregated_nvme() { configure_nvme_disks 2 configure_scsi_resource_disk 0 run_and_assert_success "Mounted /dev/md/azure-ephemeral-md_0 at /mnt with fs=ext4 chunk=512K count=2" # Simulate a reboot by resetting unmounting /mnt via systemd. $SYSTEMCTL stop mnt.mount run_and_assert_success "Mount point /mnt is mounted and ready for use" assert_in_stderr "Mount point /mnt is configured in /etc/fstab but not mounted, waiting for mnt.mount..." } test_reboot_systemctl_wait_managed_resource() { configure_nvme_disks 0 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" run_and_assert_success "Mounted /dev/disk/azure/resource at /mnt with fs=ext4" # Simulate a reboot by resetting unmounting /mnt via systemd. $SYSTEMCTL stop mnt.mount run_and_assert_success "Mount point /mnt is mounted and ready for use" assert_in_stderr "Mount point /mnt is configured in /etc/fstab but not mounted, waiting for mnt.mount..." } test_reboot_mount_fallback_single_nvme() { configure_nvme_disks 1 configure_scsi_resource_disk 0 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS=5" run_and_assert_success "Mounted ${NVME_DISKS[0]} at /mnt with fs=ext4" # Simulate a reboot by resetting unmounting /mnt. unmount_safe /mnt fake_systemctl_that_fails_on_start run_and_assert_success "Mount point /mnt is mounted and ready for use" assert_in_stderr "Mount point /mnt is configured in /etc/fstab but not mounted, waiting for mnt.mount..." assert_regex_in_stderr "^+ mount --target /mnt$" } test_reboot_mount_fallback_aggregated_nvme() { configure_nvme_disks 2 configure_scsi_resource_disk 0 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS=5" run_and_assert_success "Mounted /dev/md/azure-ephemeral-md_0 at /mnt with fs=ext4 chunk=512K count=2" # Simulate a reboot by resetting unmounting /mnt. unmount_safe /mnt fake_systemctl_that_fails_on_start run_and_assert_success "Mount point /mnt is mounted and ready for use" assert_in_stderr "Mount point /mnt is configured in /etc/fstab but not mounted, waiting for mnt.mount..." assert_regex_in_stderr "^+ mount --target /mnt$" } test_reboot_mount_fallback_managed_resource() { configure_nvme_disks 0 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" "AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS=5" run_and_assert_success "Mounted /dev/disk/azure/resource at /mnt with fs=ext4" # Simulate a reboot by resetting unmounting /mnt. unmount_safe /mnt fake_systemctl_that_fails_on_start run_and_assert_success "Mount point /mnt is mounted and ready for use" assert_in_stderr "Mount point /mnt is configured in /etc/fstab but not mounted, waiting for mnt.mount..." assert_regex_in_stderr "^+ mount --target /mnt$" } test_resource_managed_missing_ntfs_drivers() { configure_nvme_disks 0 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" fake_mount_that_fails_ntfs_and_ntfs3_with_unknown_filesystem run_and_assert_success "Mounted /dev/disk/azure/resource at /mnt with fs=ext4" assert_in_stderr "WARNING: failed to mount $RESOURCE_DISK_PART1_RESOLVED due to lack of ntfs support, assuming it is empty and safe for reformat" } test_format_failure_single_nvme() { configure_nvme_disks 1 configure_scsi_resource_disk 0 cp /bin/false "$FAKE_BIN_PATH/mkfs.ext4" hash -r run_and_assert_failure "Formatting ${NVME_DISKS[0]} failed" } test_format_failure_aggregated_nvme() { configure_nvme_disks 2 configure_scsi_resource_disk 0 cp /bin/false "$FAKE_BIN_PATH/mkfs.ext4" hash -r run_and_assert_failure "Formatting /dev/md/azure-ephemeral-md_0 failed" } test_format_failure_scsi_resource() { configure_nvme_disks 0 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" cp /bin/false "$FAKE_BIN_PATH/mkfs.ext4" hash -r run_and_assert_failure "Formatting /dev/disk/azure/resource failed" } test_idempotent_rerun() { configure_nvme_disks 1 configure_scsi_resource_disk 0 run_and_assert_success "Mounted ${NVME_DISKS[0]} at /mnt with fs=ext4" run_and_assert_success "Mount point /mnt is mounted and ready for use" } test_fstab_readonly() { configure_nvme_disks 1 configure_scsi_resource_disk 0 disable_write_fstab run_and_assert_success "Mounted ${NVME_DISKS[0]} at /mnt with fs=ext4" assert_in_stderr "WARNING: unable to persist mount to /etc/fstab" } test_fstab_readonly_reboot_single_nvme() { configure_nvme_disks 1 configure_scsi_resource_disk 0 configure_conf disable_write_fstab run_and_assert_success "Mounted ${NVME_DISKS[0]} at /mnt with fs=ext4" assert_in_stderr "WARNING: unable to persist mount to /etc/fstab" # Simulate a reboot by resetting unmounting /mnt. unmount_safe /mnt run_and_assert_success "Mounted existing filesystem with label=AzureEphmDsk at /mnt without fstab entry" } test_fstab_readonly_reboot_already_mounted_elsewhere() { configure_nvme_disks 1 configure_scsi_resource_disk 0 disable_write_fstab run_and_assert_success "Mounted ${NVME_DISKS[0]} at /mnt with fs=ext4" assert_in_stderr "WARNING: unable to persist mount to /etc/fstab" configure_conf "AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT=/media" run_and_assert_failure_regex "Filesystem with label=AzureEphmDsk is unexpectedly mounted: ${NVME_DISKS[0]}[[:space:]]+/mnt[[:space:]]+ext4[[:space:]]+rw,relatime,stripe=[0-9]+[[:space:]]+AzureEphmDsk" } test_fstab_readonly_reboot_aggregated_nvme() { configure_nvme_disks 2 configure_scsi_resource_disk 0 disable_write_fstab run_and_assert_success "Mounted /dev/md/azure-ephemeral-md_0 at /mnt with fs=ext4 chunk=512K count=2" assert_in_stderr "WARNING: unable to persist mount to /etc/fstab" # Simulate a reboot by resetting unmounting /mnt. unmount_safe /mnt run_and_assert_success "Mounted existing filesystem with label=AzureEphmDsk at /mnt without fstab entry" assert_in_stderr "WARNING: found existing filesystem with label=AzureEphmDsk but no fstab entry configured: /dev/md" } test_fstab_readonly_idempotent_single_nvme() { configure_nvme_disks 1 configure_scsi_resource_disk 0 disable_write_fstab run_and_assert_success "Mounted ${NVME_DISKS[0]} at /mnt with fs=ext4" assert_in_stderr "WARNING: unable to persist mount to /etc/fstab" run_and_assert_success "Mount point /mnt is mounted and ready for use" } test_fstab_readonly_idempotent_aggregated_nvme() { configure_nvme_disks 2 configure_scsi_resource_disk 0 disable_write_fstab run_and_assert_success "Mounted /dev/md/azure-ephemeral-md_0 at /mnt with fs=ext4 chunk=512K count=2" if ! grep -q "WARNING: unable to persist mount to /etc/fstab" "$RUN_LOG_STDERR"; then echo "Expected warning about fstab not being writable was not found in log" exit 1 fi run_and_assert_success "Mount point /mnt is mounted and ready for use" } test_fstab_conflicting_entry() { configure_nvme_disks 1 configure_scsi_resource_disk 0 fstab_add_entry "/dev/fake /mnt xfs defaults,comment=otherservice 0 2" run_and_assert_failure "Aborting due to conflicting fstab entry for /mnt with source=/dev/fake fstype=xfs options=defaults,comment=otherservice" } test_broken_symlink_resource() { configure_nvme_disks 0 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" ln -sf /dev/nonexistent /dev/disk/azure/resource run_and_assert_failure "Not a valid block device: /dev/disk/azure/resource" } test_custom_mount() { configure_nvme_disks 0 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" 'AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT=/mnt/custom-MOUNT_0' run_and_assert_success "Mounted /dev/disk/azure/resource at /mnt/custom-MOUNT_0 with fs=ext4" } test_custom_udevadm_settle_timeout() { configure_nvme_disks 0 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" 'AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS=90' run_and_assert_success "Mounted /dev/disk/azure/resource at /mnt with fs=ext4" assert_in_stderr "Waiting for udev to settle (timeout=90s)..." } test_custom_mount_deep() { configure_nvme_disks 0 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" "AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT=/mnt/foo/BAR/custom_0" run_and_assert_success "Mounted /dev/disk/azure/resource at /mnt/foo/BAR/custom_0 with fs=ext4" } test_custom_mdadm_chunk() { configure_nvme_disks 2 configure_scsi_resource_disk 0 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK=1024K" run_and_assert_success "Mounted /dev/md/azure-ephemeral-md_0 at /mnt with fs=ext4 chunk=1024K count=2" } test_custom_mdam_name() { configure_nvme_disks 2 configure_scsi_resource_disk 0 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME=my_RAID-0" run_and_assert_success "Mounted /dev/md/my_RAID-0_0 at /mnt with fs=ext4 chunk=512K count=2" } test_custom_fs_xfs_single() { configure_nvme_disks 1 configure_scsi_resource_disk 0 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE=xfs" run_and_assert_success "Mounted ${NVME_DISKS[0]} at /mnt with fs=xfs" } test_custom_fs_xfs_aggregated() { configure_nvme_disks 2 configure_scsi_resource_disk 0 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE=xfs" run_and_assert_success "Mounted /dev/md/azure-ephemeral-md_0 at /mnt with fs=xfs chunk=512K count=2" } test_invalid_config_aggregation() { configure_conf "AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION=invalid" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION must be either 'auto', 'mdadm' or 'none'." } test_invalid_config_fs_type() { configure_conf "AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE=ntfs" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE must be either 'ext4' or 'xfs'." } test_invalid_config_mdadm_chunk() { configure_conf "AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK=invalid" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK must be a positive integer followed by K, M, G, or T (e.g., 512K, 1M, 2G)." configure_conf "AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK=100" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK must be a positive integer followed by K, M, G, or T (e.g., 512K, 1M, 2G)." configure_conf "AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK=100Q" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK must be a positive integer followed by K, M, G, or T (e.g., 512K, 1M, 2G)." } test_invalid_config_mdadm_name() { configure_conf "AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME=invalid\ name" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME must be a valid name (alphanumeric, underscores, or hyphens)." } test_invalid_config_mount_point_path() { configure_conf "AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT=relative/path" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT must be an absolute path and can only contain alphanumeric characters, underscores, hyphens, and slashes." configure_conf "AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT=/mnt/spaced out" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT must be an absolute path and can only contain alphanumeric characters, underscores, hyphens, and slashes." IFS=' ' read -r -a chars <<< '! @ # $ % ^ & * ( ) + = { } [ ] ; : " '\'' | < > , ? ; :' for char in "${chars[@]}"; do configure_conf "AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT=/mnt/invalid${char}char" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT must be an absolute path and can only contain alphanumeric characters, underscores, hyphens, and slashes." done } test_invalid_config_scsi_resource() { configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=invalid" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE must be either 'true' or 'false'" } test_invalid_udevadm_settle_timeout() { configure_conf "AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS=invalid" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS must be a positive integer" configure_conf "AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS=-1" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS must be a positive integer" configure_conf "AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS=0" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_UDEVADM_SETTLE_TIMEOUT_SECS must be a positive integer" } test_invalid_systemd_unit_timeout() { configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS=invalid" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS must be a positive integer" configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS=-1" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS must be a positive integer" configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS=0" run_and_assert_failure "AZURE_EPHEMERAL_DISK_SETUP_SYSTEMD_UNIT_TIMEOUT_SECS must be a positive integer" } test_missing_mkfs_xfs() { hide_mkfs_xfs configure_conf "AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE=xfs" run_and_assert_failure "mkfs.xfs is not installed and is required for formatting" } test_aggregation_mdadm_missing_mdadm() { hide_mdadm configure_conf "AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION=mdadm" run_and_assert_failure "mdadm is not installed and is required for disk aggregation" } test_aggregation_auto_missing_mdadm() { hide_mdadm configure_conf "AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION=auto" configure_nvme_disks 2 run_and_assert_success "Mounted ${NVME_DISKS[0]} at /mnt with fs=ext4" assert_regex_in_stderr "mdadm is not available, setting AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION to 'none'" } test_aggregation_auto_with_mdadm() { configure_conf "AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION=auto" configure_nvme_disks run_and_assert_success "Mounted /dev/md/azure-ephemeral-md_0 at /mnt with fs=ext4 chunk=512K count=${#NVME_DISKS[@]}" assert_regex_in_stderr "mdadm is available, setting AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION to 'mdadm'" } test_aggregation_none_missing_mdadm() { hide_mdadm configure_conf "AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION=none" configure_nvme_disks 2 run_and_assert_success "Mounted ${NVME_DISKS[0]} at /mnt with fs=ext4" } test_mount_point_is_a_file() { configure_nvme_disks 1 configure_scsi_resource_disk 0 local temp_path temp_path="$(mktemp /tmp/XXXXXX)" configure_conf AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT="$temp_path" run_and_assert_failure "Mount point $temp_path exists, but is not a directory" rm -f "$temp_path" } test_mount_point_is_a_symlink() { configure_nvme_disks 1 configure_scsi_resource_disk 0 local temp_path temp_path="$(mktemp /tmp/XXXXXX)" local temp_path2 temp_path2="$(mktemp /tmp/XXXXXX)" ln -sf "$temp_path2" "$temp_path" configure_conf AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT="$temp_path" run_and_assert_failure "Mount point $temp_path exists, but is not a directory" rm -f "$temp_path" rm -f "$temp_path2" } test_no_nvme_or_managed_resource_disks() { configure_nvme_disks 0 configure_scsi_resource_disk 0 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" run_and_assert_success "No local NVMe or SCSI resource disks detected, exiting without action" } test_no_nvme_or_unmanaged_resource_disks() { configure_nvme_disks 0 configure_scsi_resource_disk 0 run_and_assert_success "No local NVMe disks detected, exiting without action" } test_nvme_aggregation_disabled() { configure_nvme_disks 2 configure_scsi_resource_disk 0 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION=none" run_and_assert_success "Mounted ${NVME_DISKS[0]} at /mnt with fs=ext4" assert_in_stderr "Multiple disks found but aggregation is disabled. Only using ${NVME_DISKS[0]}" } test_nvme_aggregation_max() { configure_nvme_disks ${#NVME_DISKS[@]} configure_scsi_resource_disk 0 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" run_and_assert_success "Mounted /dev/md/azure-ephemeral-md_0 at /mnt with fs=ext4 chunk=512K count=${#NVME_DISKS[@]}" } test_nvme_aggregation_max_with_managed_resource() { configure_nvme_disks ${#NVME_DISKS[@]} configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" run_and_assert_success "Mounted /dev/md/azure-ephemeral-md_0 at /mnt with fs=ext4 chunk=512K count=${#NVME_DISKS[@]}" } test_nvme_aggregation_without_managed_resource() { configure_nvme_disks 2 configure_scsi_resource_disk 0 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" run_and_assert_success "Mounted /dev/md/azure-ephemeral-md_0 at /mnt with fs=ext4 chunk=512K count=2" } test_nvme_aggregation_with_managed_resource() { configure_nvme_disks 2 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" run_and_assert_success "Mounted /dev/md/azure-ephemeral-md_0 at /mnt with fs=ext4 chunk=512K count=2" } test_nvme_already_formatted() { configure_nvme_disks 1 configure_scsi_resource_disk 0 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" mkfs.ext4 -q -F "${NVME_DISKS[0]}" run_and_assert_failure "Device ${NVME_DISKS[0]} contains a partition table or is already formatted" } test_nvme_already_mounted() { configure_nvme_disks 1 configure_scsi_resource_disk 0 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" mkfs.ext4 -q -F "${NVME_DISKS[0]}" mount_safe "${NVME_DISKS[0]}" /media run_and_assert_failure "Device ${NVME_DISKS[0]} is already mounted or in use" } test_nvme_already_partitioned_multiple_unformatted() { parted -s "${NVME_DISKS[0]}" mklabel gpt mkpart primary ext4 0% 50% parted -s "${NVME_DISKS[0]}" mkpart primary ext4 50% 100% partprobe_safe configure_scsi_resource_disk 0 configure_nvme_disks 1 run_and_assert_failure "Device ${NVME_DISKS[0]} has 2 partition(s)" } test_nvme_already_partitioned_multiple_formatted() { parted -s "${NVME_DISKS[0]}" mklabel gpt mkpart primary ext4 0% 50% parted -s "${NVME_DISKS[0]}" mkpart primary ext4 50% 100% partprobe_safe mkfs.ext4 -q -F "${NVME_DISKS[0]}p1" mkfs.ext4 -q -F "${NVME_DISKS[0]}p2" configure_scsi_resource_disk 0 configure_nvme_disks 1 run_and_assert_failure "Device ${NVME_DISKS[0]} has 2 partition(s)" } test_nvme_already_partitioned_single_unformatted() { parted -s "${NVME_DISKS[0]}" mklabel gpt mkpart primary ext4 0% 100% partprobe_safe configure_scsi_resource_disk 0 configure_nvme_disks 1 run_and_assert_failure "Device ${NVME_DISKS[0]} has 1 partition(s)" } test_nvme_already_partitioned_single_and_formatted() { parted -s "${NVME_DISKS[0]}" mklabel gpt mkpart primary ext4 0% 100% partprobe_safe mkfs.ext4 -q -F "${NVME_DISKS[0]}p1" configure_scsi_resource_disk 0 configure_nvme_disks 1 run_and_assert_failure "Device ${NVME_DISKS[0]} has 1 partition(s)" } test_nvme_single() { configure_nvme_disks 1 configure_scsi_resource_disk 0 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" run_and_assert_success "Mounted ${NVME_DISKS[0]} at /mnt with fs=ext4" } test_nvme_single_and_managed_resource() { configure_nvme_disks 1 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" run_and_assert_success "Mounted ${NVME_DISKS[0]} at /mnt with fs=ext4" } test_nvme_single_and_unmanaged_resource() { configure_nvme_disks 1 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=false" run_and_assert_success "Mounted ${NVME_DISKS[0]} at /mnt with fs=ext4" } test_resource_managed() { configure_nvme_disks 0 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" run_and_assert_success "Mounted /dev/disk/azure/resource at /mnt with fs=ext4" } test_resource_ntfs_wrong_label() { configure_nvme_disks 0 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" local label="NotTemporaryStorage" mkfs.ntfs -q --quick "$RESOURCE_DISK_PART1_RESOLVED" --label "$label" run_and_assert_failure "Resource disk partition $RESOURCE_DISK_PART1_RESOLVED has label=$label, expected label=Temporary Storage" } test_resource_unmanaged() { configure_nvme_disks 0 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=false" run_and_assert_success "No local NVMe disks detected and AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=false, exiting without action" } test_resource_already_mounted_by_cloudinit_scsi_setup_disabled() { configure_nvme_disks 1 configure_scsi_resource_disk 1 fstab_add_entry "/dev/disk/cloud/azure_resource-part1 /mnt auto defaults,nofail,comment=cloudconfig 0 2" mount_safe /dev/disk/cloud/azure_resource-part1 /mnt run_and_assert_success "Mount point /mnt is already configured by cloud-init and AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=false, nothing to do" } test_resource_already_mounted_by_cloudinit_scsi_setup_enabled() { configure_nvme_disks 1 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" fstab_add_entry "/dev/disk/cloud/azure_resource-part1 /mnt auto defaults,nofail,comment=cloudconfig 0 2" mount_safe /dev/disk/cloud/azure_resource-part1 /mnt run_and_assert_failure "Mount point /mnt is already configured by cloud-init, but AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" } test_resource_with_extra_directory() { mount_safe "${RESOURCE_DISK_PART1_RESOLVED}" /mnt mkdir /mnt/baddir unmount_safe /mnt configure_scsi_resource_disk 1 configure_nvme_disks 0 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" run_and_assert_failure "SCSI resource disk /dev/disk/azure/resource is NTFS formatted but contains unexpected files or folders: baddir" } test_resource_with_extra_file() { configure_nvme_disks 0 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" mount_safe "${RESOURCE_DISK_PART1_RESOLVED}" /mnt touch /mnt/badfile.txt unmount_safe /mnt run_and_assert_failure "SCSI resource disk /dev/disk/azure/resource is NTFS formatted but contains unexpected files or folders: badfile.txt" } test_resource_already_formatted_non_ntfs() { configure_nvme_disks 0 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" mkfs.ext4 -q -F "${RESOURCE_DISK_PART1_RESOLVED}" run_and_assert_failure "Resource disk partition $RESOURCE_DISK_PART1_RESOLVED has type=ext4, expected type=ntfs" } test_resource_already_formatted_ntfs_without_partition() { configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" mkfs.ntfs -q --quick --force "${RESOURCE_DISK}" --label "Temporary Storage" partprobe_safe configure_nvme_disks 0 run_and_assert_failure "Device /dev/disk/azure/resource contains a partition table or is already formatted" } test_resource_already_mounted_elsewhere() { configure_nvme_disks 0 configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" $MOUNT -t ntfs "${RESOURCE_DISK_PART1_RESOLVED}" /media run_and_assert_failure "Resource disk partition $RESOURCE_DISK_PART1_RESOLVED is already mounted or in use" } test_resource_already_mounted_reformatted() { configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" mkfs.ext4 -q -F "$RESOURCE_DISK" partprobe_safe mount_safe "$RESOURCE_DISK" /media configure_nvme_disks 0 run_and_assert_failure "Device /dev/disk/azure/resource is already mounted or in use" } test_resource_already_partitioned_multiple() { configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" parted -s "$RESOURCE_DISK" mklabel gpt mkpart primary ext4 0% 50% parted -s "$RESOURCE_DISK" mkpart primary ext4 50% 100% partprobe_safe configure_nvme_disks 0 run_and_assert_failure "Device /dev/disk/azure/resource has 2 partition(s)" } test_resource_existing_partition_reformatted() { configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" mkfs.ext4 -q -F "${RESOURCE_DISK_PART1_RESOLVED}" partprobe_safe configure_nvme_disks 0 run_and_assert_failure "Resource disk partition $RESOURCE_DISK_PART1_RESOLVED has type=ext4, expected type=ntfs" } test_resource_unformatted() { configure_scsi_resource_disk 1 configure_conf "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE=true" reset_disk "$RESOURCE_DISK" partprobe_safe configure_nvme_disks 0 run_and_assert_success "Mounted /dev/disk/azure/resource at /mnt with fs=ext4" } run_tests() { SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")" TESTS=$(grep -Eo '^test_[a-zA-Z0-9_]+\(\)' "$SCRIPT_PATH" | sed 's/()//' | paste -sd' ' -) START_INDEX=${START_INDEX:-0} START_COUNT=$((START_INDEX + 1)) TEST_COUNT=0 for TEST in $TESTS; do TEST_COUNT=$((TEST_COUNT + 1)) if [ "$TEST_COUNT" -lt "$START_COUNT" ]; then echo "Skipping test #$TEST_COUNT: $TEST" continue fi reset_all echo "Running test #$TEST_COUNT: $TEST" $TEST done } run_tests echo "All tests passed!" azure-vm-utils-0.8.0/initramfs/000077500000000000000000000000001527236310300164215ustar00rootroot00000000000000azure-vm-utils-0.8.0/initramfs/dracut/000077500000000000000000000000001527236310300177035ustar00rootroot00000000000000azure-vm-utils-0.8.0/initramfs/dracut/modules.d/000077500000000000000000000000001527236310300215755ustar00rootroot00000000000000azure-vm-utils-0.8.0/initramfs/dracut/modules.d/97azure-disk/000077500000000000000000000000001527236310300240335ustar00rootroot00000000000000azure-vm-utils-0.8.0/initramfs/dracut/modules.d/97azure-disk/module-setup.sh000066400000000000000000000003311527236310300270070ustar00rootroot00000000000000#!/bin/bash # called by dracut check() { return 0 } # called by dracut depends() { return 0 } # called by dracut install() { inst_multiple azure-nvme-id cut readlink inst_rules 80-azure-disk.rules } azure-vm-utils-0.8.0/initramfs/dracut/modules.d/97azure-unmanaged-sriov/000077500000000000000000000000001527236310300262005ustar00rootroot00000000000000azure-vm-utils-0.8.0/initramfs/dracut/modules.d/97azure-unmanaged-sriov/module-setup.sh000066400000000000000000000002731527236310300311610ustar00rootroot00000000000000#!/usr/bin/bash # called by dracut check() { return 0 } # called by dracut depends() { return 0 } # called by dracut install() { inst_rules 10-azure-unmanaged-sriov.rules } azure-vm-utils-0.8.0/initramfs/initramfs-tools/000077500000000000000000000000001527236310300215535ustar00rootroot00000000000000azure-vm-utils-0.8.0/initramfs/initramfs-tools/hooks/000077500000000000000000000000001527236310300226765ustar00rootroot00000000000000azure-vm-utils-0.8.0/initramfs/initramfs-tools/hooks/azure-disk000077500000000000000000000010051527236310300246760ustar00rootroot00000000000000#!/bin/sh PREREQ="udev" prereqs() { echo "${PREREQ}" } case $1 in prereqs) prereqs exit 0 ;; esac . /usr/share/initramfs-tools/hook-functions DESTINATION="${DESTDIR}/lib/udev/rules.d/" RULES_FILE="/usr/lib/udev/rules.d/80-azure-disk.rules" if [ -e "${RULES_FILE}" ]; then mkdir -p "${DESTINATION}" cp -p "${RULES_FILE}" "${DESTINATION}" fi copy_exec "/usr/bin/cut" "${DESTDIR}/bin/" copy_exec "/usr/bin/readlink" "${DESTDIR}/bin/" copy_exec "/usr/sbin/azure-nvme-id" "${DESTDIR}/sbin/" azure-vm-utils-0.8.0/initramfs/initramfs-tools/hooks/azure-unmanaged-sriov000077500000000000000000000005751527236310300270560ustar00rootroot00000000000000#!/bin/sh PREREQ="udev" prereqs() { echo "${PREREQ}" } case $1 in prereqs) prereqs exit 0 ;; esac . /usr/share/initramfs-tools/hook-functions DESTINATION="${DESTDIR}/lib/udev/rules.d/" RULES_FILE="/usr/lib/udev/rules.d/10-azure-unmanaged-sriov.rules" if [ -e "${RULES_FILE}" ]; then mkdir -p "${DESTINATION}" cp -p "${RULES_FILE}" "${DESTINATION}" fi azure-vm-utils-0.8.0/networkd/000077500000000000000000000000001527236310300162625ustar00rootroot00000000000000azure-vm-utils-0.8.0/networkd/01-azure-unmanaged-sriov.network000066400000000000000000000005361527236310300243420ustar00rootroot00000000000000# Azure VMs with accelerated networking may have MANA, mlx4, or mlx5 SR-IOV # devices which are transparently bonded to a synthetic hv_netvsc device. # 10-azure-unmanaged-sriov.rules will mark these devices with # AZURE_UNMANAGED_SRIOV=1 so this can configure the devices as unmanaged. [Match] Property=AZURE_UNMANAGED_SRIOV=1 [Link] Unmanaged=yes azure-vm-utils-0.8.0/packaging/000077500000000000000000000000001527236310300163515ustar00rootroot00000000000000azure-vm-utils-0.8.0/packaging/almalinux000077700000000000000000000000001527236310300215232fedora/ustar00rootroot00000000000000azure-vm-utils-0.8.0/packaging/azurelinux000077700000000000000000000000001527236310300221342mariner/ustar00rootroot00000000000000azure-vm-utils-0.8.0/packaging/debian/000077500000000000000000000000001527236310300175735ustar00rootroot00000000000000azure-vm-utils-0.8.0/packaging/debian/azure-vm-utils-selftest.install000066400000000000000000000001201527236310300257070ustar00rootroot00000000000000/usr/sbin/azure-vm-utils-selftest /usr/share/man/man8/azure-vm-utils-selftest.8 azure-vm-utils-0.8.0/packaging/debian/azure-vm-utils.install000066400000000000000000000007561527236310300240770ustar00rootroot00000000000000/etc/azure-ephemeral-disk-setup.conf /usr/lib/systemd/network/01-azure-unmanaged-sriov.network /usr/lib/systemd/system/azure-ephemeral-disk-setup.service /usr/lib/udev/rules.d/10-azure-unmanaged-sriov.rules /usr/lib/udev/rules.d/80-azure-disk.rules /usr/sbin/azure-ephemeral-disk-setup /usr/sbin/azure-nvme-id /usr/share/initramfs-tools/hooks/azure-disk /usr/share/initramfs-tools/hooks/azure-unmanaged-sriov /usr/share/man/man8/azure-ephemeral-disk-setup.8 /usr/share/man/man8/azure-nvme-id.8 azure-vm-utils-0.8.0/packaging/debian/control000066400000000000000000000015711527236310300212020ustar00rootroot00000000000000Source: azure-vm-utils Section: admin Priority: optional Maintainer: Chris Patterson Build-Depends: cmake, debhelper-compat (= 12), pkg-config, libcmocka-dev, libjson-c-dev, python3 Standards-Version: 4.5.0 Homepage: https://github.com/Azure/azure-vm-utils Rules-Requires-Root: no Package: azure-vm-utils Architecture: any Multi-Arch: foreign Depends: ${misc:Depends}, ${shlibs:Depends}, mdadm Recommends: xfsprogs Description: Core utilities and configuration for Linux VMs on Azure This package provides a home for core utilities, udev rules and other configuration to support Linux VMs on Azure. Package: azure-vm-utils-selftest Architecture: all Multi-Arch: foreign Depends: ${misc:Depends}, python3, azure-vm-utils (= ${binary:Version}) Description: Self-test script for Azure VM Utils Test script for maintainer sanity checks in various environments. azure-vm-utils-0.8.0/packaging/debian/copyright000066400000000000000000000024721527236310300215330ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: azure-vm-utils Upstream-Contact: https://github.com/Azure/azure-vm-utils/issues Source: https://github.com/Azure/azure-vm-utils Files: * Copyright: 2024 Microsoft Corporation License: Expat License: Expat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: . The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. . THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE azure-vm-utils-0.8.0/packaging/debian/rules000077500000000000000000000004161527236310300206540ustar00rootroot00000000000000#!/usr/bin/make -f export DH_VERBOSE = 1 export DEB_BUILD_MAINT_OPTIONS = hardening=+all export DEB_CFLAGS_MAINT_APPEND = -Wall -Werror -Wextra export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed %: dh $@ override_dh_auto_test: CTEST_OUTPUT_ON_FAILURE=1 dh_auto_test azure-vm-utils-0.8.0/packaging/debian/source/000077500000000000000000000000001527236310300210735ustar00rootroot00000000000000azure-vm-utils-0.8.0/packaging/debian/source/format000066400000000000000000000000151527236310300223020ustar00rootroot000000000000003.0 (native) azure-vm-utils-0.8.0/packaging/debian/source/lintian-overrides000066400000000000000000000001141527236310300244500ustar00rootroot00000000000000source: no-nmu-in-changelog source: source-nmu-has-incorrect-version-number azure-vm-utils-0.8.0/packaging/fedora/000077500000000000000000000000001527236310300176115ustar00rootroot00000000000000azure-vm-utils-0.8.0/packaging/fedora/azure-vm-utils.spec000066400000000000000000000036341527236310300233770ustar00rootroot00000000000000Name: azure-vm-utils Version: %{__git_version} Release: %{__git_release}%{?dist} Summary: Utilities and udev rules for Linux on Azure License: MIT URL: https://github.com/Azure/%{name} Source0: %{name}_dev.tgz BuildRequires: cmake BuildRequires: gcc BuildRequires: json-c-devel BuildRequires: libcmocka-devel Requires: mdadm Requires: util-linux %description A collection of utilities and udev rules to make the most of the Linux experience on Azure. %package selftest Summary: Self-test script for Azure VM Utils Requires: %{name} = %{version}-%{release} %if 0%{?rhel} == 8 || 0%{?centos} == 8 || 0%{?almalinux} == 8 Requires: python39 %else Requires: python3 >= 3.9 %endif %description selftest This package contains the self-test script for the Azure VM Utils package. %prep %autosetup %build %cmake -DVERSION="%{version}-%{release}" -DAZURE_NVME_ID_INSTALL_DIR="%{_bindir}" %cmake_build %install %cmake_install %check %ctest %if 0%{?rhel} == 8 && 0%{?centos} == 8 && 0%{?almalinux} == 8 %undefine __brp_mangle_shebangs %endif %files %{_exec_prefix}/lib/dracut/modules.d/97azure-disk/module-setup.sh %{_exec_prefix}/lib/dracut/modules.d/97azure-unmanaged-sriov/module-setup.sh %{_exec_prefix}/lib/systemd/network/01-azure-unmanaged-sriov.network %{_exec_prefix}/lib/systemd/system/azure-ephemeral-disk-setup.service %{_exec_prefix}/lib/udev/rules.d/10-azure-unmanaged-sriov.rules %{_exec_prefix}/lib/udev/rules.d/80-azure-disk.rules %{_bindir}/azure-ephemeral-disk-setup %{_bindir}/azure-nvme-id %{_mandir}/man8/azure-ephemeral-disk-setup.8.gz %{_mandir}/man8/azure-nvme-id.8.gz %{_sysconfdir}/azure-ephemeral-disk-setup.conf %files selftest %{_bindir}/azure-vm-utils-selftest %{_mandir}/man8/azure-vm-utils-selftest.8.gz %changelog * Wed Feb 05 2025 Test - %{__git_version}-%{__git_release} - Test test test azure-vm-utils-0.8.0/packaging/mariner/000077500000000000000000000000001527236310300200065ustar00rootroot00000000000000azure-vm-utils-0.8.0/packaging/mariner/azure-vm-utils.spec000066400000000000000000000031551527236310300235720ustar00rootroot00000000000000Name: azure-vm-utils Version: %{__git_version} Release: %{__git_release}%{?dist} Summary: Utilities and udev rules for Linux on Azure License: MIT URL: https://github.com/Azure/%{name} Source0: azure-vm-utils_dev.tgz BuildRequires: binutils BuildRequires: cmake BuildRequires: gcc BuildRequires: glibc-devel BuildRequires: json-c-devel BuildRequires: kernel-headers BuildRequires: libcmocka-devel BuildRequires: make Requires: mdadm Requires: util-linux %description A collection of utilities and udev rules to make the most of the Linux experience on Azure. %package selftest Summary: Self-test script for Azure VM Utils Requires: %{name} = %{version}-%{release} Requires: python3 %description selftest This package contains the self-test script for the Azure VM Utils package. %prep %autosetup %build %cmake -DVERSION="%{version}-%{release}" %cmake_build %install %cmake_install %check %ctest %files %{_libdir}/dracut/modules.d/97azure-disk/module-setup.sh %{_libdir}/dracut/modules.d/97azure-unmanaged-sriov/module-setup.sh %{_libdir}/systemd/network/01-azure-unmanaged-sriov.network %{_libdir}/systemd/system/azure-ephemeral-disk-setup.service %{_libdir}/udev/rules.d/10-azure-unmanaged-sriov.rules %{_libdir}/udev/rules.d/80-azure-disk.rules %{_sbindir}/azure-ephemeral-disk-setup %{_sbindir}/azure-nvme-id %{_mandir}/man8/azure-ephemeral-disk-setup.8.gz %{_mandir}/man8/azure-nvme-id.8.gz %{_sysconfdir}/azure-ephemeral-disk-setup.conf %files selftest %{_sbindir}/azure-vm-utils-selftest %{_mandir}/man8/azure-vm-utils-selftest.8.gz azure-vm-utils-0.8.0/packaging/rhel000077700000000000000000000000001527236310300204632fedora/ustar00rootroot00000000000000azure-vm-utils-0.8.0/scripts/000077500000000000000000000000001527236310300161145ustar00rootroot00000000000000azure-vm-utils-0.8.0/scripts/build-deb.sh000077500000000000000000000026211527236310300203030ustar00rootroot00000000000000#!/bin/bash set -eux -o pipefail if [[ $UID -ne 0 ]]; then sudo_cmd="sudo" else sudo_cmd="" fi # Ensure dependencies are installed and up-to-date. $sudo_cmd apt update $sudo_cmd apt install -y \ build-essential \ clang-format \ cmake \ cppcheck \ devscripts \ debhelper \ gcc \ initramfs-tools \ libcmocka-dev \ libjson-c-dev \ mdadm \ pandoc \ pkg-config \ rsync project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" git_version="$(git describe --tags --always --dirty)" git_ref="$(echo "${git_version}" | sed 's/.*-g//' | sed 's/-dirty/DIRTY/')" deb_version="$(echo "${git_version}" | sed 's/^v//' | sed 's/-.*//')+git${git_ref}" output_dir="${project_dir}/out" echo "project root: $project_dir" echo "project version: $git_version" cd "$project_dir" mkdir -p debian rsync -a packaging/debian/. debian/. rm -f debian/changelog if [[ -z ${DEBEMAIL:-} ]]; then git_user="$(git config user.name || echo Azure VM Utils CI)" git_email="$(git config user.email || echo azure/azure-vm-utils@github.com)" export DEBEMAIL="${git_user} <${git_email}>" fi dch --create -v "${deb_version}" --package "azure-vm-utils" "development build: ${git_version}" debuild --no-tgz-check mkdir -p "${output_dir}" rm -f "${output_dir}"/*.deb mv ../azure-vm-utils*"${deb_version}"* "${output_dir}"/ azure-vm-utils-0.8.0/scripts/build-rpm.sh000077500000000000000000000027011527236310300203460ustar00rootroot00000000000000#!/bin/bash # shellcheck disable=SC1091 set -eu -o pipefail if [[ $UID -ne 0 ]]; then sudo_cmd="sudo" else sudo_cmd="" fi source /etc/os-release distro="$ID" project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" version="$(git describe --tags | cut -f 1 -d "-" | sed 's/^v//')" release="dev_$(git describe --tags --dirty --always | sed 's/^v//g' | sed 's/-/_/g')" output_dir="${project_dir}/out" echo "project root: ${project_dir}" echo "build version: ${version}" echo "build release: ${release}" set -x # Create rpmbuild directory layout. build_dir="$(mktemp -d)" mkdir -p "${build_dir}"/{BUILD,RPMS,SOURCES,SPECS,SRPMS} # Create source tarball. output_source="${build_dir}/SOURCES/azure-vm-utils_dev.tgz" cd "$project_dir" git archive --verbose --format=tar.gz --prefix="azure-vm-utils-${version}/" HEAD --output "${output_source}" # Create spec file from template. cd "${project_dir}/packaging/${distro}" # Install dependencies. build_requirements=$(grep ^BuildRequires azure-vm-utils.spec | awk '{{print $2}}' | tr '\n' ' ') # shellcheck disable=SC2086 $sudo_cmd dnf install -y ${build_requirements} rpm-build dracut # Build RPM. rpmbuild -ba --define "__git_version ${version}" --define "__git_release ${release}" --define "_topdir ${build_dir}" azure-vm-utils.spec # Copy RPM to output directory. mkdir -p "${output_dir}" rm -f "${output_dir}"/*.rpm cp -v "${build_dir}"/RPMS/*/azure-vm-utils*"${version}-${release}"*.rpm "${output_dir}" azure-vm-utils-0.8.0/scripts/generate-cloud-config-ephemeral-disk-setup.sh000077500000000000000000000031321527236310300266410ustar00rootroot00000000000000#!/bin/bash # Generate cloud-config.yaml for azure-ephemeral-disk-setup without requiring curl, etc. set -e SCRIPT="ephemeral-disk-setup/azure-ephemeral-disk-setup" SERVICE="ephemeral-disk-setup/azure-ephemeral-disk-setup.service" CONF="ephemeral-disk-setup/azure-ephemeral-disk-setup.conf" OUTPUT="cloud-config-ephemeral-disk-setup.yaml" INSTALL_BIN_DIR="/usr/local/bin" INSTALL_ETC_DIR="/etc" SYSTEMD_SYSTEM_SERVICE_DIR="/etc/systemd/system" # Validate all input files for file in "$SCRIPT" "$SERVICE" "$CONF"; do if [ ! -f "$file" ]; then echo "❌ Missing required file: $file" exit 1 fi done # Indent helper indent_file() { awk '{ print " " $0 }' "$1" } SCRIPT_CONTENT=$(indent_file "$SCRIPT" | sed "s|@AZURE_EPHEMERAL_DISK_SETUP_CONF_INSTALL_DIR@|$INSTALL_ETC_DIR|g") # Get service with updated install binary directory. SERVICE_CONTENT=$(indent_file "$SERVICE" | sed "s|@AZURE_NVME_ID_INSTALL_DIR@|$INSTALL_BIN_DIR|g") # Get config. CONF_CONTENT=$(indent_file "$CONF") # Generate cloud-config cat < "$OUTPUT" #cloud-config write_files: - path: $INSTALL_BIN_DIR/azure-ephemeral-disk-setup permissions: '0755' content: | $SCRIPT_CONTENT - path: $SYSTEMD_SYSTEM_SERVICE_DIR/azure-ephemeral-disk-setup.service permissions: '0644' content: | $SERVICE_CONTENT - path: $INSTALL_ETC_DIR/azure-ephemeral-disk-setup.conf permissions: '0644' content: | $CONF_CONTENT runcmd: - systemctl daemon-reload - systemctl enable azure-ephemeral-disk-setup.service - systemctl start azure-ephemeral-disk-setup.service EOF echo "✅ cloud-config written to $OUTPUT" azure-vm-utils-0.8.0/scripts/update-selftest-requirements.sh000077500000000000000000000011271527236310300243060ustar00rootroot00000000000000#!/bin/bash set -euo pipefail project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT output="test-requirements-frozen.txt" docker run --rm \ --mount "type=bind,src=$project_dir/selftest,dst=/selftest,readonly" \ "python:3.10-slim" sh -ec ' python -m venv /tmp/venv /tmp/venv/bin/python -m pip install --disable-pip-version-check \ -r /selftest/test-requirements.txt >&2 /tmp/venv/bin/python -m pip freeze ' > "$tmp_dir/$output" cat "$tmp_dir/$output" > "$project_dir/selftest/$output" azure-vm-utils-0.8.0/selftest/000077500000000000000000000000001527236310300162565ustar00rootroot00000000000000azure-vm-utils-0.8.0/selftest/__init__.py000066400000000000000000000000001527236310300203550ustar00rootroot00000000000000azure-vm-utils-0.8.0/selftest/conftest.py000066400000000000000000000061051527236310300204570ustar00rootroot00000000000000# -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See LICENSE in the project root for license information. # -------------------------------------------------------------------------- """pytest configuration for selftest.""" import logging import os import sys from datetime import datetime from pathlib import Path import pytest logger = logging.getLogger(__name__) # pylint: disable=unused-argument def pytest_sessionstart(session): """Initialize session attributes that are marked on finish.""" session.aborted = False session.failed = False def pytest_sessionfinish(session, exitstatus): """Mark test session as aborted or failed.""" if exitstatus == 2: # Interrupted (e.g., Ctrl+C) session.aborted = True elif exitstatus != 0: session.failed = True def pytest_configure(config): """Configure pytest logging and artifacts directory. Create artifacts directory based on the current timestamp if not configured. This is used to store logs and other artifacts generated during the test run. If running under pytest-xdist, use the worker ID to create separate logs for each worker. """ artifacts_path = os.getenv("SELFTEST_ARTIFACTS_PATH") if "PYTEST_XDIST_WORKER" not in os.environ: for name in ( "SELFTEST_AZURE_LOCATION", "SELFTEST_HOLD_FAILURES", "SELFTEST_INSTALL_FROM_SOURCE", "SELFTEST_RESOURCE_GROUP", "SELFTEST_PUBLIC_IP_TAGS", "SELFTEST_REALLOCATES", "SELFTEST_REBOOTS", ): print(f"{name}={os.getenv(name)}", file=sys.stderr) if not artifacts_path: timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f") artifacts_path = f"/tmp/selftest-{timestamp}" os.environ["SELFTEST_ARTIFACTS_PATH"] = artifacts_path print(f"artifacts={artifacts_path}", file=sys.stderr) artifacts_path = os.getenv("SELFTEST_ARTIFACTS_PATH") assert artifacts_path, "SELFTEST_ARTIFACTS_PATH must be set" log_path = Path(artifacts_path) log_path.mkdir(parents=True, exist_ok=True) worker_id = os.environ.get("PYTEST_XDIST_WORKER") if worker_id is not None: logging.basicConfig( format="%(asctime)s [%(levelname)s] %(message)s", filename=log_path / f"{worker_id}.log", level=logging.DEBUG, ) else: logging.basicConfig( format="%(asctime)s [%(levelname)s] %(message)s", filename=log_path / "main.log", level=logging.DEBUG, ) logger.debug("test") @pytest.hookimpl(tryfirst=True) def pytest_collection_modifyitems(config, items): """Group tests by vm_size for pytest-xdist to avoid quota issues.""" for item in items: vm_size = ( item.callspec.params.get("vm_size", None) if hasattr(item, "callspec") else None ) if vm_size: item.add_marker(pytest.mark.xdist_group(vm_size)) azure-vm-utils-0.8.0/selftest/install-from-source.sh000077500000000000000000000067021527236310300225270ustar00rootroot00000000000000#!/bin/sh # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. set -eu source_dir=$1 version=$2 trap 'rm -rf "$source_dir"' EXIT zypper_with_retries() { max_attempts=30 attempt=1 while :; do if LC_ALL=C zypper --non-interactive "$@" > "$source_dir/zypper.log" 2>&1; then cat "$source_dir/zypper.log" return 0 else status=$? fi cat "$source_dir/zypper.log" if [ "$status" -ne 8 ] || ! grep -Eq "can't create transaction lock.*Resource temporarily unavailable" "$source_dir/zypper.log"; then return "$status" fi if [ "$attempt" -ge "$max_attempts" ]; then echo "RPM transaction lock remained busy after $attempt zypper attempts." >&2 return "$status" fi attempt=$((attempt + 1)) printf 'RPM transaction lock is busy; retrying zypper in 10 seconds (attempt %s/%s).\n' "$attempt" "$max_attempts" >&2 sleep 10 done } configure_selftest_python() { # SLES 15 keeps Python 3.6 as python3 and provides python3.11 alongside it. for interpreter in python3 python3.11; do if command -v "$interpreter" >/dev/null 2>&1 && "$interpreter" -c 'from dataclasses import dataclass; from typing import Literal' 2>/dev/null; then interpreter_path=$(command -v "$interpreter") sed -i "1c\\#!$interpreter_path" "$1" printf 'Selftest Python: %s\n' "$interpreter_path" return fi done echo "Selftest requires Python 3.8 or newer with dataclasses and typing.Literal" >&2 return 1 } if command -v apt-get >/dev/null 2>&1; then export DEBIAN_FRONTEND=noninteractive export NEEDRESTART_MODE=a set -- -o Acquire::Retries=2 -o DPkg::Lock::Timeout=-1 apt-get "$@" -o APT::Update::Error-Mode=any update apt-get "$@" install -y --no-install-recommends \ gcc libc6-dev make cmake pkg-config libjson-c-dev elif command -v zypper >/dev/null 2>&1; then zypper_with_retries refresh zypper_with_retries install --no-recommends \ gcc glibc-devel make cmake pkg-config libjson-c-devel elif command -v dnf >/dev/null 2>&1; then dnf -y --setopt=install_weak_deps=False install \ gcc glibc-devel make cmake pkgconf-pkg-config json-c-devel elif command -v tdnf >/dev/null 2>&1; then tdnf -y install gcc glibc-devel make cmake pkg-config json-c-devel else echo "Unsupported guest: source-install mode requires apt-get, zypper, dnf or tdnf" >&2 exit 1 fi cmake -S "$source_dir" -B "$source_dir/build" \ -DENABLE_TESTS=OFF -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_INSTALL_PREFIX=/usr -DVERSION="$version" cmake --build "$source_dir/build" --parallel 2 cmake --install "$source_dir/build" configure_selftest_python /usr/sbin/azure-vm-utils-selftest systemctl daemon-reload # Include the new rules and helpers in early boot as well as the root filesystem. if command -v update-initramfs >/dev/null 2>&1; then update-initramfs -u -k all elif command -v dracut >/dev/null 2>&1; then dracut --regenerate-all --force else echo "Cannot update initramfs: neither update-initramfs nor dracut is available" >&2 exit 1 fi while IFS= read -r installed; do sha256sum "$installed" done < "$source_dir/build/install_manifest.txt" > /var/tmp/azure-vm-utils-selftest.sha256 sha256sum --check /var/tmp/azure-vm-utils-selftest.sha256 /usr/sbin/azure-nvme-id --version azure-vm-utils-0.8.0/selftest/selftest.py000077500000000000000000002140601527236310300204670ustar00rootroot00000000000000#!/usr/bin/env python3 # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See LICENSE in the project root for license information. # -------------------------------------------------------------------------- """Azure VM utilities self-tests script.""" import argparse import glob import json import logging import os import re import shlex import subprocess import time import urllib.error import urllib.request import uuid from dataclasses import dataclass, field from pathlib import Path from typing import Dict, List, Literal, Optional, Tuple logger = logging.getLogger("selftest") # pylint: disable=broad-exception-caught # pylint: disable=line-too-long # pylint: disable=too-many-branches # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-lines # pylint: disable=too-many-locals SYS_CLASS_BLOCK = "/sys/class/block" SYS_CLASS_NET = "/sys/class/net" AZURE_EPHEMERAL_DISK_SETUP_CONF = "/etc/azure-ephemeral-disk-setup.conf" AZURE_EPHEMERAL_DISK_SETUP_SERVICE = "azure-ephemeral-disk-setup.service" DEV_DISK_AZURE_RESOURCE = "/dev/disk/azure/resource" DEV_DISK_CLOUD_AZURE_RESOURCE = "/dev/disk/cloud/azure_resource" # Global state. AZURE_VM_UTILS_VERSION: str = "" AZURE_VM_UTILS_VERSION_TUPLE: Optional[Tuple[int, int, int]] = None COMPAT_CHECKS_ENABLED: bool = False def is_azure_vm_utils_compat(max_unsupported_version: Tuple[int, int, int]) -> bool: """Check whether compatibility applies through max_unsupported_version.""" return ( COMPAT_CHECKS_ENABLED and AZURE_VM_UTILS_VERSION_TUPLE is not None and AZURE_VM_UTILS_VERSION_TUPLE <= max_unsupported_version ) def is_systemd_fixed_for_unmanaged_bug(systemd_version: str) -> bool: """Check if systemd version is >= 257.7. Due to https://github.com/systemd/systemd/issues/36997 there may be an unexpected IP assigned to VF that we would otherwise assert on. """ parts = systemd_version.split(".") match = re.search(r"\d+", parts[0]) if not match: raise RuntimeError( f"Unexpected systemd version format: {systemd_version}. Expected format is 'major.minor'." ) major = int(match.group()) minor = 0 # minor may have -patch data, remove it then convert to int. if len(parts) > 1: match = re.search(r"\d+", parts[1]) if match: minor = int(match.group()) return major > 257 or (major == 257 and minor >= 7) @dataclass(eq=True, repr=True) class SkuConfig: """VM sku-specific configuration related to disks.""" vm_size: str vm_size_type: Literal["arm64", "x64"] = "x64" nvme_controller_toggle_supported: bool = ( False # whether the sku supports NVMe controller toggle (Eb[d]s_v5) ) nvme_only: bool = False # NVMe-only skus (v6+) nvme_id_enabled_local: bool = False # whether the sku supports NVMe ID locally nvme_id_enabled_remote: bool = False # whether the sku supports NVMe ID remotely nvme_local_disk_count: int = 0 nvme_local_disk_size_gib: int = 0 temp_disk_size_gib: int = 0 # SCSI temp/resource disk size in GiB @dataclass(eq=True, repr=True) class V6SkuConfig(SkuConfig): """V6 VM sku-specific configuration related to disks.""" nvme_only: bool = True nvme_id_enabled_local: bool = True nvme_id_enabled_remote: bool = False def gb_to_gib(size_gb: int) -> int: """Roughly convert GB to GiB as sizes are documented in both ways.""" return int(size_gb * (1000**3) / (1024**3)) def unchecked_run(cmd) -> str: """ "Run a command without checking the return code and return stripped output.""" result = subprocess.run(cmd, capture_output=True, text=True, check=False) return result.stdout.strip() SKU_CONFIGS = { "Standard_B2ts_v2": SkuConfig(vm_size="Standard_B2ts_v2"), # "Standard_D2s_v3": SkuConfig(vm_size="Standard_D2s_v3", temp_disk_size_gib=16), "Standard_D2s_v4": SkuConfig(vm_size="Standard_D2s_v4"), "Standard_D2ds_v4": SkuConfig(vm_size="Standard_D2ds_v4", temp_disk_size_gib=75), "Standard_D2s_v5": SkuConfig(vm_size="Standard_D2s_v5"), "Standard_D2ds_v5": SkuConfig(vm_size="Standard_D2ds_v5", temp_disk_size_gib=75), "Standard_D2ads_v5": SkuConfig(vm_size="Standard_D2ads_v5", temp_disk_size_gib=75), "Standard_D16ads_v5": SkuConfig( vm_size="Standard_D16ads_v5", temp_disk_size_gib=600 ), "Standard_L8s_v2": SkuConfig( vm_size="Standard_L8s_v2", temp_disk_size_gib=80, nvme_local_disk_count=1, nvme_local_disk_size_gib=gb_to_gib(1920), ), "Standard_L8s_v3": SkuConfig( vm_size="Standard_L8s_v3", temp_disk_size_gib=80, nvme_local_disk_count=1, nvme_local_disk_size_gib=gb_to_gib(1920), ), "Standard_L80s_v3": SkuConfig( vm_size="Standard_L80s_v3", nvme_controller_toggle_supported=True, temp_disk_size_gib=800, nvme_local_disk_count=10, nvme_local_disk_size_gib=gb_to_gib(1920), ), "Standard_L8s_v4": SkuConfig( vm_size="Standard_L8s_v4", nvme_only=True, nvme_id_enabled_local=True, nvme_local_disk_count=4, nvme_local_disk_size_gib=gb_to_gib(480), ), "Standard_L80s_v4": SkuConfig( vm_size="Standard_L80s_v4", nvme_only=True, nvme_id_enabled_local=True, nvme_local_disk_count=10, nvme_local_disk_size_gib=gb_to_gib(1920), ), "Standard_E2bs_v5": SkuConfig( vm_size="Standard_E2bs_v5", nvme_controller_toggle_supported=True ), "Standard_E2bds_v5": SkuConfig( vm_size="Standard_E2bds_v5", nvme_controller_toggle_supported=True, temp_disk_size_gib=75, ), "Standard_D2s_v6": V6SkuConfig(vm_size="Standard_D2s_v6"), "Standard_D2ds_v6": V6SkuConfig( vm_size="Standard_D2ds_v6", nvme_local_disk_count=1, nvme_local_disk_size_gib=110, ), "Standard_D16ds_v6": V6SkuConfig( vm_size="Standard_D16ds_v6", nvme_local_disk_count=2, nvme_local_disk_size_gib=440, ), "Standard_D32ds_v6": V6SkuConfig( vm_size="Standard_D32ds_v6", nvme_local_disk_count=4, nvme_local_disk_size_gib=440, ), "Standard_D2as_v6": V6SkuConfig(vm_size="Standard_D2as_v6"), "Standard_D2ads_v6": V6SkuConfig( vm_size="Standard_D2ads_v6", nvme_local_disk_count=1, nvme_local_disk_size_gib=110, ), "Standard_D16ads_v6": V6SkuConfig( vm_size="Standard_D16ads_v6", nvme_local_disk_count=2, nvme_local_disk_size_gib=440, ), "Standard_D32ads_v6": V6SkuConfig( vm_size="Standard_D32ads_v6", nvme_local_disk_count=4, nvme_local_disk_size_gib=440, ), "Standard_D2ds_v7": SkuConfig( vm_size="Standard_D2ds_v7", nvme_only=True, nvme_id_enabled_local=True, nvme_local_disk_count=1, nvme_local_disk_size_gib=110, ), "Standard_D32ds_v7": SkuConfig( vm_size="Standard_D32ds_v7", nvme_only=True, nvme_id_enabled_local=True, nvme_local_disk_count=4, nvme_local_disk_size_gib=440, ), "Standard_D32ads_v7": SkuConfig( vm_size="Standard_D32ads_v7", nvme_only=True, nvme_id_enabled_local=True, nvme_local_disk_count=4, nvme_local_disk_size_gib=440, ), "Standard_D2pls_v5": SkuConfig( vm_size="Standard_D2pls_v5", vm_size_type="arm64", ), "Standard_D2plds_v5": SkuConfig( vm_size="Standard_D2plds_v5", vm_size_type="arm64", temp_disk_size_gib=75, ), "Standard_D8pls_v5": SkuConfig( vm_size="Standard_D8pls_v5", vm_size_type="arm64", ), "Standard_D8plds_v5": SkuConfig( vm_size="Standard_D8plds_v5", vm_size_type="arm64", temp_disk_size_gib=300, ), "Standard_D2pls_v6": SkuConfig( vm_size="Standard_D2pls_v6", vm_size_type="arm64", ), "Standard_D2plds_v6": SkuConfig( vm_size="Standard_D2plds_v6", vm_size_type="arm64", nvme_local_disk_count=1, nvme_local_disk_size_gib=110, ), "Standard_D16pls_v6": SkuConfig( vm_size="Standard_D16pls_v6", vm_size_type="arm64", ), "Standard_D16plds_v6": SkuConfig( vm_size="Standard_D16plds_v6", vm_size_type="arm64", nvme_local_disk_count=2, nvme_local_disk_size_gib=440, ), } def device_sort(devices: List[str]) -> List[str]: """Natural sort for devices.""" def natural_sort_key(s: str): # Natural sort by turning a string into a list of string and number chunks. # e.g. "nvme0n10" -> ["nvme", 0, "n", 10] return [ int(text) if text.isdigit() else text for text in re.split("([0-9]+)", s) ] return sorted(devices, key=natural_sort_key) def get_disk_io_timeout(disk_name: str) -> int: """Get disk's queue/io_timeout.""" timeout_path = Path(SYS_CLASS_BLOCK, disk_name, "queue/io_timeout") logger.debug("checking disk %s io_timeout: %s", disk_name, timeout_path) if not timeout_path.exists(): logger.error("disk %s io_timeout not found", disk_name) raise FileNotFoundError(f"disk {disk_name} io_timeout not found") timeout = timeout_path.read_text(encoding="utf-8").strip() try: return int(timeout) except ValueError: logger.error("invalid value for disk %s io_timeout: %r", disk_name, timeout) raise def get_disk_size_gb(disk_path: str) -> int: """Get the size of the disk in GB.""" try: proc = subprocess.run( ["lsblk", "-b", "-n", "-o", "SIZE", "-d", disk_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True, ) logger.debug("lsblk output: %r", proc) size_bytes = int(proc.stdout.strip()) size_gib = size_bytes // (1000**3) return size_gib except subprocess.CalledProcessError as error: logger.error("error while fetching disk size: %r", error) raise except FileNotFoundError: logger.error("lsblk command not found") raise def get_disk_size_gib(disk_path: str) -> int: """Get the size of the disk in GiB.""" try: proc = subprocess.run( ["lsblk", "-b", "-n", "-o", "SIZE", "-d", disk_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True, ) logger.debug("lsblk output: %r", proc) size_bytes = int(proc.stdout.strip()) size_gib = size_bytes // (1024**3) return size_gib except subprocess.CalledProcessError as error: logger.error("error while fetching disk size: %r", error) raise except FileNotFoundError: logger.error("lsblk command not found") raise def get_imds_metadata() -> Dict: """Fetch IMDS metadata using urllib.""" url = "http://169.254.169.254/metadata/instance?api-version=2021-02-01" request_id = str(uuid.uuid4()) headers = {"Metadata": "true", "x-ms-client-request-id": request_id} req = urllib.request.Request(url, headers=headers) last_error = None deadline = time.time() + 300 while time.time() < deadline: try: with urllib.request.urlopen(req, timeout=60) as response: if response.status != 200: raise urllib.error.HTTPError( url, response.status, "Failed to fetch metadata", response.headers, None, ) metadata = json.load(response) logger.debug( "fetched IMDS metadata with request id %s: %r", request_id, metadata ) return metadata except urllib.error.URLError as error: last_error = error logger.error( "error fetching IMDS metadata with request id %s: %r", request_id, error ) time.sleep(1) raise RuntimeError(f"failed to fetch IMDS metadata: {last_error}") def get_local_nvme_disks() -> List[str]: """Get all local NVMe disks.""" local_disk_controllers = get_nvme_controllers_with_model( "Microsoft NVMe Direct Disk" ) local_disk_controllers_v2 = get_nvme_controllers_with_model( "Microsoft NVMe Direct Disk v2" ) return device_sort( [ namespace for controller in local_disk_controllers + local_disk_controllers_v2 for namespace in get_nvme_namespace_devices(controller) ] ) def get_remote_nvme_disks() -> List[str]: """Get all remote NVMe disks.""" remote_disk_controllers = get_nvme_controllers_with_model( "MSFT NVMe Accelerator v1.0" ) return device_sort( [ namespace for controller in remote_disk_controllers for namespace in get_nvme_namespace_devices(controller) ] ) def get_nvme_namespace_id(device_name: str) -> int: """Read namespace ID from sysfs for the given NVMe device.""" nsid = int( Path(SYS_CLASS_BLOCK, device_name, "nsid").read_text(encoding="utf-8").strip() ) if nsid <= 0: raise ValueError(f"invalid namespace ID for {device_name}: {nsid}") return nsid def get_nvme_controllers_with_model(model: str) -> List[str]: """Get a list of all NVMe controllers with the specified model.""" nvme_controllers = [] nvme_path = "/sys/class/nvme" for controller in glob.glob(os.path.join(nvme_path, "nvme*")): logger.debug("checking controller: %s", controller) model_path = os.path.join(controller, "model") try: with open(model_path, "r", encoding="utf-8") as file: controller_model = file.read().strip() logger.debug("controller: %s model: %s", controller, controller_model) if controller_model == model: controller_name = controller.split("/")[-1] nvme_controllers.append(controller_name) except FileNotFoundError: logger.debug("model file not found: %s", model_path) continue return device_sort(nvme_controllers) def get_nvme_namespace_devices_with_model(model: str) -> List[str]: """Get all NVMe namespace devices for a given NVMe controller model.""" controllers = get_nvme_controllers_with_model(model) logger.debug("controllers found for model=%s: %r", model, controllers) return device_sort( [ namespace for controller in controllers for namespace in get_nvme_namespace_devices(controller) ] ) def get_nvme_namespace_devices(controller: str) -> List[str]: """Get all NVMe namespace devices for a given NVMe controller.""" namespace_devices = [] controller_name = controller.split("/")[-1] nvme_path = f"/sys/class/nvme/{controller_name}" logger.debug("checking namespaces under %s", nvme_path) for namespace in glob.glob(os.path.join(nvme_path, "nvme*")): logger.debug("checking namespace device: %s", namespace) if os.path.isdir(namespace): device_name = namespace.split("/")[-1] namespace_devices.append(device_name) return device_sort(namespace_devices) def get_root_block_device() -> str: """Get the root block device using findmnt.""" try: proc = subprocess.run( ["findmnt", "-n", "-o", "SOURCE", "/"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True, ) logger.debug("findmnt output: %r", proc) return proc.stdout.strip() except subprocess.CalledProcessError as error: logger.error("error while fetching root block device: %r", error) raise except FileNotFoundError: logger.error("findmnt command not found") raise def get_scsi_resource_disk() -> Optional[str]: """Get the SCSI resource disk device.""" paths = [ # azure resource disk "/dev/disk/azure/resource", # cloud-init udev rules "/dev/disk/cloud/azure_resource", # gen2 "/dev/disk/by-path/acpi-VMBUS:00-vmbus-f8b3781a1e824818a1c363d806ec15bb-lun-1", # gen1 "/dev/disk/by-path/acpi-VMBUS:01-vmbus-000000000001*-lun-0", ] for path in paths: if "*" in path: matched_paths = glob.glob(path) for matched_path in matched_paths: resolved_path = os.path.realpath(matched_path) if os.path.exists(resolved_path): return resolved_path.split("/")[-1] else: if os.path.exists(path): resolved_path = os.path.realpath(path) if os.path.exists(resolved_path): return resolved_path.split("/")[-1] logger.info("no SCSI resource disk found") return None @dataclass(eq=True, repr=True) class DiskInfo: """Information about different types of disks present.""" root_device: str dev_disk_azure_links: List[str] = field(default_factory=list) dev_disk_azure_resource_disk: Optional[str] = None # resolved path dev_disk_azure_resource_disk_size_gib: int = 0 dev_disk_cloud_azure_resource: Optional[str] = None # resolved path nvme_io_timeouts: Dict[str, int] = field(default_factory=dict) nvme_local_disk_size_gib: int = 0 nvme_local_disks_v1: List[str] = field(default_factory=list) nvme_local_disks_v2: List[str] = field(default_factory=list) nvme_local_disks: List[str] = field(default_factory=list) nvme_remote_data_disks: List[str] = field(default_factory=list) nvme_remote_disks: List[str] = field(default_factory=list) nvme_remote_os_disk: Optional[str] = None root_device_is_nvme: bool = False scsi_resource_disk: Optional[str] = None scsi_resource_disk_size_gib: int = 0 @classmethod def gather(cls) -> "DiskInfo": """Gather disk information and return an instance of DiskInfo.""" dev_disk_azure_links = device_sort( [ link for link in glob.glob( os.path.join("/dev/disk/azure", "**"), recursive=True ) if os.path.islink(link) ] ) dev_disk_azure_resource_disk = None dev_disk_azure_resource_disk_size_gib = 0 if os.path.exists(DEV_DISK_AZURE_RESOURCE): dev_disk_azure_resource_disk = os.path.realpath(DEV_DISK_AZURE_RESOURCE) dev_disk_azure_resource_disk_size_gib = get_disk_size_gib( dev_disk_azure_resource_disk ) dev_disk_cloud_azure_resource = None if os.path.exists(DEV_DISK_CLOUD_AZURE_RESOURCE): dev_disk_cloud_azure_resource = os.path.realpath( DEV_DISK_CLOUD_AZURE_RESOURCE ) nvme_local_disks_v1 = get_nvme_namespace_devices_with_model( "Microsoft NVMe Direct Disk" ) nvme_local_disks_v2 = get_nvme_namespace_devices_with_model( "Microsoft NVMe Direct Disk v2" ) nvme_local_disks = device_sort(nvme_local_disks_v1 + nvme_local_disks_v2) nvme_local_disk_size_gib = 0 if nvme_local_disks: nvme_local_disk_size_gib = min( get_disk_size_gib(f"/dev/{disk}") for disk in nvme_local_disks ) local_disk_max_size = max( get_disk_size_gib(f"/dev/{disk}") for disk in nvme_local_disks ) assert ( nvme_local_disk_size_gib == local_disk_max_size ), f"local disk size mismatch: {nvme_local_disk_size_gib} != {local_disk_max_size} for {nvme_local_disks}" nvme_remote_disks = get_remote_nvme_disks() nvme_remote_os_disks = [ disk for disk in nvme_remote_disks if get_nvme_namespace_id(disk) == 1 ] assert ( len(nvme_remote_os_disks) <= 1 ), f"unexpected number of remote OS disks: {nvme_remote_os_disks}" nvme_remote_os_disk = nvme_remote_os_disks[0] if nvme_remote_os_disks else None nvme_remote_data_disks = [ disk for disk in nvme_remote_disks if disk != nvme_remote_os_disk ] root_device = get_root_block_device() root_device_is_nvme = root_device.startswith("/dev/nvme") root_device = root_device.split("/")[-1] scsi_resource_disk = get_scsi_resource_disk() scsi_resource_disk_size_gib = ( get_disk_size_gib(f"/dev/{scsi_resource_disk}") if scsi_resource_disk else 0 ) nvme_io_timeouts: Dict[str, int] = {} for disk_name in nvme_local_disks + nvme_remote_disks: nvme_io_timeouts[disk_name] = get_disk_io_timeout(disk_name) disk_info = cls( dev_disk_azure_links=dev_disk_azure_links, dev_disk_azure_resource_disk=dev_disk_azure_resource_disk, dev_disk_azure_resource_disk_size_gib=dev_disk_azure_resource_disk_size_gib, dev_disk_cloud_azure_resource=dev_disk_cloud_azure_resource, nvme_io_timeouts=nvme_io_timeouts, nvme_local_disk_size_gib=nvme_local_disk_size_gib, nvme_local_disks_v1=nvme_local_disks_v1, nvme_local_disks_v2=nvme_local_disks_v2, nvme_local_disks=nvme_local_disks, nvme_remote_os_disk=nvme_remote_os_disk, nvme_remote_data_disks=nvme_remote_data_disks, nvme_remote_disks=nvme_remote_disks, root_device=root_device, root_device_is_nvme=root_device_is_nvme, scsi_resource_disk=scsi_resource_disk, scsi_resource_disk_size_gib=scsi_resource_disk_size_gib, ) return disk_info @dataclass(eq=True, repr=True) class AzureEphemeralDiskConfig: """Parsed /etc/azure-ephemeral-disk-setup.conf config.""" aggregation: str = "mdadm" fs_type: str = "ext4" mdadm_chunk: str = "512K" mdadm_name: str = "azure-ephemeral-md" mount_point: str = "/mnt" scsi_resource: bool = False @staticmethod def _parse_bool(value: str) -> Optional[bool]: if value == "true": return True if value == "false": return False return None @classmethod def gather(cls) -> "AzureEphemeralDiskConfig": """Parse service config.""" aggregation = "auto" fs_type = "ext4" mdadm_chunk = "512K" mdadm_name = "azure-ephemeral-md" mount_point = "/mnt" scsi_resource = False config_path = Path(AZURE_EPHEMERAL_DISK_SETUP_CONF) if not config_path.exists(): return cls() config_str = config_path.read_text(encoding="utf-8") for line in config_str.splitlines(): line = line.strip() if not line or line.startswith("#"): continue key, value = line.split("=", 1) value = shlex.split(value, posix=True)[0] if key == "AZURE_EPHEMERAL_DISK_SETUP_AGGREGATION": if value not in {"auto", "mdadm", "none"}: raise ValueError(f"Invalid aggregation: {value}") aggregation = value elif key == "AZURE_EPHEMERAL_DISK_SETUP_FS_TYPE": if value not in {"ext4", "xfs"}: raise ValueError(f"Invalid filesystem type: {value}") fs_type = value elif key == "AZURE_EPHEMERAL_DISK_SETUP_MDADM_CHUNK": if not re.match(r"^\d+[KMGT]$", value, re.IGNORECASE): raise ValueError(f"Invalid mdadm chunk size: {value}") mdadm_chunk = value elif key == "AZURE_EPHEMERAL_DISK_SETUP_MDADM_NAME": if not re.match(r"^[a-zA-Z0-9_-]+$", value): raise ValueError(f"Invalid mdadm array name: {value}") mdadm_name = value elif key == "AZURE_EPHEMERAL_DISK_SETUP_MOUNT_POINT": if not os.path.isabs(value) or " " in value: raise ValueError(f"Invalid mount point: {value}") mount_point = value elif key == "AZURE_EPHEMERAL_DISK_SETUP_SCSI_RESOURCE": value_bool = cls._parse_bool(value) if value_bool is None: raise ValueError(f"Invalid boolean for scsi_resource: {value}") scsi_resource = value_bool else: raise ValueError( f"Unexpected key={key} line in {AZURE_EPHEMERAL_DISK_SETUP_CONF}: {line}" ) return cls( aggregation=aggregation, fs_type=fs_type, mdadm_chunk=mdadm_chunk, mdadm_name=mdadm_name, mount_point=mount_point, scsi_resource=scsi_resource, ) @dataclass(eq=True, repr=True) class ServiceInfo: """Status of various system services.""" systemd_version: str systemd_version_fixed_unmanaged_bug: bool cloud_init_service_enabled: bool ephemeral_service_enabled: bool ephemeral_service_active: bool ephemeral_service_failed: bool ephemeral_service_journal: Optional[str] = None waagent_resource_disk_format: Optional[bool] = None @classmethod def gather(cls) -> "ServiceInfo": """Gather information about necessary system services.""" cloud_init_service_enabled = ( unchecked_run(["systemctl", "is-enabled", "cloud-init-local.service"]) == "enabled" ) ephemeral_service_enabled = ( unchecked_run( ["systemctl", "is-enabled", AZURE_EPHEMERAL_DISK_SETUP_SERVICE] ) == "enabled" ) ephemeral_service_active = ( unchecked_run( ["systemctl", "is-active", AZURE_EPHEMERAL_DISK_SETUP_SERVICE] ) == "active" ) ephemeral_service_failed = ( unchecked_run( ["systemctl", "is-failed", AZURE_EPHEMERAL_DISK_SETUP_SERVICE] ) == "failed" ) ephemeral_service_journal = unchecked_run( [ "journalctl", "--no-pager", "--output=short-precise", "--boot", "--unit", AZURE_EPHEMERAL_DISK_SETUP_SERVICE, ] ) # Get systemd version line. Then parse for detailed version. # Example first line output: `systemd 255 (255.4-1ubuntu8.8)`. systemd_version = ( unchecked_run(["systemctl", "--version"]).strip().split("\n")[0] ) systemd_version = systemd_version.split()[-1].strip("()") systemd_version_fixed_unmanaged_bug = is_systemd_fixed_for_unmanaged_bug( systemd_version ) waagent_config_path = Path("/etc/waagent.conf") waagent_resource_disk_format = ( "ResourceDisk.Format=y" in waagent_config_path.read_text(encoding="utf-8") if waagent_config_path.exists() else None ) return cls( cloud_init_service_enabled=cloud_init_service_enabled, ephemeral_service_enabled=ephemeral_service_enabled, ephemeral_service_active=ephemeral_service_active, ephemeral_service_failed=ephemeral_service_failed, ephemeral_service_journal=ephemeral_service_journal, systemd_version=systemd_version, systemd_version_fixed_unmanaged_bug=systemd_version_fixed_unmanaged_bug, waagent_resource_disk_format=waagent_resource_disk_format, ) @dataclass(eq=True, repr=True) class Mount: """Mount parameters.""" target: str source: Optional[str] = None fstype: Optional[str] = None options: Optional[str] = None rest: Dict[str, str] = field(default_factory=dict) @dataclass(eq=True, repr=True) class MountInfo: """Information about mounted filesystems from kernel and fstab sources.""" fstab_mounts: Dict[str, Mount] = field(default_factory=dict) kernel_mounts: Dict[str, Mount] = field(default_factory=dict) @classmethod def gather(cls) -> "MountInfo": """Gather mount info from both kernel and fstab.""" return cls( kernel_mounts=cls._gather_from_findmnt(mode="kernel"), fstab_mounts=cls._gather_from_findmnt(mode="fstab"), ) @staticmethod def _gather_from_findmnt(mode: str) -> Dict[str, Mount]: """Helper to run findmnt --json -- and parse output.""" assert mode in ("fstab", "kernel") cmd = ["findmnt", "--json", f"--{mode}"] proc = subprocess.run(cmd, capture_output=True, check=False, text=True) if proc.returncode != 0: logger.error( "error running findmnt: %r (stderr=%r stdout=%r)", proc, proc.stderr, proc.stdout, ) return {} data = json.loads(proc.stdout) mounts: Dict[str, Mount] = {} def add_mounts(filesystems): for fs in filesystems: target = fs.get("target") if not target: continue mounts[target] = Mount( target=target, source=fs.get("source"), fstype=fs.get("fstype"), options=fs.get("options"), rest={ k: v for k, v in fs.items() if k not in {"target", "source", "fstype", "options", "children"} }, ) # Recurse into children if they exist children = fs.get("children", []) if isinstance(children, list): add_mounts(children) add_mounts(data.get("filesystems", [])) return mounts def validate_ephemeral_mount( self, disk_info: DiskInfo, azure_ephemeral_disk_config: AzureEphemeralDiskConfig, service_info: ServiceInfo, ) -> None: """Validate ephemeral disk mount.""" mnt_fstab = self.fstab_mounts.get("/mnt") mnt_kernel = self.kernel_mounts.get("/mnt") cloud_init_resource_disk_link_exists = ( disk_info.dev_disk_cloud_azure_resource is not None ) if service_info.ephemeral_service_enabled: # The service is expected to fail if is configured to manage the # resource disk at the same time cloud-init is, or WALA is configured # to manage the resource disk. if azure_ephemeral_disk_config.scsi_resource and ( cloud_init_resource_disk_link_exists or service_info.waagent_resource_disk_format ): assert ( service_info.ephemeral_service_failed ), f"azure-ephemeral-disk-setup service did not fail as expected: {service_info.ephemeral_service_journal}" else: assert ( service_info.ephemeral_service_active ), f"azure-ephemeral-disk-setup service is enabled but not active: {service_info.ephemeral_service_journal}" assert ( not service_info.ephemeral_service_failed ), f"azure-ephemeral-disk-setup service is enabled but failed: {service_info.ephemeral_service_journal}" mount_expected = ( ( disk_info.scsi_resource_disk and cloud_init_resource_disk_link_exists and service_info.cloud_init_service_enabled ) or ( disk_info.scsi_resource_disk and azure_ephemeral_disk_config.scsi_resource and service_info.ephemeral_service_active ) or (disk_info.nvme_local_disks and service_info.ephemeral_service_active) ) if mount_expected: assert ( mnt_fstab and mnt_fstab.options and mnt_fstab.source ), f"no fstab mount found for /mnt: {self.fstab_mounts}" assert ( mnt_kernel and mnt_kernel.options and mnt_kernel.source ), f"no kernel mount found for /mnt: {self.kernel_mounts}" # Assume cloud-init manages SCSI resource disk if cloud-init's symlink is present. if ( cloud_init_resource_disk_link_exists and service_info.cloud_init_service_enabled ): assert ( "comment=cloudconfig" in mnt_fstab.options ), f"expected comment=cloudconfig in fstab options for /mnt: {mnt_fstab.options}" else: assert ( mnt_fstab.options == "defaults,nofail,comment=azure-ephemeral-disk-setup" ), f"unexpected fstab options for /mnt: {mnt_fstab.options}" assert ( mnt_fstab.source == "LABEL=AzureEphmDsk" ), f"unexpected fstab source for /mnt: {mnt_fstab.source}" assert mnt_kernel.options and ( "rw" in mnt_kernel.options ), f"missing rw in fstab options: {mnt_kernel.options}" else: if mnt_fstab: assert mnt_fstab.options and ( "comment=cloudconfig" in mnt_fstab.options ), f"unexpected fstab mount for /mnt: {mnt_fstab}" assert not mnt_kernel, f"unexpected kernel mount for /mnt: {mnt_kernel}" logger.info( "validate_ephemeral_mount OK: fstab=%r kernel=%r", mnt_fstab, mnt_kernel ) def validate( self, disk_info: DiskInfo, azure_ephemeral_disk_config: AzureEphemeralDiskConfig, service_info: ServiceInfo, ) -> None: """Validate mounts.""" self.validate_ephemeral_mount( disk_info, azure_ephemeral_disk_config, service_info ) @dataclass class AzureNvmeIdDevice: """Azure NVMe ID device.""" device: str model: Optional[str] nvme_id: str type: Optional[str] index: Optional[int] lun: Optional[int] name: Optional[str] extra: Dict[str, str] nsid: Optional[int] = None @dataclass(repr=True) class AzureNvmeIdInfo: """Azure NVMe ID.""" azure_nvme_id_stdout: str azure_nvme_id_stderr: str azure_nvme_id_returncode: int azure_nvme_id_disks: Dict[str, AzureNvmeIdDevice] azure_nvme_id_json_stdout: str azure_nvme_id_json_stderr: str azure_nvme_id_json_returncode: int azure_nvme_id_json_disks: Dict[str, AzureNvmeIdDevice] azure_nvme_id_help_stdout: str azure_nvme_id_help_stderr: str azure_nvme_id_help_returncode: int azure_nvme_id_version_stdout: str azure_nvme_id_version_stderr: str azure_nvme_id_version_returncode: int azure_nvme_id_version: str azure_nvme_id_zzz_stdout: str azure_nvme_id_zzz_stderr: str azure_nvme_id_zzz_returncode: int def _validate_azure_nvme_disks( self, azure_nvme_id_disks: Dict[str, AzureNvmeIdDevice], disk_info: DiskInfo ) -> None: disk_cfg: Optional[AzureNvmeIdDevice] = None for device_name, disk_cfg in azure_nvme_id_disks.items(): assert f"/dev/{device_name}" == disk_cfg.device assert disk_cfg.device.startswith( "/dev/nvme" ), f"unexpected device: {disk_cfg}" for device_name in disk_info.nvme_local_disks_v2: assert ( device_name in azure_nvme_id_disks ), f"missing azure-nvme-id for {device_name}" disk_cfg = azure_nvme_id_disks.get(device_name) assert disk_cfg, f"failed to find azure-nvme-id for {device_name}" assert disk_cfg.type == "local", f"unexpected local disk type {disk_cfg}" assert disk_cfg.name, f"unexpected local disk name {disk_cfg}" assert disk_cfg.index, f"unexpected local disk index {disk_cfg}" assert disk_cfg.lun is None, f"unexpected local disk lun {disk_cfg}" assert disk_cfg.nvme_id, f"unexpected local disk id {disk_cfg}" assert not disk_cfg.extra, f"unexpected local disk extra {disk_cfg}" for device_name in disk_info.nvme_local_disks_v1: assert ( device_name in azure_nvme_id_disks ), f"missing azure-nvme-id for {device_name}" disk_cfg = azure_nvme_id_disks.get(device_name) assert disk_cfg, f"failed to find azure-nvme-id for {device_name}" assert disk_cfg.type == "local", f"unexpected disk type {disk_cfg}" assert not disk_cfg.name, f"unexpected disk name {disk_cfg}" assert not disk_cfg.index, f"unexpected disk index {disk_cfg}" assert disk_cfg.lun is None, f"unexpected local disk lun {disk_cfg}" assert disk_cfg.nvme_id, f"unexpected disk id {disk_cfg}" assert not disk_cfg.extra, f"unexpected disk extra {disk_cfg}" for device_name in disk_info.nvme_remote_disks: assert ( device_name in azure_nvme_id_disks ), f"missing azure-nvme-id for {device_name}" disk_cfg = azure_nvme_id_disks.get(device_name) assert disk_cfg, f"failed to find azure-nvme-id for {device_name}" nsid = get_nvme_namespace_id(device_name) expected_type = "os" if nsid == 1 else "data" expected_lun = None if nsid == 1 else nsid - 2 assert ( disk_cfg.type == expected_type ), f"unexpected remote disk type for namespace ID {nsid}: {disk_cfg}" assert ( disk_cfg.lun == expected_lun ), f"unexpected remote disk lun for namespace ID {nsid}: {disk_cfg}" assert not disk_cfg.name, f"unexpected remote disk name {disk_cfg}" assert disk_cfg.nvme_id, f"unexpected remote disk id {disk_cfg}" assert not disk_cfg.extra, f"unexpected remote disk extra {disk_cfg}" logger.info("validate_azure_nvme_disks OK") def validate_azure_nvme_id(self, disk_info: DiskInfo) -> None: """Validate azure-nvme-id outputs.""" assert self.azure_nvme_id_returncode == 0, "azure-nvme-id failed" if not os.path.exists("/sys/class/nvme"): assert ( self.azure_nvme_id_stderr == "no NVMe devices in /sys/class/nvme: No such file or directory\n" ), f"unexpected azure-nvme-id stderr without /sys/class/nvme: {self.azure_nvme_id_stderr}" else: assert ( self.azure_nvme_id_stderr == "" ), f"unexpected azure-nvme-id stderr: {self.azure_nvme_id_stderr}" self._validate_azure_nvme_disks(self.azure_nvme_id_disks, disk_info) logger.info("validate_azure_nvme_id OK") def validate_azure_nvme_id_help(self) -> None: """Validate azure-nvme-id --help outputs.""" assert self.azure_nvme_id_help_returncode == 0, "azure-nvme-id --help failed" assert ( self.azure_nvme_id_help_stderr == "" ), f"unexpected azure-nvme-id --help stderr: {self.azure_nvme_id_help_stderr!r}" assert ( self.azure_nvme_id_help_stdout and self.azure_nvme_id_help_stdout.startswith("Usage: azure-nvme-id ") ), "unexpected azure-nvme-id --help stdout: {self.azure_nvme_id_help_stdout!r}" logger.info("validate_azure_nvme_id_help OK") def validate_azure_nvme_id_json(self, disk_info: DiskInfo) -> None: """Validate azure-nvme-id --format json outputs.""" assert self.azure_nvme_id_json_returncode == 0, "azure-nvme-id failed" if not os.path.exists("/sys/class/nvme"): assert ( self.azure_nvme_id_json_stderr == "no NVMe devices in /sys/class/nvme: No such file or directory\n" ), f"unexpected azure-nvme-id stderr without /sys/class/nvme: {self.azure_nvme_id_json_stderr}" else: assert ( self.azure_nvme_id_json_stderr == "" ), f"unexpected azure-nvme-id stderr: {self.azure_nvme_id_json_stderr}" self._validate_azure_nvme_disks(self.azure_nvme_id_json_disks, disk_info) ignore_nsid_for_compat = ( is_azure_vm_utils_compat((0, 7, 0)) and self.azure_nvme_id_json_disks and not any( "nsid" in device for device in json.loads(self.azure_nvme_id_json_stdout) ) ) if ignore_nsid_for_compat: logger.info( "azure-nvme-id %s omits JSON namespace IDs; " "allowing legacy output while retaining disk identity and LUN checks", self.azure_nvme_id_version, ) else: for device_name, disk in self.azure_nvme_id_json_disks.items(): expected_nsid = get_nvme_namespace_id(device_name) assert disk.nsid == expected_nsid, ( f"unexpected namespace ID for {device_name}: " f"{disk.nsid!r} != {expected_nsid}" ) assert all( disk.model in ( "MSFT NVMe Accelerator v1.0", "Microsoft NVMe Direct Disk", "Microsoft NVMe Direct Disk v2", ) for disk in self.azure_nvme_id_json_disks.values() ), "missing model in azure-nvme-id --format json" logger.info("validate_azure_nvme_id_json OK") def validate_azure_nvme_id_version(self) -> None: """Validate azure-nvme-id --version outputs.""" assert ( self.azure_nvme_id_version_returncode == 0 ), "azure-nvme-id --version failed" assert ( self.azure_nvme_id_version_stderr == "" ), f"unexpected azure-nvme-id stderr: {self.azure_nvme_id_stderr}" assert self.azure_nvme_id_version_stdout, "missing azure-nvme-id version stdout" assert re.match( r"azure-nvme-id [0v]\.*", self.azure_nvme_id_version_stdout.strip() ), f"unexpected azure-nvme-id version stdout: {self.azure_nvme_id_version_stdout}" assert re.match( r"[0v]\.*", self.azure_nvme_id_version ), f"unexpected azure-nvme-id version: {self.azure_nvme_id_version}" logger.info("validate_azure_nvme_id_version OK: %s", self.azure_nvme_id_version) def validate_azure_nvme_id_zzz_invalid_arg(self) -> None: """Validate azure-nvme-id handles invalid arguments.""" assert ( self.azure_nvme_id_zzz_returncode == 1 ), f"azure-nvme-id zzz rc={self.azure_nvme_id_zzz_returncode}" assert ( self.azure_nvme_id_zzz_stderr == "invalid argument: zzz\n" ), f"unexpected azure-nvme-id zzz stderr: {self.azure_nvme_id_zzz_stderr!r}" assert ( self.azure_nvme_id_zzz_stdout and self.azure_nvme_id_zzz_stdout.startswith("Usage: azure-nvme-id ") ), (f"unexpected azure-nvme-id zzz stdout: {self.azure_nvme_id_zzz_stdout!r}") logger.info("validate_azure_nvme_id_invalid_arg OK") def validate(self, disk_info: DiskInfo) -> None: """Validate Azure NVMe ID output.""" self.validate_azure_nvme_id_help() self.validate_azure_nvme_id_version() self.validate_azure_nvme_id_zzz_invalid_arg() self.validate_azure_nvme_id(disk_info) self.validate_azure_nvme_id_json(disk_info) @classmethod def gather(cls) -> "AzureNvmeIdInfo": """Gather Azure NVMe ID information.""" global AZURE_VM_UTILS_VERSION, AZURE_VM_UTILS_VERSION_TUPLE # pylint: disable=global-statement # Start with version check to populate global version information. proc = subprocess.run( ["azure-nvme-id", "--version"], capture_output=True, check=False ) azure_nvme_id_version_stdout = proc.stdout.decode("utf-8") azure_nvme_id_version_stderr = proc.stderr.decode("utf-8") azure_nvme_id_version_returncode = proc.returncode AZURE_VM_UTILS_VERSION, AZURE_VM_UTILS_VERSION_TUPLE = ( cls.parse_azure_nvme_id_version(azure_nvme_id_version_stdout) ) proc = subprocess.run(["azure-nvme-id"], capture_output=True, check=False) azure_nvme_id_stdout = proc.stdout.decode("utf-8") azure_nvme_id_stderr = proc.stderr.decode("utf-8") azure_nvme_id_returncode = proc.returncode azure_nvme_id_disks = cls.parse_azure_nvme_id_output(azure_nvme_id_stdout) proc = subprocess.run( ["azure-nvme-id", "--format", "json"], capture_output=True, check=False ) azure_nvme_id_json_stdout = proc.stdout.decode("utf-8") azure_nvme_id_json_stderr = proc.stderr.decode("utf-8") azure_nvme_id_json_returncode = proc.returncode azure_nvme_id_json_disks = cls.parse_azure_nvme_id_json_output( azure_nvme_id_json_stdout ) proc = subprocess.run( ["azure-nvme-id", "--help"], capture_output=True, check=False ) azure_nvme_id_help_stdout = proc.stdout.decode("utf-8") azure_nvme_id_help_stderr = proc.stderr.decode("utf-8") azure_nvme_id_help_returncode = proc.returncode proc = subprocess.run( ["azure-nvme-id", "zzz"], capture_output=True, check=False ) azure_nvme_id_zzz_stdout = proc.stdout.decode("utf-8") azure_nvme_id_zzz_stderr = proc.stderr.decode("utf-8") azure_nvme_id_zzz_returncode = proc.returncode azure_nvme_id_info = cls( azure_nvme_id_stdout=azure_nvme_id_stdout, azure_nvme_id_stderr=azure_nvme_id_stderr, azure_nvme_id_returncode=azure_nvme_id_returncode, azure_nvme_id_help_stdout=azure_nvme_id_help_stdout, azure_nvme_id_help_stderr=azure_nvme_id_help_stderr, azure_nvme_id_help_returncode=azure_nvme_id_help_returncode, azure_nvme_id_disks=azure_nvme_id_disks, azure_nvme_id_json_stdout=azure_nvme_id_json_stdout, azure_nvme_id_json_stderr=azure_nvme_id_json_stderr, azure_nvme_id_json_returncode=azure_nvme_id_json_returncode, azure_nvme_id_json_disks=azure_nvme_id_json_disks, azure_nvme_id_version_stdout=azure_nvme_id_version_stdout, azure_nvme_id_version_stderr=azure_nvme_id_version_stderr, azure_nvme_id_version_returncode=azure_nvme_id_version_returncode, azure_nvme_id_version=AZURE_VM_UTILS_VERSION, azure_nvme_id_zzz_returncode=azure_nvme_id_zzz_returncode, azure_nvme_id_zzz_stdout=azure_nvme_id_zzz_stdout, azure_nvme_id_zzz_stderr=azure_nvme_id_zzz_stderr, ) return azure_nvme_id_info @staticmethod def parse_azure_nvme_id_json_output(output: str) -> Dict[str, AzureNvmeIdDevice]: """Parse azure-nvme-id --format json output. Example output: [ { "path": "/dev/nvme0n33", "nsid": 33, "model": "MSFT NVMe Accelerator v1.0", "properties": { "type": "data", "lun": 31 }, "vs": "" }, { "path": "/dev/nvme1n1", "nsid": 1, "model": "Microsoft NVMe Direct Disk v2", "properties": { "type": "local", "index": 1, "name": "nvme-440G-1" }, "vs": "type=local,index=1,name=nvme-440G-1" } ] """ devices = {} for device in json.loads(output): device_path = device["path"] model = device["model"] properties = device["properties"] nvme_id = ",".join(f"{k}={v}" for k, v in properties.items()) device_type = properties.pop("type", None) device_index = ( int(properties.pop("index")) if "index" in properties else None ) device_lun = int(properties.pop("lun")) if "lun" in properties else None device_name = properties.pop("name", None) azure_nvme_id_device = AzureNvmeIdDevice( device=device_path, model=model, nsid=device.get("nsid"), nvme_id=nvme_id, type=device_type, index=device_index, lun=device_lun, name=device_name, extra=properties, ) key = device_path.split("/")[-1] devices[key] = azure_nvme_id_device return devices @staticmethod def parse_azure_nvme_id_output(output: str) -> Dict[str, AzureNvmeIdDevice]: """Parse azure-nvme-id output. Example output: /dev/nvme0n1: type=os /dev/nvme0n2: type=data,lun=0 /dev/nvme0n3: type=data,lun=1 /dev/nvme1n1: type=local,index=1,name=nvme-440G-1 /dev/nvme2n1: type=local,index=2,name=nvme-440G-2 /dev/nvme3n1: """ devices = {} for line in output.splitlines(): parts = line.strip().split(":", 1) if parts[-1] == "": parts.pop() device = parts[0].strip() if len(parts) == 2: nvme_id = parts[1].strip() properties = dict(kv.split("=", 1) for kv in nvme_id.split(",")) elif len(parts) == 1: nvme_id = "" properties = {} else: raise ValueError(f"unexpected azure-nvme-id output: {line}") device_type = properties.pop("type", None) device_index = ( int(properties.pop("index")) if "index" in properties else None ) device_lun = int(properties.pop("lun")) if "lun" in properties else None device_name = properties.pop("name", None) azure_nvme_id_device = AzureNvmeIdDevice( device=device, model=None, nvme_id=nvme_id, type=device_type, index=device_index, lun=device_lun, name=device_name, extra=properties, ) key = device.split("/")[-1] devices[key] = azure_nvme_id_device return devices @staticmethod def parse_azure_nvme_id_version( azure_nvme_id_version_output: str, ) -> Tuple[str, Optional[Tuple[int, int, int]]]: """Parse the full version and numeric release, including distro revisions.""" parts = azure_nvme_id_version_output.split() assert ( len(parts) == 2 ), f"unexpected azure-nvme-id version output: {azure_nvme_id_version_output}" version = parts[1] match = re.match(r"v?(\d+)\.(\d+)\.(\d+)", version) if match is None: return version, None major, minor, patch = match.groups() return version, (int(major), int(minor), int(patch)) @dataclass class NetworkInterface: """Network interface.""" name: str driver: str mac: str ipv4_addrs: List[str] udev_properties: Dict[str, str] @dataclass(eq=True, repr=True) class NetworkInfo: """Network information.""" interfaces: Dict[str, NetworkInterface] = field(default_factory=dict) @classmethod def enumerate_interfaces(cls) -> Dict[str, NetworkInterface]: """Retrieve all Ethernet interfaces on the system.""" interfaces: Dict[str, NetworkInterface] = {} interface_names = [ interface for interface in os.listdir(SYS_CLASS_NET) if os.path.exists(os.path.join(SYS_CLASS_NET, interface, "device")) ] for interface_name in interface_names: sys_path = Path(SYS_CLASS_NET, interface_name) udev_properties = cls.query_udev_properties(interface_name) driver_path = Path(sys_path, "device", "driver") if not driver_path.is_symlink(): logger.debug( "ignoring interface %s without driver symlink", interface_name ) continue link = os.readlink(driver_path) driver = os.path.basename(link) mac = (sys_path / "address").read_text().strip() ipv4_addrs = cls.get_ipv4_addresses(interface_name) interfaces[interface_name] = NetworkInterface( name=interface_name, driver=driver, mac=mac, ipv4_addrs=ipv4_addrs, udev_properties=udev_properties, ) return interfaces @staticmethod def get_ipv4_addresses(interface_name: str) -> List[str]: """Get the IPv4 addresses of a given network interface using `ip addr`.""" try: result = subprocess.run( ["ip", "-4", "addr", "show", interface_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True, ) ipv4_addresses = re.findall(r"inet (\d+\.\d+\.\d+\.\d+)", result.stdout) return ipv4_addresses except subprocess.CalledProcessError as error: logger.error("failed to get IPv4 address for %s: %r", interface_name, error) raise @staticmethod def query_udev_properties(interface_name: str) -> Dict[str, str]: """Query all udev properties for a given interface using udevadm.""" try: result = subprocess.run( [ "udevadm", "info", "--query=property", f"--path={SYS_CLASS_NET}/{interface_name}", ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True, ) properties: Dict[str, str] = {} for line in result.stdout.splitlines(): if "=" in line: key, value = line.split("=", 1) properties[key] = value return properties except subprocess.CalledProcessError as error: logger.error( "Failed to query udev properties for %s: %r", interface_name, error ) return {} def _validate_interface( self, interface: NetworkInterface, service_info: ServiceInfo ) -> None: """Ensure the required properties are set for hv_netvsc, mlx4, mlx5, and mana devices.""" if interface.driver in ["mlx4_core", "mlx5_core", "mana"]: assert ( interface.udev_properties.get("NM_UNMANAGED") == "1" and interface.udev_properties.get("AZURE_UNMANAGED_SRIOV") == "1" and interface.udev_properties.get("ID_NET_MANAGED_BY") == "unmanaged" ), f"missing required properties for network interface: {interface}" elif interface.driver == "hv_netvsc": assert ( "AZURE_UNMANAGED_SRIOV" not in interface.udev_properties ), f"unexpected AZURE_UNMANAGED_SRIOV property: {interface}" assert ( interface.udev_properties.get("ID_NET_MANAGED_BY") != "unmanaged" ), f"hv_netvsc interface should be managed: {interface}" mana_has_synthetic_netvsc = interface.driver == "mana" and any( i.driver == "hv_netvsc" and i.mac == interface.mac for i in self.interfaces.values() ) if interface.driver == "hv_netvsc" or ( interface.driver == "mana" and not mana_has_synthetic_netvsc ): assert interface.ipv4_addrs, f"missing IPv4 addresses for {interface}" else: # Ensure accelerated network VFs do not have an IP assigned. There is a # systemd bug which will cause this check to fail, only log the failure # if unpatched to avoid unnecessary assertion failures. if service_info.systemd_version_fixed_unmanaged_bug: assert ( not interface.ipv4_addrs ), f"unexpected IPv4 addresses for {interface}" elif interface.ipv4_addrs: logger.error( "unexpected IPv4 addresses for %s: %r", interface.name, interface.ipv4_addrs, ) logger.info( "validate_interface %s %s OK: %r", interface.name, interface.driver, interface.ipv4_addrs, ) def validate(self, service_info: ServiceInfo) -> None: """Validate network configuration.""" for _, interface in self.interfaces.items(): self._validate_interface(interface, service_info) @classmethod def gather(cls) -> "NetworkInfo": """Gather networking information.""" return NetworkInfo(interfaces=cls.enumerate_interfaces()) class AzureVmUtilsValidator: """Validate Azure VM utilities.""" def __init__( self, *, skip_imds_validation: bool = False, skip_udev_validation: bool = False, ) -> None: self.azure_nvme_id_info = AzureNvmeIdInfo.gather() self.azure_ephemeral_disk_config = AzureEphemeralDiskConfig.gather() self.disk_info = DiskInfo.gather() self.mount_info = MountInfo.gather() self.net_info = NetworkInfo.gather() self.service_info = ServiceInfo.gather() self.skip_imds_validation = skip_imds_validation self.skip_udev_validation = skip_udev_validation try: self.imds_metadata = get_imds_metadata() except Exception as error: logger.error("failed to fetch IMDS metadata: %r", error) if not self.skip_imds_validation: raise self.imds_metadata = {} self.vm_size = self.imds_metadata.get("compute", {}).get("vmSize") self.sku_config = SKU_CONFIGS.get(self.vm_size) logger.info("sku config: %r", self.sku_config) logger.info("azure_ephemeral_disk_config: %r", self.azure_ephemeral_disk_config) logger.info("azure_nvme_id_info: %r", self.azure_nvme_id_info) logger.info("disk_info: %r", self.disk_info) logger.info("mount_info: %r", self.mount_info) logger.info("net_info: %r", self.net_info) logger.info("service_info: %r", self.service_info) def validate_dev_disk_azure_links_data(self) -> None: """Validate /dev/disk/azure/data links. All data disks should have by-lun if azure-vm-utils is installed. Future variants of remote disks will include by-name. """ imds_data_disks = ( self.imds_metadata.get("compute", {}) .get("storageProfile", {}) .get("dataDisks", []) ) expected_data_disks = len(imds_data_disks) data_disks = [ link for link in self.disk_info.dev_disk_azure_links if link.startswith("/dev/disk/azure/data/by-lun") ] if self.disk_info.nvme_remote_disks: assert len(data_disks) == len( self.disk_info.nvme_remote_data_disks ), f"unexpected number of data disks: {data_disks} configured={self.disk_info.nvme_remote_data_disks}" if expected_data_disks == 0 and len(data_disks) > 0: # Treat this as a soft error because IMDS metadata may be missing details # that we would need to retry on for some unknown amount of time. logger.error( "IMDS reports no data disks but /dev/disk/azure/data/by-lun links found: %r" " (assuming IMDS metadata is invalid)", data_disks, ) else: assert ( len(data_disks) == expected_data_disks ), f"unexpected number of data disks: {data_disks} IMDS configured={imds_data_disks} (note that IMDS may not be accurate)" # Verify disk sizes match up with IMDS configuration. for imds_disk in imds_data_disks: lun = imds_disk.get("lun") # Disk size is actually reported in GiB not GB. expected_size_gib = int(imds_disk.get("diskSizeGB")) disk_path = f"/dev/disk/azure/data/by-lun/{lun}" actual_size_gib = get_disk_size_gib(disk_path) assert ( actual_size_gib == expected_size_gib ), f"disk size mismatch for {disk_path}: expected {expected_size_gib} GiB, found {actual_size_gib} GiB" logger.info("validate_dev_disk_azure_links_data OK: %r", data_disks) def validate_dev_disk_azure_links_local(self) -> None: """Validate /dev/disk/azure/local links. All local disks should have by-serial if azure-vm-utils is installed. If NVMe id is supported, by-index and by-name will be available as well. """ local_disks = sorted( [ link for link in self.disk_info.dev_disk_azure_links if link.startswith("/dev/disk/azure/local") ] ) for key in ["index", "name", "serial"]: local_disks_by_key = sorted( [ link for link in self.disk_info.dev_disk_azure_links if link.startswith(f"/dev/disk/azure/local/by-{key}") ] ) if key == "serial": expected_count = len(self.disk_info.nvme_local_disks) else: expected_count = len(self.disk_info.nvme_local_disks_v2) assert ( len(local_disks_by_key) == expected_count ), f"unexpected number of local disks by-{key}: {local_disks_by_key} (expected {expected_count})" assert ( not self.sku_config or not self.sku_config.nvme_id_enabled_local or len(local_disks_by_key) == self.sku_config.nvme_local_disk_count ), f"unexpected number of local disks by sku for by-{key}: {local_disks_by_key} (expected {expected_count})" if key == "name": for disk in local_disks_by_key: name = disk.split("/")[-1] assert name.startswith( "nvme-" ), f"unexpected local disk name: {name}" match = re.match(r"nvme-(\d+)G-(\d+)", name) assert ( match ), f"local disk name does not conform to expected pattern: {name}" size, index = match.groups() assert ( size.isdigit() and index.isdigit() ), f"invalid size or index in local disk name: {name}" # Cross-check by-index links with by-name links. by_index_path = f"/dev/disk/azure/local/by-index/{index}" assert os.path.realpath(by_index_path) == os.path.realpath( disk ), f"mismatch between by-index and by-name links: {by_index_path} != {disk}" logger.info("validate_dev_disk_azure_links_local OK: %r", local_disks) def validate_dev_disk_azure_links_os(self) -> None: """Validate /dev/disk/azure/os link.""" os_disk = "/dev/disk/azure/os" assert os_disk in self.disk_info.dev_disk_azure_links, f"missing {os_disk}" logger.info("validate_dev_disk_azure_links_os OK: %r", os_disk) def validate_dev_disk_azure_links_resource(self) -> None: """Validate /dev/disk/azure/resource link.""" resource_disk = DEV_DISK_AZURE_RESOURCE expected = (self.sku_config and self.sku_config.temp_disk_size_gib) or bool( self.disk_info.scsi_resource_disk ) if expected: assert ( resource_disk in self.disk_info.dev_disk_azure_links ), f"missing {resource_disk}" else: assert ( resource_disk not in self.disk_info.dev_disk_azure_links ), f"unexpected {resource_disk}" logger.info("validate_dev_disk_azure_links_resource OK: %r", resource_disk) def validate_mounts(self) -> None: """Ensure SCSI resource disk and/or NVMe local disks are mounted correctly.""" self.mount_info.validate( self.disk_info, self.azure_ephemeral_disk_config, self.service_info ) def validate_networking(self) -> None: """Validate networking configuration.""" self.net_info.validate(self.service_info) logger.info("validate_networking OK") def validate_nvme_io_timeouts(self) -> None: """Validate NVMe queue I/O timeouts.""" for namespace in self.disk_info.nvme_remote_disks: assert ( namespace in self.disk_info.nvme_io_timeouts ), f"missing NVMe I/O timeout for {namespace}" timeout = self.disk_info.nvme_io_timeouts[namespace] assert ( timeout == 240000 ), f"unexpected NVMe I/O timeout for {namespace}: {timeout} (expected 240000 ms)" logger.info("validate_nvme_io_timeouts OK: %r", self.disk_info.nvme_io_timeouts) def validate_nvme_local_disks(self) -> None: """Validate NVMe local disks.""" logger.info("validate_nvme_local_disks OK: %r", self.disk_info.nvme_local_disks) def validate_scsi_resource_disk(self) -> None: """Validate SCSI resource disk symlink and size.""" assert ( self.disk_info.scsi_resource_disk_size_gib == self.disk_info.dev_disk_azure_resource_disk_size_gib ), f"resource disk size mismatch: {self.disk_info}" if self.disk_info.scsi_resource_disk: assert ( f"/dev/{self.disk_info.scsi_resource_disk}" == self.disk_info.dev_disk_azure_resource_disk ), f"unexpected resource disk path: {self.disk_info}" else: assert ( self.disk_info.scsi_resource_disk is None and self.disk_info.dev_disk_azure_resource_disk is None ), f"unexpected resource disk path: {self.disk_info}" logger.info( "validate_scsi_resource_disk OK: /dev/disk/azure/resource => %s", self.disk_info.dev_disk_azure_resource_disk, ) def validate_services(self) -> None: """Validate services.""" logger.info("validate_services OK") def validate_sku_config(self) -> None: """Validate SKU config.""" if not self.sku_config: logger.warning( "validate_sku_config SKIPPED: no sku configuration for VM size %r", self.vm_size, ) return assert ( self.sku_config.vm_size == self.vm_size ), f"vm size mismatch: {self.sku_config.vm_size} != {self.vm_size}" assert ( len(self.disk_info.nvme_local_disks) == self.sku_config.nvme_local_disk_count ), f"local disk count mismatch: {len(self.disk_info.nvme_local_disks)} != {self.sku_config.nvme_local_disk_count}" assert ( self.disk_info.nvme_local_disk_size_gib == self.sku_config.nvme_local_disk_size_gib ), f"local disk size mismatch: {self.disk_info.nvme_local_disk_size_gib} != {self.sku_config.nvme_local_disk_size_gib}" assert ( self.disk_info.scsi_resource_disk_size_gib == self.sku_config.temp_disk_size_gib ), f"temp disk size mismatch: {self.disk_info.scsi_resource_disk_size_gib} != {self.sku_config.temp_disk_size_gib}" assert ( self.disk_info.dev_disk_azure_resource_disk_size_gib == self.sku_config.temp_disk_size_gib ), f"temp disk size mismatch: {self.disk_info.dev_disk_azure_resource_disk_size_gib} != {self.sku_config.temp_disk_size_gib}" logger.info("validate_sku_config OK") def validate(self) -> None: """Run validations.""" logger.info("Starting validation...") self.azure_nvme_id_info.validate(self.disk_info) if self.skip_udev_validation: logger.info("validate_dev_disk_azure_links_data SKIPPED") logger.info("validate_dev_disk_azure_links_local SKIPPED") logger.info("validate_dev_disk_azure_links_os SKIPPED") logger.info("validate_dev_disk_azure_links_resource SKIPPED") logger.info("validate_mounts SKIPPED") logger.info("validate_scsi_resource_disk SKIPPED") logger.info("validate_services SKIPPED") logger.info("validate_networking SKIPPED") logger.info("validate_nvme_io_timeouts SKIPPED") else: self.validate_dev_disk_azure_links_data() self.validate_dev_disk_azure_links_local() self.validate_dev_disk_azure_links_os() self.validate_dev_disk_azure_links_resource() self.validate_mounts() self.validate_networking() self.validate_nvme_io_timeouts() self.validate_services() self.validate_scsi_resource_disk() self.validate_sku_config() logger.info("success!") def main() -> None: """Main entry point.""" global COMPAT_CHECKS_ENABLED # pylint: disable=global-statement parser = argparse.ArgumentParser( description="Azure VM utilities self-tests script." ) parser.add_argument( "--debug", action="store_true", help="Enable debug logging", ) parser.add_argument( "--skip-imds-validation", action="store_true", help="Skip imds validation (allow for running tests outside Azure VM)", ) parser.add_argument( "--skip-udev-validation", action="store_true", help="Skip udev validation (allow for running test without reboot after install)", ) parser.add_argument( "--compat", action="store_true", help="Allow known version-specific compatibility behavior", ) args = parser.parse_args() COMPAT_CHECKS_ENABLED = args.compat if args.debug: logging.basicConfig(format="[%(asctime)s] %(message)s", level=logging.DEBUG) else: logging.basicConfig(format="[%(asctime)s] %(message)s", level=logging.INFO) validator = AzureVmUtilsValidator( skip_imds_validation=args.skip_imds_validation, skip_udev_validation=args.skip_udev_validation, ) validator.validate() if __name__ == "__main__": main() azure-vm-utils-0.8.0/selftest/test-requirements-frozen.txt000066400000000000000000000011261527236310300240200ustar00rootroot00000000000000ast_serialize==0.13.1 astroid==4.3.4 autoflake==2.4.0 black==26.10.1 certifi==2026.10.23 charset-normalizer==3.5.2 click==8.5.0 dill==0.4.1 exceptiongroup==1.3.1 execnet==2.1.2 flake8==7.4.1 idna==3.20 iniconfig==2.3.1 isort==9.0.2 librt==0.16.0 mccabe==0.7.0 mypy==2.4.1 mypy_extensions==1.1.0 packaging==26.3 pathspec==1.1.1 platformdirs==4.12.4 pluggy==1.7.0 pycodestyle==2.15.0 pyflakes==4.0.3 Pygments==2.21.0 pylint==4.1.3 pytest==9.2.0 pytest-xdist==3.8.0 pytokens==0.4.1 requests==2.34.2 tomli==2.5.0 tomlkit==0.15.2 types-requests==2.33.0.20261030 typing_extensions==4.16.0 urllib3==2.8.0 azure-vm-utils-0.8.0/selftest/test-requirements.txt000066400000000000000000000001741527236310300225210ustar00rootroot00000000000000autoflake black flake8 isort mypy pycodestyle pyflakes pylint pytest pytest-xdist requests types-requests typing_extensions azure-vm-utils-0.8.0/selftest/test_images.py000066400000000000000000002206331527236310300211420ustar00rootroot00000000000000#!/usr/bin/env python3 # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See LICENSE in the project root for license information. # -------------------------------------------------------------------------- """Azure VM utilities self-tests script. Environment variables supported by this script: Environment variable Description Default ---------------------------- ---------------------- ------------------------- SELFTEST_ARM64_IMAGES Comma-separated images ARM64 catalog SELFTEST_ARM64_VM_SIZES Comma-separated sizes ARM64 SKU catalog SELFTEST_GEN1_IMAGES Comma-separated images Gen1 catalog SELFTEST_GEN1_VM_SIZES Comma-separated sizes x64 SCSI SKU catalog SELFTEST_GEN2_IMAGES Comma-separated images Gen2 catalog SELFTEST_GEN2_VM_SIZES Comma-separated sizes x64 SKU catalog SELFTEST_VM_CONFIGS Comma-separated names All configurations SELFTEST_FULL_MATRIX All combinations 0 0: representative union 1: compatible product SELFTEST_AZURE_SUBSCRIPTION Subscription ID Required SELFTEST_AZURE_LOCATION Azure region Required SELFTEST_ADMIN_USERNAME VM admin username azureuser SELFTEST_ADMIN_PASSWORD VM admin password Unset SELFTEST_ARTIFACTS_PATH Artifact directory /tmp/selftest- SELFTEST_RESOURCE_GROUP Resource group name temp-rg--selftest- <8-hex random suffix> SELFTEST_HOLD_FAILURES Retain failed VMs 0 0: clean up failures 1: retain for debugging SELFTEST_INSTALL_FROM_SOURCE Working-tree build 0 0: image utilities 1: install and reboot SELFTEST_PUBLIC_IP_TAGS Azure CLI --ip-tags Unset SELFTEST_REALLOCATES Deallocate/start count 1 SELFTEST_REBOOTS Reboots per allocation 1 Boolean options accept 1/0, true/false, yes/no, y/n, on/off, and t/f, case-insensitively. The selected images, VM sizes, and configurations define the compatible matrix. Coverage uses '*' representatives below to reduce combinations by default. Category Images VM sizes VM configurations --------------- -------------- ----------------------- ----------------- image-coverage All Representative Representative sku-coverage Representative All Representative config-coverage Representative Compatible reps All full-matrix All All All Rep Image type Image --- ---------- ----------------------------------------------- ARM64 Fedora Cloud Rawhide Arm64 (community gallery) * ARM64 canonical:ubuntu-24_04-lts:server-arm64:latest ARM64 canonical:ubuntu-26_04-lts:server-arm64:latest ARM64 debian:debian-13-daily:13-arm64:latest ARM64 debian:debian-14-daily:14-arm64:latest ARM64 debian:debian-sid-daily:sid-arm64:latest ARM64 microsoftazurelinux:azurelinux-4:4-arm64:latest ARM64 redhat:rhel-arm64:10_2-arm64:latest ARM64 suse:sles-15-sp7-arm64:gen2:latest ARM64 suse:sles-16-0-arm64:gen2:latest Gen1 x64 canonical:ubuntu-24_04-lts:server-gen1:latest Gen1 x64 canonical:ubuntu-26_04-lts:server-gen1:latest Gen1 x64 debian:debian-13-daily:13:latest Gen1 x64 debian:debian-14-daily:14:latest Gen1 x64 debian:debian-sid-daily:sid:latest Gen1 x64 redhat:rhel:10_2:latest * Gen1 x64 suse:sles-15-sp7:gen1:latest Gen1 x64 suse:sles-16-0-x86-64:gen1:latest Gen2 x64 Fedora Cloud Rawhide x64 (community gallery) Gen2 x64 canonical:ubuntu-24_04-lts:server:latest Gen2 x64 canonical:ubuntu-26_04-lts:server:latest Gen2 x64 debian:debian-13-daily:13-gen2:latest Gen2 x64 debian:debian-14-daily:14-gen2:latest Gen2 x64 debian:debian-sid-daily:sid-gen2:latest Gen2 x64 microsoftazurelinux:azurelinux-4:4:latest * Gen2 x64 redhat:rhel:10_2-gen2:latest Gen2 x64 suse:sles-15-sp7:gen2:latest Gen2 x64 suse:sles-16-0-x86-64:gen2:latest Rep VM size Arch OS/data Temp GiB Local NVMe Direct Disk --- ------------------- ----- ------- -------- ---------- ----------- * Standard_D2plds_v5 ARM64 SCSI 75 None None Standard_D8plds_v5 ARM64 SCSI 300 None None * Standard_D2plds_v6 ARM64 SCSI None 1 x 110 v2 Standard_D16plds_v6 ARM64 SCSI None 2 x 440 v2 Standard_D2pls_v5 ARM64 SCSI None None None Standard_D8pls_v5 ARM64 SCSI None None None Standard_D2pls_v6 ARM64 SCSI None None None Standard_D16pls_v6 ARM64 SCSI None None None Standard_B2ts_v2 x64 SCSI None None None Standard_D2ads_v5 x64 SCSI 75 None None Standard_D16ads_v5 x64 SCSI 600 None None Standard_D2ads_v6 x64 NVMe None 1 x 110 v2 Standard_D16ads_v6 x64 NVMe None 2 x 440 v2 Standard_D32ads_v6 x64 NVMe None 4 x 440 v2 Standard_D32ads_v7 x64 NVMe None 4 x 440 v2 Standard_D2as_v6 x64 NVMe None None None * Standard_D2ds_v4 x64 SCSI 75 None None Standard_D2ds_v5 x64 SCSI 75 None None * Standard_D2ds_v6 x64 NVMe None 1 x 110 v2 Standard_D16ds_v6 x64 NVMe None 2 x 440 v2 Standard_D32ds_v6 x64 NVMe None 4 x 440 v2 Standard_D2ds_v7 x64 NVMe None 1 x 110 v2 Standard_D32ds_v7 x64 NVMe None 4 x 440 v2 Standard_D2s_v4 x64 SCSI None None None Standard_D2s_v5 x64 SCSI None None None Standard_D2s_v6 x64 NVMe None None None Standard_E2bds_v5 x64 SCSI 75 None None Standard_E2bs_v5 x64 SCSI None None None Standard_L8s_v2 x64 SCSI 80 1 x 1788 v1 * Standard_L8s_v3 x64 SCSI 80 1 x 1788 v1 Standard_L80s_v3 x64 SCSI 800 10 x 1788 v1 Standard_L8s_v4 x64 NVMe None 4 x 447 v2 Standard_L80s_v4 x64 NVMe None 10 x 1788 v2 Rep VM configuration Data disk LUNs --- ------------------- --------------- no-data-disks None single-lun 0 * mixed-luns 0, 1, 31, 63 nonzero-luns 1, 2, 31, 63 max-data-disks-32 0-31 (32 disks) max-data-disks-64 0-63 (64 disks) Example usage: export SELFTEST_AZURE_SUBSCRIPTION="" export SELFTEST_AZURE_LOCATION=eastus2 pytest -v selftest/test_images.py -k image-coverage pytest -v selftest/test_images.py -k sku-coverage pytest -v selftest/test_images.py -k config-coverage # Build and test the working tree instead of image-installed utilities. SELFTEST_INSTALL_FROM_SOURCE=1 pytest -v selftest/test_images.py # Custom Gen2 image/size/config selections. SELFTEST_GEN2_IMAGES=/my/image1,/my/image2,... \\ SELFTEST_GEN2_VM_SIZES=Standard_D2ds_v5,Standard_D2ds_v6 \\ SELFTEST_VM_CONFIGS=mixed-luns,single-lun \\ SELFTEST_FULL_MATRIX=1 \\ pytest -v selftest/test_images.py """ import base64 import codecs import fcntl import getpass import hashlib import json import logging import os import re import shlex import subprocess import tarfile import tempfile import time import traceback import uuid from dataclasses import asdict, dataclass, replace from datetime import datetime, timedelta, timezone from enum import Enum from itertools import product from pathlib import Path from typing import Any, Dict, List, Optional, Set, Tuple import pytest from . import selftest logger = logging.getLogger(__name__) # pylint: disable=unknown-option-value # pylint: disable=line-too-long # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-arguments # pylint: disable=too-many-lines # pylint: disable=too-many-locals # pylint: disable=too-many-positional-arguments # pylint: disable=unused-argument # pylint: disable=attribute-defined-outside-init # pylint: disable=redefined-outer-name class ImageType(str, Enum): """Supported image architectures and VM generations.""" ARM64 = "ARM64" GEN1 = "GEN1" GEN2 = "GEN2" SSH_OPTIONS = [ "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "-o", "ServerAliveInterval=10", "-o", "ServerAliveCountMax=3", ] @dataclass(frozen=True) class ImageConfig: """Image identity, observed utilities version, and optional provisioning settings.""" urn: str image_type: ImageType azure_vm_utils_version: Optional[str] = None representative: bool = False # https://github.com/util-linux/util-linux/issues/4527 workaround_util_linux_bug: bool = False IMAGES = [ ImageConfig( "canonical:ubuntu-24_04-lts:server-arm64:latest", ImageType.ARM64, "0.6.0-4~ubuntu0.24.04.1", representative=True, ), ImageConfig( "canonical:ubuntu-26_04-lts:server-arm64:latest", ImageType.ARM64, "0.6.0-4build1", ), ImageConfig( "canonical:ubuntu-24_04-lts:server-gen1:latest", ImageType.GEN1, "0.6.0-4~ubuntu0.24.04.1", ), ImageConfig( "canonical:ubuntu-26_04-lts:server-gen1:latest", ImageType.GEN1, "0.6.0-4build1" ), ImageConfig("canonical:ubuntu-24_04-lts:server:latest", ImageType.GEN2), ImageConfig("canonical:ubuntu-26_04-lts:server:latest", ImageType.GEN2), ImageConfig("debian:debian-13-daily:13-arm64:latest", ImageType.ARM64, "0.6.0-2"), ImageConfig( "debian:debian-14-daily:14-arm64:latest", ImageType.ARM64, "0.6.0-4+b1", workaround_util_linux_bug=True, ), ImageConfig( "debian:debian-sid-daily:sid-arm64:latest", ImageType.ARM64, "0.6.0-4+b1", workaround_util_linux_bug=True, ), ImageConfig("debian:debian-13-daily:13:latest", ImageType.GEN1, "0.6.0-2"), ImageConfig("debian:debian-14-daily:14:latest", ImageType.GEN1, "0.6.0-4"), ImageConfig("debian:debian-sid-daily:sid:latest", ImageType.GEN1, "0.6.0-4"), ImageConfig("debian:debian-13-daily:13-gen2:latest", ImageType.GEN2), ImageConfig("debian:debian-14-daily:14-gen2:latest", ImageType.GEN2), ImageConfig("debian:debian-sid-daily:sid-gen2:latest", ImageType.GEN2), ImageConfig( "/CommunityGalleries/Fedora-5e266ba4-2250-406d-adad-5d73860d958f/Images/Fedora-Cloud-Rawhide-Arm64/versions/latest", ImageType.ARM64, "0.7.0-4.fc45", ), ImageConfig( "/CommunityGalleries/Fedora-5e266ba4-2250-406d-adad-5d73860d958f/Images/Fedora-Cloud-Rawhide-x64/versions/latest", ImageType.GEN2, ), ImageConfig( "microsoftazurelinux:azurelinux-4:4-arm64:latest", ImageType.ARM64, "0.7.0-4.azl4", ), ImageConfig("microsoftazurelinux:azurelinux-4:4:latest", ImageType.GEN2), ImageConfig("redhat:rhel-arm64:10_2-arm64:latest", ImageType.ARM64, "0.7.0-1.el10"), ImageConfig("redhat:rhel:10_2:latest", ImageType.GEN1, "0.7.0-1.el10"), ImageConfig("redhat:rhel:10_2-gen2:latest", ImageType.GEN2, representative=True), ImageConfig( "suse:sles-15-sp7-arm64:gen2:latest", ImageType.ARM64, "0.7.0-150500.11.6.1" ), ImageConfig( "suse:sles-16-0-arm64:gen2:latest", ImageType.ARM64, "0.7.0-160000.1.1" ), ImageConfig( "suse:sles-15-sp7:gen1:latest", ImageType.GEN1, "0.7.0-150500.11.6.1", representative=True, ), ImageConfig( "suse:sles-16-0-x86-64:gen1:latest", ImageType.GEN1, "0.7.0-160000.1.1" ), ImageConfig("suse:sles-15-sp7:gen2:latest", ImageType.GEN2), ImageConfig("suse:sles-16-0-x86-64:gen2:latest", ImageType.GEN2), ] REPRESENTATIVE_VM_SIZES: Dict[ImageType, Tuple[str, str]] = { ImageType.ARM64: ("Standard_D2plds_v5", "Standard_D2plds_v6"), ImageType.GEN1: ("Standard_D2ds_v4", "Standard_L8s_v3"), ImageType.GEN2: ("Standard_D2ds_v4", "Standard_D2ds_v6"), } @dataclass(frozen=True) class VMConfig: """VM settings independent of image and size, with size compatibility limits.""" name: str data_disk_luns: Tuple[int, ...] representative: bool = False vm_sizes: Tuple[str, ...] = () VM_CONFIGS = [ VMConfig("mixed-luns", (0, 1, 31, 63), representative=True), VMConfig("no-data-disks", ()), VMConfig("single-lun", (0,)), VMConfig("nonzero-luns", (1, 2, 31, 63)), VMConfig("max-data-disks-32", tuple(range(32)), vm_sizes=("Standard_L80s_v3",)), VMConfig("max-data-disks-64", tuple(range(64)), vm_sizes=("Standard_L80s_v4",)), ] def _get_overrides(variable: str) -> List[str]: """Read nonempty entries from a comma-separated selection override.""" return [value for value in os.getenv(variable, "").split(",") if value] def _get_bool_env(variable: str) -> bool: """Parse a boolean environment setting, defaulting to false.""" value = os.getenv(variable, "0").lower() if value in ("1", "true", "yes", "y", "on", "t"): return True if value in ("0", "false", "no", "n", "off", "f"): return False raise ValueError(f"{variable} must be 1/0, true/false, yes/no, y/n, on/off, or t/f") def _get_images(image_type: ImageType) -> List[ImageConfig]: """Select a type from the shared catalog, honoring explicit overrides.""" overrides = _get_overrides(f"SELFTEST_{image_type.value}_IMAGES") if overrides: catalog = { image.urn: image for image in IMAGES if image.image_type == image_type } return [ catalog.get(image, ImageConfig(image, image_type)) for image in overrides ] return [image for image in IMAGES if image.image_type == image_type] def _get_vm_sizes(image_type: ImageType) -> List[str]: """Select compatible VM sizes, honoring explicit overrides.""" overrides = _get_overrides(f"SELFTEST_{image_type.value}_VM_SIZES") if overrides: return overrides architecture = "arm64" if image_type == ImageType.ARM64 else "x64" return [ sku.vm_size for sku in selftest.SKU_CONFIGS.values() if sku.vm_size_type == architecture and (image_type != ImageType.GEN1 or not sku.nvme_only) ] def _get_vm_configs() -> List[VMConfig]: """Select configurations without adding unselected layouts.""" overrides = _get_overrides("SELFTEST_VM_CONFIGS") if not overrides: return list(VM_CONFIGS) catalog = {config.name: config for config in VM_CONFIGS} unknown = [name for name in overrides if name not in catalog] if unknown: raise ValueError( f"Unknown SELFTEST_VM_CONFIGS: {', '.join(unknown)}; " f"choose from {', '.join(catalog)}" ) return [catalog[name] for name in overrides] def _get_test_matrix( image_type: ImageType, images: List[ImageConfig], vm_sizes: List[str], vm_configs: List[VMConfig], *, full_matrix: bool = False, ) -> Dict[Tuple[str, ImageConfig, VMConfig], Set[str]]: """Union coverage products by case identity, retaining overlapping labels.""" representative_sizes = set(REPRESENTATIVE_VM_SIZES[image_type]) & set(vm_sizes) if not images or not vm_sizes or not vm_configs: raise ValueError(f"{image_type.value} matrix selections must not be empty") if not full_matrix and ( not any(image.representative for image in images) or not representative_sizes or not any(config.representative for config in vm_configs) ): raise ValueError( f"{image_type.value} reduced matrix requires selected representative " "images, VM sizes, and VM configurations; " "set SELFTEST_FULL_MATRIX=1 for custom selections" ) cases: Dict[Tuple[str, ImageConfig, VMConfig], Set[str]] = {} architecture = "arm64" if image_type == ImageType.ARM64 else "x64" for size, image, config in product(vm_sizes, images, vm_configs): sku = selftest.SKU_CONFIGS.get(size) if sku and ( sku.vm_size_type != architecture or (image_type == ImageType.GEN1 and sku.nvme_only) ): continue if config.vm_sizes and size not in config.vm_sizes: continue coverage: Set[str] = set() if size in representative_sizes and config.representative: coverage.add("image-coverage") if image.representative and config.representative: coverage.add("sku-coverage") if image.representative and ( size in representative_sizes or size in config.vm_sizes ): coverage.add("config-coverage") if full_matrix: coverage.add("full-matrix") if coverage: cases.setdefault((size, image, config), set()).update(coverage) if not cases: raise ValueError( f"No compatible {image_type.value} image/size/configuration combinations selected" ) return cases def _image_case_id( size: str, image: ImageConfig, config: VMConfig, coverage: Set[str] ) -> str: """Expose every coverage membership and disk capability to pytest -k.""" sku = selftest.SKU_CONFIGS.get(size) if sku is None: storage = "unknown-storage" elif sku.nvme_only: storage = "nvme" elif sku.nvme_local_disk_count: storage = "scsi-and-local-nvme" else: storage = "scsi" groups = "-".join(sorted(coverage)) return ( f"{image.image_type.value}-{groups}-{storage}-{size}-{image.urn}" f"-{config.name}" ) def _get_image_test_cases(): """Read selection settings once and attach filterable case IDs.""" full_matrix = _get_bool_env("SELFTEST_FULL_MATRIX") configs = _get_vm_configs() cases = [] for image_type in ImageType: images = _get_images(image_type) sizes = _get_vm_sizes(image_type) compatible_configs = [ config for config in configs if not config.vm_sizes or set(config.vm_sizes) & set(sizes) ] if not compatible_configs: continue for (size, image, config), coverage in _get_test_matrix( image_type, images, sizes, compatible_configs, full_matrix=full_matrix ).items(): cases.append( pytest.param( size, image, config.data_disk_luns, id=_image_case_id(size, image, config, coverage), ) ) if not cases: raise ValueError("No compatible image/size/configuration combinations selected") return cases def safe_parse_bytes_literal(s: str) -> str: """Parse a bytes literal string (e.g. b'foo') safely to a string (foo).""" s = s.strip() if not s.startswith("b'") or not s.endswith("'"): return s # Strip the b' and trailing ' inner = s[2:-1] # Decode escape sequences (e.g., \r, \n, \x00) raw_bytes: bytes = codecs.escape_decode(inner.encode())[0] # type: ignore return raw_bytes.decode("utf-8", errors="ignore").rstrip("\x00") def _subprocess_run( cmd: List[str], *, artifacts_path: Path, artifact_name: str, check: bool = False, decode: bool = False, redact_values: Tuple[str, ...] = (), timeout: Optional[float] = None, ) -> subprocess.CompletedProcess: """Run a subprocess command and capture outputs as utf-8.""" artifact_path = artifacts_path / artifact_name printable_cmd = shlex.join(cmd) for value in redact_values: printable_cmd = printable_cmd.replace(value, "") logger.debug("executing command: %s", printable_cmd) try: proc = subprocess.run( cmd, check=False, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, ) except subprocess.TimeoutExpired as error: stdout = ( error.stdout.decode("utf-8", errors="replace") if isinstance(error.stdout, bytes) else error.stdout or "" ) stderr = ( error.stderr.decode("utf-8", errors="replace") if isinstance(error.stderr, bytes) else error.stderr or "" ) for value in redact_values: stdout = stdout.replace(value, "") stderr = stderr.replace(value, "") artifact_path.write_text( f"cmd: {printable_cmd}\nerror: timed out after {timeout} seconds\n" f"stdout:\n{stdout}\nstderr:\n{stderr}", encoding="utf-8", ) logger.error( "command timed out after %s seconds: %s artifact=%s\nstdout:\n%s\nstderr:\n%s", timeout, printable_cmd, artifact_path.as_posix(), stdout, stderr, ) raise logger.debug( "executed command=%s rc=%d artifact=%s", printable_cmd, proc.returncode, artifact_path.as_posix(), ) for value in redact_values: proc.stdout = proc.stdout.replace(value, "") proc.stderr = proc.stderr.replace(value, "") stdout = proc.stdout stderr = proc.stderr if decode: # Decode potential byte repr strings from get-boot-log. stdout = safe_parse_bytes_literal(stdout) stderr = safe_parse_bytes_literal(stderr) artifact_path.write_text( f"cmd: {printable_cmd}\nrc: {proc.returncode}\nstdout:\n{stdout!s}\nstderr:\n{stderr!s}", encoding="utf-8", ) if proc.returncode != 0: logger.error( "command failed: %s rc=%d artifact=%s\nstdout:\n%s\nstderr:\n%s", printable_cmd, proc.returncode, artifact_path.as_posix(), stdout, stderr, ) if check and proc.returncode != 0: raise RuntimeError( f"Command '{printable_cmd}' failed with return code {proc.returncode}: {artifact_path.as_posix()}\nstdout={stdout!s}\nstderr={stderr!s}" ) return proc @pytest.fixture def artifacts_path(tmp_path, vm_size, image, temp_vm_name): """Generate a temporary SSH key pair for the test.""" artifacts_path = os.getenv("SELFTEST_ARTIFACTS_PATH") if artifacts_path: tmp_path = Path(artifacts_path) path = ( tmp_path / vm_size / image.urn.replace("/", "_").replace(":", "_") / temp_vm_name ) path.mkdir(exist_ok=True, parents=True) yield path @pytest.fixture def ssh_key_path(artifacts_path): """Generate a temporary SSH key pair for the test.""" path = artifacts_path / "id_rsa" _subprocess_run( ["ssh-keygen", "-t", "rsa", "-b", "2048", "-f", str(path), "-N", ""], artifact_name="ssh-keygen", artifacts_path=artifacts_path, check=True, ) yield path @pytest.fixture def azure_subscription(): """Get the Azure subscription ID.""" subscription = os.getenv("SELFTEST_AZURE_SUBSCRIPTION") or "" assert subscription, "SELFTEST_AZURE_SUBSCRIPTION environment variable is required" yield subscription @pytest.fixture def azure_location(): """Get the Azure location.""" location = os.getenv("SELFTEST_AZURE_LOCATION") assert location, "SELFTEST_AZURE_LOCATION environment variable is required" yield location @pytest.fixture def admin_username(): """Get the admin username.""" username = os.getenv("SELFTEST_ADMIN_USERNAME", "azureuser") yield username @pytest.fixture def admin_password(): """Get the admin password.""" yield os.getenv("SELFTEST_ADMIN_PASSWORD") @pytest.fixture def hold_failures(): """Get the hold failures flag.""" yield _get_bool_env("SELFTEST_HOLD_FAILURES") @pytest.fixture def random_suffix(): """Generate a random suffix for the resource group.""" yield str(uuid.uuid4().hex[:8]) @pytest.fixture def reallocates(): """Get the number of reallocates.""" yield int(os.getenv("SELFTEST_REALLOCATES", "1")) @pytest.fixture def reboots(): """Get the number of reboots.""" yield int(os.getenv("SELFTEST_REBOOTS", "1")) @pytest.fixture def temp_vm_name(random_suffix): """Create a temporary resource group for the test.""" user = getpass.getuser() yield f"temp-vm-{user}-selftest-{random_suffix}" def is_cleanup_permitted(request, hold_failures: bool = False) -> bool: """Check to see if resources should be deleted.""" aborted = getattr(request.session, "aborted", False) failed = getattr(request.session, "failed", False) logger.debug( "test session aborted=%s failed=%s hold_failures=%s", aborted, failed, hold_failures, ) if hold_failures and failed: return False return True @pytest.fixture def temp_resource_group( azure_subscription, azure_location, delete_after_tag, hold_failures, random_suffix, request, artifacts_path, ): """Create a temporary resource group for the test.""" env_rg = os.getenv("SELFTEST_RESOURCE_GROUP") if env_rg: resource_group_name = env_rg group_key = hashlib.sha256( f"{azure_subscription}/{resource_group_name}".encode("utf-8") ).hexdigest() lock_path = ( Path(os.environ["SELFTEST_ARTIFACTS_PATH"]) / f"resource-group-{group_key}.lock" ) with lock_path.open("a+", encoding="utf-8") as lock: fcntl.flock(lock, fcntl.LOCK_EX) lock.seek(0) if not lock.read(): _ensure_resource_group( azure_subscription, azure_location, resource_group_name, delete_after_tag, artifacts_path, ) lock.write("ready\n") lock.flush() else: user = getpass.getuser() resource_group_name = f"temp-rg-{user}-selftest-{random_suffix}" _ensure_resource_group( azure_subscription, azure_location, resource_group_name, delete_after_tag, artifacts_path, ) yield resource_group_name if not env_rg and is_cleanup_permitted(request, hold_failures): _subprocess_run( [ "az", "group", "delete", "--name", resource_group_name, "--yes", "--no-wait", ], artifact_name="az_group_delete", artifacts_path=artifacts_path, check=True, ) def _run_network_command( command: List[str], *, artifact_name: str, artifacts_path: Path, check: bool = True, ) -> subprocess.CompletedProcess: """Retry only Azure's transient network control-plane errors.""" for attempt in range(1, 6): proc = _subprocess_run( command, artifact_name=( artifact_name if attempt == 1 else f"{artifact_name}-attempt{attempt}" ), artifacts_path=artifacts_path, ) if proc.returncode == 0: return proc if ( not any( code in proc.stderr for code in ( "RetryableError", "AnotherOperationInProgress", "ConflictingConcurrentWriteNotAllowed", "CanceledAndSupersededDueToAnotherOperation", ) ) or attempt == 5 ): break logger.warning( "Transient Azure network error for %s; retrying in 2 seconds (%s/5)", artifact_name, attempt + 1, ) time.sleep(2) if check: raise RuntimeError( f"Azure network command failed: {shlex.join(command)}\n" f"stdout={proc.stdout}\nstderr={proc.stderr}" ) return proc def _get_network_resource( command: List[str], *, artifact_name: str, artifacts_path: Path ) -> Optional[Dict[str, Any]]: """Distinguish a missing network resource from other inspection failures.""" proc = _run_network_command( [*command, "--output", "json"], artifact_name=artifact_name, artifacts_path=artifacts_path, check=False, ) if proc.returncode == 0: resource = json.loads(proc.stdout) if not isinstance(resource, dict): raise RuntimeError(f"Unexpected network resource response: {proc.stdout}") return resource if any( code in proc.stderr for code in ("ResourceNotFound", "ParentResourceNotFound", "Code: NotFound") ): return None raise RuntimeError(f"Failed to inspect {artifact_name}: {proc.stderr}") def _ensure_resource_group( azure_subscription, azure_location, resource_group_name, delete_after_tag, artifacts_path, ): """Create missing group infrastructure, including in an existing shared group.""" proc = _subprocess_run( [ "az", "group", "show", "--subscription", azure_subscription, "--name", resource_group_name, ], artifact_name="az_group_show", artifacts_path=artifacts_path, ) if proc.returncode != 0 and "ResourceGroupNotFound" not in proc.stderr: raise RuntimeError( f"Failed to inspect resource group {resource_group_name}: stdout={proc.stdout} stderr={proc.stderr}" ) nsg_name = f"{resource_group_name}-nsg" subnet_name = f"{resource_group_name}-subnet" vnet_name = f"{resource_group_name}-vnet" if proc.returncode != 0: _subprocess_run( [ "az", "group", "create", "--subscription", azure_subscription, "--location", azure_location, "--name", resource_group_name, "--tags", f"deleteAfter={delete_after_tag}", ], artifact_name="az_group_create", artifacts_path=artifacts_path, check=True, ) logger.info("created resource group: %s", resource_group_name) group_existed = proc.returncode == 0 network_args = [ "--subscription", azure_subscription, "--resource-group", resource_group_name, ] network = ( _get_network_resource( [ "az", "network", "vnet", "subnet", "show", *network_args, "--vnet-name", vnet_name, "--name", subnet_name, ], artifact_name="az_network_vnet_subnet_show", artifacts_path=artifacts_path, ) if group_existed else None ) ssh_rule = ( _get_network_resource( [ "az", "network", "nsg", "rule", "show", *network_args, "--nsg-name", nsg_name, "--name", "DefaultAllowSSH", ], artifact_name="az_network_nsg_rule_ssh_show", artifacts_path=artifacts_path, ) if group_existed else None ) expected_nsg = ( f"/subscriptions/{azure_subscription}/resourceGroups/{resource_group_name}" f"/providers/Microsoft.Network/networkSecurityGroups/{nsg_name}" ) subnet_ready = ( network is not None and network.get("provisioningState") == "Succeeded" and (network.get("networkSecurityGroup") or {}).get("id", "").casefold() == expected_nsg.casefold() and network.get("defaultOutboundAccess") is False ) ssh_ready = ssh_rule is not None and all( ssh_rule.get(key) == value for key, value in { "provisioningState": "Succeeded", "access": "Allow", "direction": "Inbound", "protocol": "Tcp", "destinationPortRange": "22", "priority": 1000, "sourceAddressPrefix": "*", "sourcePortRange": "*", "destinationAddressPrefix": "*", }.items() ) if not subnet_ready and ssh_rule is None: nsg = ( _get_network_resource( ["az", "network", "nsg", "show", *network_args, "--name", nsg_name], artifact_name="az_network_nsg_show", artifacts_path=artifacts_path, ) if group_existed else None ) if nsg is None: _run_network_command( [ "az", "network", "nsg", "create", *network_args, "--location", azure_location, "--name", nsg_name, ], artifact_name="az_network_nsg_create", artifacts_path=artifacts_path, check=True, ) if not ssh_ready: _run_network_command( [ "az", "network", "nsg", "rule", "create", *network_args, "--nsg-name", nsg_name, "--name", "DefaultAllowSSH", "--priority", "1000", "--protocol", "Tcp", "--destination-port-range", "22", "--access", "Allow", ], artifact_name="az_network_nsg_rule_ssh_create", artifacts_path=artifacts_path, check=True, ) if subnet_ready: logger.info("reusing ready network in %s", resource_group_name) return vnet = ( _get_network_resource( ["az", "network", "vnet", "show", *network_args, "--name", vnet_name], artifact_name="az_network_vnet_show", artifacts_path=artifacts_path, ) if group_existed else None ) if vnet is None: _run_network_command( [ "az", "network", "vnet", "create", *network_args, "--location", azure_location, "--name", vnet_name, "--address-prefix", "10.0.0.0/16", ], artifact_name="az_network_vnet_create", artifacts_path=artifacts_path, check=True, ) proc = _run_network_command( [ "az", "network", "vnet", "subnet", "create", "--subscription", azure_subscription, "--resource-group", resource_group_name, "--vnet-name", vnet_name, "--name", subnet_name, "--address-prefix", "10.0.1.0/24", "--network-security-group", nsg_name, "--default-outbound-access", "false", ], artifact_name="az_network_vnet_subnet_create", artifacts_path=artifacts_path, check=True, ) logger.info("created subnet: %s", proc.stdout.strip()) @pytest.fixture def public_ip( temp_resource_group, azure_subscription, azure_location, hold_failures, random_suffix, request, artifacts_path, ): """Create a temporary public IP address for the test.""" while True: ip_name = f"temp-ip-{random_suffix}" public_ip_create_cmd = [ "az", "network", "public-ip", "create", "--subscription", azure_subscription, "--resource-group", temp_resource_group, "--location", azure_location, "--name", ip_name, "--sku", "Standard", "--allocation-method", "Static", "--query", "publicIp.ipAddress", "--output", "tsv", ] ip_tags = os.getenv("SELFTEST_PUBLIC_IP_TAGS", "") if ip_tags: public_ip_create_cmd += ["--ip-tags", ip_tags] proc = _run_network_command( public_ip_create_cmd, artifact_name="az_public_ip_create", artifacts_path=artifacts_path, check=True, ) public_ip = proc.stdout.strip() delete_cmd = [ "az", "network", "public-ip", "delete", "--subscription", azure_subscription, "--resource-group", temp_resource_group, "--name", ip_name, ] yield public_ip, ip_name if is_cleanup_permitted(request, hold_failures): for _ in range(30): # Some retries to delete the public IP may be required. proc = _subprocess_run( delete_cmd, check=False, artifacts_path=artifacts_path, artifact_name="az_public_ip_delete", ) if proc.returncode == 0: break time.sleep(2) return @pytest.fixture def delete_after_tag(): """Get the deleteAfter tag value.""" yield (datetime.now(timezone.utc) + timedelta(hours=24)).isoformat() @pytest.fixture(scope="session") def source_install(tmp_path_factory) -> Tuple[bool, Optional[Path], str]: """Snapshot checkout sources once per worker when testing source bits.""" if not _get_bool_env("SELFTEST_INSTALL_FROM_SOURCE"): return False, None, "" source = Path(__file__).resolve().parent.parent version_header = source / "src/version.h" if not version_header.is_file(): raise ValueError( "Source-install mode requires src/version.h; configure the checkout " "with cmake -S . -B build first" ) match = re.search( r'^#define\s+VERSION\s+"([^"\n]+)"\s*$', version_header.read_text(encoding="utf-8"), re.MULTILINE, ) if match is None or match.group(1) == "@VERSION@": raise ValueError(f"Missing or malformed VERSION definition in {version_header}") version = match.group(1) archive = tmp_path_factory.mktemp("source-install") / "source.tar.gz" with tarfile.open(archive, "w:gz") as tar: for name in ( "CMakeLists.txt", "cmake", "src", "doc", "ephemeral-disk-setup", "initramfs", "networkd", "udev", "selftest/selftest.py", "selftest/install-from-source.sh", ): tar.add(source / name, arcname=name) return True, archive, version class TestVMs: """Test VMs with different images and sizes.""" @pytest.fixture(autouse=True) def setup( self, admin_username, admin_password, azure_location, azure_subscription, artifacts_path, delete_after_tag, hold_failures, public_ip, reallocates, reboots, data_disk_luns, request, ssh_key_path, temp_resource_group, temp_vm_name, image, vm_size, source_install, ): """Initialize the test.""" self.admin_username = admin_username self.admin_password = admin_password self.artifacts_path = artifacts_path self.azure_location = azure_location self.azure_subscription = azure_subscription self.alloc = 0 self.boot = 0 self.delete_after_tag = delete_after_tag self.hold_failures = hold_failures self.image_config = image self.image = image.urn ( self.install_from_source, self.install_from_source_archive, self.install_from_source_version, ) = source_install self.public_ip, self.public_ip_name = public_ip self.reallocates = reallocates self.reboots = reboots self.data_disk_luns = data_disk_luns self.request = request self.selftest_script_path = ( Path(os.path.abspath(__file__)).parent / "selftest.py" ) self.ssh_key_path = ssh_key_path self.target_script_path = f"/home/{self.admin_username}/selftest.py" self.temp_resource_group = temp_resource_group self.vm_name = temp_vm_name self.vm_size = vm_size self.nsg_name = f"{temp_resource_group}-nsg" self.subnet_name = f"{temp_resource_group}-subnet" self.vnet_name = f"{temp_resource_group}-vnet" self.ssh_command = [ "ssh", "-i", self.ssh_key_path.as_posix(), "-o", "StrictHostKeyChecking=no", *SSH_OPTIONS, f"{self.admin_username}@{self.public_ip}", "--", "sudo", ] self.vm_cfg = ( f"name={self.vm_name}\n" f"rg={self.temp_resource_group}\n" f"ip={self.public_ip}\n" f"image={self.image}\n" f"size={self.vm_size}\n" f"install_from_source={self.install_from_source}\n" f"ssh_key_path={self.ssh_key_path.as_posix()}\n" f'ssh_cmd="{shlex.join(self.ssh_command)}"\n' f'console_cmd="az serial-console connect -n {self.vm_name} -g {self.temp_resource_group}"\n' f"artifacts_path={self.artifacts_path.as_posix()}\n" ) # Ensure the failed flag is reset for each test run as it is used request.session.failed = False try: yield finally: if is_cleanup_permitted(request, hold_failures): logger.info("cleaning:\n%s", self.vm_cfg) self._cleanup() else: logger.error( "TEST FAILED, HOLDING RESOURCES FOR DEBUGGING:\n%s", self.vm_cfg, ) @property def boot_id(self) -> str: """Generate a boot ID based on the alloc and boot counts.""" return f"alloc{self.alloc}-boot{self.boot}" def _cleanup(self): """Cleanup resources.""" proc = _subprocess_run( [ "az", "vm", "delete", "--subscription", self.azure_subscription, "--resource-group", self.temp_resource_group, "--name", self.vm_name, "--yes", ], artifacts_path=self.artifacts_path, artifact_name="az_vm_delete", ) if proc.returncode != 0: logger.error("failed to delete VM %s: %s", self.vm_name, proc.stderr) # A failed ARM deployment can create the NIC without creating the VM. # Make sure it's deleted in case it still exists and ignore any errors. proc = _subprocess_run( [ "az", "network", "nic", "delete", "--subscription", self.azure_subscription, "--resource-group", self.temp_resource_group, "--name", f"{self.vm_name}-nic", ], artifacts_path=self.artifacts_path, artifact_name="az_nic_delete", ) if proc.returncode != 0: logger.debug( "failed to delete NIC for VM %s: %s", self.vm_name, proc.stderr ) def _get_image_reference(self) -> Dict[str, str]: """Convert an image URN or resource ID into an ARM image reference.""" image = self.image if image.startswith("/"): if image.lower().startswith("/communitygalleries/"): return {"communityGalleryImageId": image} if image.lower().startswith("/sharedgalleries/"): return {"sharedGalleryImageId": image} return {"id": image} parts = image.split(":") if len(parts) != 4 or not all(parts): raise ValueError(f"expected publisher:offer:sku:version image URN: {image}") return dict(zip(("publisher", "offer", "sku", "version"), parts)) def _build_vm_template(self, image_reference: Dict[str, str]) -> Dict[str, Any]: """Create the NIC and all VM disks in a single ARM deployment.""" os_profile: Dict[str, Any] = { "computerName": self.vm_name, "adminUsername": self.admin_username, "linuxConfiguration": { "disablePasswordAuthentication": not bool(self.admin_password), "ssh": { "publicKeys": [ { "path": f"/home/{self.admin_username}/.ssh/authorized_keys", "keyData": Path(f"{self.ssh_key_path}.pub") .read_text(encoding="utf-8") .strip(), } ] }, }, } if self.admin_password: os_profile["adminPassword"] = "[parameters('adminPassword')]" sku_config = selftest.SKU_CONFIGS.get(self.vm_size) if ( self.image_config.workaround_util_linux_bug and sku_config is not None and not sku_config.nvme_only and sku_config.temp_disk_size_gib > 0 ): cloud_config = b"#cloud-config\nmounts:\n - [ephemeral0, /mnt, ext4]\n" os_profile["customData"] = base64.b64encode(cloud_config).decode("ascii") logger.info( "Applying util-linux mount workaround for %s", self.image_config.urn, ) elif self.image_config.workaround_util_linux_bug: logger.info( "Not applying util-linux mount workaround for %s: " "no known SCSI resource disk on %s", self.image_config.urn, self.vm_size, ) return { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "nicName": {"type": "string", "defaultValue": f"{self.vm_name}-nic"}, **( {"adminPassword": {"type": "secureString"}} if self.admin_password else {} ), }, "resources": [ { "type": "Microsoft.Network/networkInterfaces", "apiVersion": "2024-05-01", "name": "[parameters('nicName')]", "location": self.azure_location, "tags": {"deleteAfter": self.delete_after_tag}, "properties": { "networkSecurityGroup": { "id": f"[resourceId('Microsoft.Network/networkSecurityGroups', '{self.nsg_name}')]" }, "ipConfigurations": [ { "name": "ipconfig1", "properties": { "privateIPAllocationMethod": "Dynamic", "subnet": { "id": f"[resourceId('Microsoft.Network/virtualNetworks/subnets', '{self.vnet_name}', '{self.subnet_name}')]" }, "publicIPAddress": { "id": f"[resourceId('Microsoft.Network/publicIPAddresses', '{self.public_ip_name}')]" }, }, } ], }, }, { "type": "Microsoft.Compute/virtualMachines", "apiVersion": "2024-03-01", "name": self.vm_name, "location": self.azure_location, "tags": {"deleteAfter": self.delete_after_tag}, "dependsOn": [ "[resourceId('Microsoft.Network/networkInterfaces', parameters('nicName'))]" ], "properties": { "hardwareProfile": {"vmSize": self.vm_size}, "osProfile": os_profile, "storageProfile": { "imageReference": image_reference, "osDisk": { "createOption": "FromImage", "caching": "ReadWrite", "deleteOption": "Delete", "managedDisk": {"storageAccountType": "Premium_LRS"}, }, "dataDisks": [ { "lun": lun, "name": f"{self.vm_name}-lun-{lun}", "createOption": "Empty", "diskSizeGB": 100 * (index % 4 + 1), "caching": "None", "deleteOption": "Delete", "managedDisk": { "storageAccountType": "Premium_LRS" }, } for index, lun in enumerate(self.data_disk_luns) ], }, "networkProfile": { "networkInterfaces": [ { "id": "[resourceId('Microsoft.Network/networkInterfaces', parameters('nicName'))]", "properties": {"deleteOption": "Delete"}, } ] }, "diagnosticsProfile": {"bootDiagnostics": {"enabled": True}}, }, }, ], } def _create_vm(self) -> None: """Deploy a VM with its complete disk layout present at first boot.""" reference = self._get_image_reference() parameters = ( ["--parameters", f"adminPassword={self.admin_password}"] if self.admin_password else [] ) with tempfile.TemporaryDirectory(prefix="selftest-template-") as directory: template_path = Path(directory) / "vm-template.json" template_path.write_text( json.dumps(self._build_vm_template(reference)), encoding="utf-8", ) proc = _subprocess_run( [ "az", "deployment", "group", "create", "--subscription", self.azure_subscription, "--resource-group", self.temp_resource_group, "--name", self.vm_name, "--mode", "Incremental", "--template-file", str(template_path), "--query", "properties.provisioningState", "--output", "tsv", ] + parameters, artifacts_path=self.artifacts_path, artifact_name="az_vm_create", redact_values=(self.admin_password,) if self.admin_password else (), ) if proc.returncode != 0: for code in ( "QuotaExceeded", "SkuNotAvailable", "AllocationFailed", f"The requested VM size {self.vm_size} is not available in the current region.", ): if code in proc.stderr: pytest.skip( f"Unable to create VM due to {code} for {self.vm_size}: {proc.stderr}" ) raise RuntimeError( f"VM deployment failed: stdout={proc.stdout} stderr={proc.stderr}" ) logger.info("created VM:\n%s", self.vm_cfg) (self.artifacts_path / "vm.cfg").write_text(self.vm_cfg) def _execute_ssh_command( self, command: List[str], *, artifact_name: str, decode: bool = False, timeout: Optional[float] = None, ) -> subprocess.CompletedProcess: """Execute a command on the VM with sudo.""" proc = _subprocess_run( self.ssh_command + command, artifacts_path=self.artifacts_path, artifact_name=artifact_name, decode=decode, timeout=timeout, ) return proc def _fetch_boot_diagnostics(self) -> None: """Fetch boot diagnostics logs.""" deadline = time.monotonic() + 300 while time.monotonic() < deadline: proc = _subprocess_run( [ "az", "vm", "boot-diagnostics", "get-boot-log", "--subscription", self.azure_subscription, "--resource-group", self.temp_resource_group, "--name", self.vm_name, "--output", "tsv", ], artifacts_path=self.artifacts_path, artifact_name=f"{self.boot_id}-console", decode=True, timeout=max(0.1, min(30, deadline - time.monotonic())), ) if proc.returncode == 0 and "BlobNotFound" not in proc.stderr: return time.sleep(1) raise RuntimeError( f"boot diagnostics unavailable for VM {self.vm_name} after five minutes" ) def _fetch_logs(self) -> None: """Fetch logs.""" proc = self._execute_ssh_command( ["journalctl", "-o", "short-monotonic", "-b"], artifact_name=f"{self.boot_id}-journal", ) if proc.returncode != 0: raise RuntimeError( f"failed to fetch journalctl logs on {self.boot_id} @ {self.artifacts_path.as_posix()}: {proc.stderr}" ) self._scp_from_vm("/var/log/cloud-init.log", f"{self.boot_id}-cloud-init.log") self._scp_from_vm( "/var/log/cloud-init-output.log", f"{self.boot_id}-cloud-init-output.log" ) self._execute_ssh_command( ["rm", "-f", "/var/log/cloud-init.log", "/var/log/cloud-init-output.log"], artifact_name=f"{self.boot_id}-rm-logs", ) def _run_selftest_script(self) -> None: """Run selftest script on the VM.""" script = ( "/usr/sbin/azure-vm-utils-selftest" if self.install_from_source else self.target_script_path ) command = [script, "--debug"] if not self.install_from_source: command = [ "sh", "-c", shlex.quote( "for python in python3 python3.11; do " 'if "$python" -c "from dataclasses import dataclass; from typing import Literal" 2>/dev/null; then ' 'exec "$python" "$@"; fi; done; ' 'echo "Selftest requires Python 3.8 or newer with dataclasses and typing.Literal" >&2; exit 1' ), "selftest", script, "--debug", "--compat", ] proc = self._execute_ssh_command( command, artifact_name=f"{self.boot_id}-selftest", ) if proc.returncode != 0: raise RuntimeError( f"self-test failed on {self.boot_id} @ {self.artifacts_path.as_posix()}: {proc.stderr}" ) def _install_from_source(self) -> None: """Install from source and reboot before validation.""" if not self.install_from_source: return assert self.install_from_source_archive is not None remote_archive = f"/home/{self.admin_username}/azure-vm-utils-source.tar.gz" self._scp_to_vm(self.install_from_source_archive, remote_archive) proc = self._execute_ssh_command( ["mktemp", "-d", "/var/tmp/azure-vm-utils-selftest-XXXXXXXX"], artifact_name="create-source-directory", ) source_dir = proc.stdout.strip() if proc.returncode != 0 or not source_dir: raise RuntimeError( f"failed to create source directory: {proc.stdout} {proc.stderr}" ) proc = self._execute_ssh_command( ["tar", "-xzf", remote_archive, "-C", source_dir], artifact_name="extract-source", ) if proc.returncode != 0: raise RuntimeError( f"failed to extract source archive: {proc.stdout} {proc.stderr}" ) (self.artifacts_path / "source-install-version").write_text( f"{self.install_from_source_version}\nsha256={hashlib.sha256(self.install_from_source_archive.read_bytes()).hexdigest()}\n", encoding="utf-8", ) proc = self._execute_ssh_command( [ "sh", "-x", f"{source_dir}/selftest/install-from-source.sh", source_dir, self.install_from_source_version, ], artifact_name="install-from-source", ) if proc.returncode != 0: raise RuntimeError( f"failed to install source bits (exit {proc.returncode}); " f"log: {self.artifacts_path / 'install-from-source'}\n" f"stdout:\n{proc.stdout[-4000:]}\nstderr:\n{proc.stderr[-4000:]}" ) self._reboot_vm() def _verify_source_install(self) -> None: """Ensure the installed checkout files remain in use on every boot.""" if not self.install_from_source: return proc = self._execute_ssh_command( ["sha256sum", "--check", "/var/tmp/azure-vm-utils-selftest.sha256"], artifact_name=f"{self.boot_id}-source-install-checksums", ) if proc.returncode != 0: raise RuntimeError( f"source-install checksum verification failed: {proc.stdout} {proc.stderr}" ) proc = self._execute_ssh_command( ["/usr/sbin/azure-nvme-id", "--version"], artifact_name=f"{self.boot_id}-source-install-version", ) if proc.returncode != 0 or proc.stdout.split() != [ "/usr/sbin/azure-nvme-id", self.install_from_source_version, ]: raise RuntimeError( f"unexpected source-install version: {proc.stdout} {proc.stderr}" ) def _scp_from_vm(self, src: str, artifact_name: str) -> Path: """Copy file from the VM using ssh with sudo.""" dst = self.artifacts_path / artifact_name proc = self._execute_ssh_command( ["cat", src], artifact_name=f"{artifact_name}.cmd" ) if proc.returncode != 0: raise RuntimeError( f"failed to copy {src} -> {dst.as_posix()}: {proc.stderr}" ) dst.write_text(proc.stdout, encoding="utf-8") return dst def _scp_to_vm(self, src: Path, dst: str) -> None: """Copy file to VM.""" src_path = src.as_posix() dst = f"{self.admin_username}@{self.public_ip}:{dst}" proc = _subprocess_run( [ "scp", "-i", self.ssh_key_path.as_posix(), *SSH_OPTIONS, src_path, dst, ], artifacts_path=self.artifacts_path, artifact_name=f"scp-{src.name}", ) if proc.returncode != 0: raise RuntimeError(f"failed to scp {src_path} -> {dst}: {proc.stderr}") def _deallocate_vm(self) -> None: """Deallocate the VM.""" proc = _subprocess_run( [ "az", "vm", "deallocate", "--subscription", self.azure_subscription, "--resource-group", self.temp_resource_group, "--name", self.vm_name, ], artifacts_path=self.artifacts_path, artifact_name="az_vm_deallocate", ) if proc.returncode != 0: raise RuntimeError(f"failed to deallocate VM {self.vm_name}: {proc.stderr}") logger.info("deallocated VM: %s", self.vm_name) def _get_vm_powerstate(self) -> str: """Get the power state of the VM.""" proc = _subprocess_run( [ "az", "vm", "get-instance-view", "--subscription", self.azure_subscription, "--resource-group", self.temp_resource_group, "--name", self.vm_name, "--query", "instanceView.statuses[?starts_with(code,'PowerState/')].displayStatus", "-o", "tsv", ], artifacts_path=self.artifacts_path, artifact_name="az_vm_get_power_state", ) if proc.returncode != 0: raise RuntimeError( f"failed to get VM power state {self.vm_name}: {proc.stderr}" ) return proc.stdout.strip() def _is_vm_running(self) -> bool: """Check if the VM is running.""" power_state = self._get_vm_powerstate() logger.debug("VM %s power state: %s", self.vm_name, power_state) return power_state == "VM running" def _start_vm(self) -> None: """Start the VM with retries if needed.""" retries_remaining = 5 while True: proc = _subprocess_run( [ "az", "vm", "start", "--subscription", self.azure_subscription, "--resource-group", self.temp_resource_group, "--name", self.vm_name, ], artifacts_path=self.artifacts_path, artifact_name=( "az_vm_start" if retries_remaining == 5 else f"az_vm_start-attempt{6 - retries_remaining}" ), ) if proc.returncode == 0: return transient_errors = ( "Conflict", "InternalExecutionError", "Temporary failure in name resolution", ) if ( any(error in proc.stderr for error in transient_errors) and retries_remaining > 0 ): logger.warning( "start VM %s failed transiently: %s; retrying (%d attempts left)", self.vm_name, proc.stderr.strip(), retries_remaining, ) time.sleep(5) retries_remaining -= 1 continue raise RuntimeError(f"failed to start VM {self.vm_name}: {proc.stderr}") def _reboot_vm(self, retry_wait: int = 10, retry_total_secs: int = 1200) -> None: """Reboot the VM and verify it is ready on a different kernel boot.""" before = self._execute_ssh_command( ["cat", "/proc/sys/kernel/random/boot_id"], artifact_name=f"{self.boot_id}-reboot-id-before", timeout=30, ) before_id = before.stdout.strip() if before.returncode != 0 or not before_id: raise RuntimeError(f"failed to read boot ID before reboot: {before.stderr}") try: proc = self._execute_ssh_command( ["reboot"], artifact_name=f"{self.boot_id}-reboot", timeout=30 ) except subprocess.TimeoutExpired: logger.warning( "reboot command timed out; verifying VM %s boot ID", self.vm_name ) else: # SSH can disconnect before reboot returns its exit status. if proc.returncode not in (0, 255): raise RuntimeError(f"failed to reboot VM {self.vm_name}: {proc.stderr}") time.sleep(retry_wait) deadline = time.monotonic() + retry_total_secs while True: remaining = deadline - time.monotonic() if remaining <= 0: break try: after = self._execute_ssh_command( ["cat", "/proc/sys/kernel/random/boot_id"], artifact_name=f"{self.boot_id}-reboot-id-after", timeout=min(30, remaining), ) except subprocess.TimeoutExpired: logger.debug("waiting for VM %s SSH after reboot", self.vm_name) else: after_id = after.stdout.strip() if after.returncode == 0 and after_id and after_id != before_id: self._wait_for_vm() return if after.returncode not in (0, 255): raise RuntimeError( f"failed to read boot ID after reboot: {after.stdout} {after.stderr}" ) remaining = deadline - time.monotonic() if remaining < retry_wait: break logger.debug( "waiting for VM %s boot ID to change, %d seconds until timeout", self.vm_name, remaining, ) time.sleep(retry_wait) raise RuntimeError( f"reboot not verified for VM {self.vm_name}: " f"boot ID did not change from {before.stdout.strip()} within " f"{retry_total_secs} seconds" ) def _verify_image(self) -> None: """Verify the image has scrubbed conflicting configurations.""" for config in [ "/etc/NetworkManager/conf.d/99-azure-unmanaged-devices.conf", "/etc/udev/rules.d/68-azure-sriov-nm-unmanaged.rules", ]: proc = self._execute_ssh_command( ["test", "-f", config], artifact_name=f"verify-image-{os.path.basename(config)}", ) if proc.returncode == 0: msg = f"config {config} found in {self.image}" logger.error(msg) def _inspect_image_utilities(self) -> None: """Record what the image provides before installing any source bits.""" proc = self._execute_ssh_command( ["sh", "-c", shlex.quote("command -v azure-nvme-id")], artifact_name="image-azure-vm-utils-presence", ) if proc.returncode not in (0, 1): raise RuntimeError( f"failed to check image utilities (exit {proc.returncode}): {proc.stderr}" ) version = None if proc.returncode == 0: proc = self._execute_ssh_command( ["azure-nvme-id", "--version"], artifact_name="image-azure-vm-utils-version", ) fields = proc.stdout.split() if proc.returncode != 0 or len(fields) != 2: raise RuntimeError( f"failed to read image utilities version (exit {proc.returncode}): " f"{proc.stdout} {proc.stderr}" ) version = fields[1] self.image_config = replace( self.image_config, azure_vm_utils_version=version, ) (self.artifacts_path / "image-info.json").write_text( json.dumps(asdict(self.image_config), indent=2) + "\n", encoding="utf-8" ) logger.info("image utilities: %s", self.image_config) if ( self.image_config.azure_vm_utils_version is None and not self.install_from_source ): pytest.skip(f"azure-vm-utils is not installed in {self.image}") def _wait_for_vm(self) -> None: """Wait for the VM to be ready.""" attempt = 0 deadline = datetime.now(timezone.utc) + timedelta(minutes=30) while datetime.now(timezone.utc) < deadline: attempt += 1 logger.debug( "waiting for VM to be ready on boot %s (attempt %d)...", self.boot_id, attempt, ) if not self._is_vm_running(): logger.debug("VM %s is not running, starting...", self.vm_name) self._start_vm() # Refresh boot diagnostics on every loop in case something went wrong. self._fetch_boot_diagnostics() proc = self._execute_ssh_command( ["systemctl", "is-system-running", "--wait"], artifact_name="systemctl-status", ) status = proc.stdout.strip() if not status: status = "failed to ssh" logger.debug("VM status: %s", status) if status in ("running", "degraded"): # Ensure cloud-init is done before proceeding too. self._execute_ssh_command( ["cloud-init", "status", "--wait"], artifact_name="cloud-init-status", ) return logger.debug("debug: %s bash", shlex.join(self.ssh_command)) logger.debug( " az serial-console connect --name %s --resource-group %s", self.vm_name, self.temp_resource_group, ) time.sleep(2) raise RuntimeError( f"VM {self.vm_name} did not become ready in time after 30 minutes: {self.artifacts_path.as_posix()}" ) def run_test(self) -> None: """Create VM and run self-tests.""" try: logger.debug("artifacts path: %s", self.artifacts_path.as_posix()) logger.debug("public IP: %s", self.public_ip) self._create_vm() self._wait_for_vm() self._inspect_image_utilities() self._scp_to_vm( self.selftest_script_path, self.target_script_path, ) self._verify_image() self._install_from_source() for self.alloc in range(0, self.reallocates + 1): for self.boot in range(0, self.reboots + 1): self._wait_for_vm() self._fetch_boot_diagnostics() self._fetch_logs() self._verify_source_install() self._run_selftest_script() # Check if we need to reboot or reallocate VM. if self.boot < self.reboots: self._reboot_vm() elif self.alloc < self.reallocates: self._deallocate_vm() self._start_vm() except Exception as error: self.request.session.failed = True logger.error( "test failed: %r\nartifacts: %s\ntraceback: %s", error, self.artifacts_path.as_posix(), traceback.format_exc(), ) raise RuntimeError( f"test failed: {error} @ {self.artifacts_path.as_posix()}" ) from error @pytest.mark.parametrize( "vm_size,image,data_disk_luns", _get_image_test_cases(), ) def test_image(self, image, vm_size, data_disk_luns): """Validate an image on its selected hardware configuration.""" self.run_test() azure-vm-utils-0.8.0/src/000077500000000000000000000000001527236310300152145ustar00rootroot00000000000000azure-vm-utils-0.8.0/src/checked.h000066400000000000000000000055551527236310300167650ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #ifndef __CHECKED_H__ #define __CHECKED_H__ #include #include #include #include #include #include "exit_code.h" /* Checked allocation helpers terminate the process on failure. */ static inline void json_object_array_add_checked(json_object *array, json_object *value) { if (json_object_array_add(array, value) != 0) { fprintf(stderr, "json_object_array_add() failed: %m\n"); exit(EXIT_CODE_NO_MEMORY); } } static inline json_object *json_object_new_array_checked(void) { json_object *array = json_object_new_array(); if (array == NULL) { fprintf(stderr, "json_object_new_array() failed: %m\n"); exit(EXIT_CODE_NO_MEMORY); } return array; } static inline json_object *json_object_new_int_checked(int value) { json_object *property = json_object_new_int(value); if (property == NULL) { fprintf(stderr, "json_object_new_int() failed: %m\n"); exit(EXIT_CODE_NO_MEMORY); } return property; } static inline json_object *json_object_new_object_checked(void) { json_object *object = json_object_new_object(); if (object == NULL) { fprintf(stderr, "json_object_new_object() failed: %m\n"); exit(EXIT_CODE_NO_MEMORY); } return object; } static inline json_object *json_object_new_string_checked(const char *value) { json_object *property = json_object_new_string(value); if (property == NULL) { fprintf(stderr, "json_object_new_string() failed: %m\n"); exit(EXIT_CODE_NO_MEMORY); } return property; } static inline void json_object_object_add_checked(json_object *object, const char *key, json_object *value) { if (json_object_object_add(object, key, value) != 0) { fprintf(stderr, "json_object_object_add() failed for %s: %m\n", key); exit(EXIT_CODE_NO_MEMORY); } } static inline const char *json_object_to_json_string_ext_checked(json_object *object, int flags) { const char *json = json_object_to_json_string_ext(object, flags); if (json == NULL) { fprintf(stderr, "json_object_to_json_string_ext() failed: %m\n"); exit(EXIT_CODE_NO_MEMORY); } return json; } static inline char *strdup_checked(const char *value) { char *copy = strdup(value); if (copy == NULL) { fprintf(stderr, "strdup() failed: %m\n"); exit(EXIT_CODE_NO_MEMORY); } return copy; } static inline char *strndup_checked(const char *value, size_t length) { char *copy = strndup(value, length); if (copy == NULL) { fprintf(stderr, "strndup() failed: %m\n"); exit(EXIT_CODE_NO_MEMORY); } return copy; } #endif // __CHECKED_H__ azure-vm-utils-0.8.0/src/debug.c000066400000000000000000000007131527236310300164470ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #include "debug.h" bool debug = false; extern char **environ; /** * Dump environment variables. */ void debug_environment_variables() { int i = 0; DEBUG_PRINTF("Environment Variables:\n"); while (environ[i]) { DEBUG_PRINTF("%s\n", environ[i]); i++; } } azure-vm-utils-0.8.0/src/debug.h000066400000000000000000000022461527236310300164570ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #ifndef __DEBUG_H__ #define __DEBUG_H__ #include #include extern bool debug; #define DEBUG_PRINTF(fmt, ...) \ do \ { \ if (debug == true) \ { \ fprintf(stderr, "DEBUG: " fmt, ##__VA_ARGS__); \ } \ } while (0) void debug_environment_variables(); #endif // __DEBUG_H__ azure-vm-utils-0.8.0/src/exit_code.h000066400000000000000000000013171527236310300173320ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #ifndef __EXIT_CODE_H__ #define __EXIT_CODE_H__ enum exit_code { EXIT_CODE_INVALID_ARGUMENT = 1, EXIT_CODE_CONTROLLER_SCAN = 2, EXIT_CODE_NAMESPACE_SCAN = 3, EXIT_CODE_NO_MEMORY = 4, EXIT_CODE_NVME_DEVICE_OPEN = 5, EXIT_CODE_NVME_IDENTIFY_NAMESPACE_IOCTL = 6, EXIT_CODE_NVME_IDENTIFY_NAMESPACE_STATUS = 7, EXIT_CODE_NVME_NAMESPACE_ID_IOCTL = 8, EXIT_CODE_NVME_PAGE_SIZE = 9, EXIT_CODE_SUCCESS = 0, EXIT_CODE_UDEV_INVALID_PROPERTIES = 10, EXIT_CODE_UDEV_MISSING_DEVNAME = 11, }; #endif // __EXIT_CODE_H__ azure-vm-utils-0.8.0/src/identify_disks.c000066400000000000000000000256031527236310300203760ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #include #include #include #include #include #include #include #include #include "checked.h" #include "debug.h" #include "exit_code.h" #include "identify_disks.h" #include "nvme.h" /** * Trim trailing whitespace from a string in-place. */ void trim_trailing_whitespace(char *str) { size_t len = strlen(str); while (len > 0 && isspace((unsigned char)str[len - 1])) { str[--len] = '\0'; // Replace trailing whitespace with null terminator } } /** * Callback for scandir() to filter for NVMe namespaces. */ int is_nvme_namespace(const struct dirent *entry) { unsigned int ctrl, nsid; char p; // Check for NVME controller name format: nvmen return sscanf(entry->d_name, "nvme%un%u%c", &ctrl, &nsid, &p) == 2; } /** * Identify namespace without using vendor-specific command set. * Used as a fallback when vendor-specific command set is not supported. */ char *identify_namespace_without_vs(const char *controller_sys_path, int nsid) { char model_name[MAX_PATH] = {0}; char model_path[MAX_PATH]; snprintf(model_path, sizeof(model_path), "%s/model", controller_sys_path); FILE *file = fopen(model_path, "r"); if (file == NULL) { fprintf(stderr, "failed to open %s: %m\n", model_path); // Keep the namespace with empty properties when fallback metadata is unavailable. return strdup_checked(""); } char *result = fgets(model_name, sizeof(model_name), file); if (result == NULL) { if (ferror(file)) { fprintf(stderr, "failed to read model name from %s: %m\n", model_path); } else { fprintf(stderr, "failed to read model name from %s: unexpected end of file\n", model_path); } fclose(file); // Keep the namespace with empty properties when fallback metadata is unavailable. return strdup_checked(""); } fclose(file); trim_trailing_whitespace(model_name); DEBUG_PRINTF("read model name=\"%s\"\n", model_name); if (strcmp(model_name, "MSFT NVMe Accelerator v1.0") == 0) { // Microsoft Azure NVMe Accelerator v1.0 supports remote OS and data disks. // nsid=1 is the OS disk and nsid=2+ for data disks. // A data disk's lun id == nsid - 2. if (nsid == 1) { return strdup_checked("type=os"); } else { char *output = NULL; if (asprintf(&output, "type=data,lun=%d", nsid - 2) < 0) { fprintf(stderr, "asprintf() failed: %m\n"); exit(EXIT_CODE_NO_MEMORY); } return output; } } else if (strcmp(model_name, "Microsoft NVMe Direct Disk") == 0 || strcmp(model_name, "Microsoft NVMe Direct Disk v2") == 0) { return strdup_checked("type=local"); } return strdup_checked(""); } /** * Parse a vs string and turn it into a JSON dictionary. * Example: "type=local,index=2,name=nvme-600G-2" */ json_object *parse_vs_string(const char *vs) { json_object *vs_obj = json_object_new_object_checked(); char *vs_copy = strdup_checked(vs); char *token; char *rest = vs_copy; while ((token = strtok_r(rest, ",", &rest))) { char *key = strtok_r(token, "=", &token); char *value = strtok_r(NULL, "=", &token); if (key != NULL && value != NULL) { int int_value = atoi(value); if ((strcmp(key, "lun") == 0) || (strcmp(key, "index") == 0)) { if (int_value == 0 && strcmp(value, "0") != 0) { fprintf(stderr, "failed to parse vs=%s key=%s value=%s as int\n", vs, key, value); // Preserve the reported value as a string instead of failing enumeration. json_object_object_add_checked(vs_obj, key, json_object_new_string_checked(value)); } else { json_object_object_add_checked(vs_obj, key, json_object_new_int_checked(int_value)); } } else { json_object_object_add_checked(vs_obj, key, json_object_new_string_checked(value)); } } } free(vs_copy); return vs_obj; } /** * Enumerate namespaces for a controller. */ int enumerate_namespaces_for_controller(struct nvme_controller *ctrl, struct context *ctx, json_object *namespaces_array) { struct dirent **namelist; int n = scandir(ctrl->sys_path, &namelist, is_nvme_namespace, versionsort); if (n < 0) { // A controller removed during enumeration has no remaining namespaces to report. int result = errno == ENOENT ? EXIT_CODE_SUCCESS : EXIT_CODE_NAMESPACE_SCAN; fprintf(stderr, "failed scandir for %s: %m\n", ctrl->sys_path); return result; } DEBUG_PRINTF("found %d namespace(s) for controller=%s:\n", n, ctrl->name); for (int i = 0; i < n; i++) { char *id; char namespace_path[MAX_PATH]; snprintf(namespace_path, sizeof(namespace_path), "/dev/%s", namelist[i]->d_name); int nsid = get_nsid_from_namespace_device_path(namespace_path); char *vs = nvme_identify_namespace_vs(namespace_path, nsid); json_object *namespace_obj = json_object_new_object_checked(); json_object_object_add_checked(namespace_obj, "path", json_object_new_string_checked(namespace_path)); json_object_object_add_checked(namespace_obj, "model", json_object_new_string_checked(ctrl->model)); json_object_object_add_checked(namespace_obj, "nsid", json_object_new_int_checked(nsid)); json_object_array_add_checked(namespaces_array, namespace_obj); if (vs[0] == 0) { id = identify_namespace_without_vs(ctrl->sys_path, nsid); } else { id = strdup_checked(vs); } if (ctx->output_format == PLAIN) { printf("%s: %s\n", namespace_path, id); } json_object_object_add_checked(namespace_obj, "properties", parse_vs_string(id)); free(id); json_object_object_add_checked(namespace_obj, "vs", json_object_new_string_checked(vs)); free(vs); free(namelist[i]); } free(namelist); return EXIT_CODE_SUCCESS; } /** * Check if NVMe vendor in sysfs matches Microsoft's vendor ID. */ int is_microsoft_nvme_device(const char *device_name) { char vendor_id_path[MAX_PATH]; snprintf(vendor_id_path, sizeof(vendor_id_path), "%s/%s/device/vendor", SYS_CLASS_NVME_PATH, device_name); FILE *file = fopen(vendor_id_path, "r"); if (file == NULL) { fprintf(stderr, "failed to open %s: %m\n", vendor_id_path); // Skip unverified controllers so other devices can still be enumerated. return 0; } unsigned int vendor_id; int result = fscanf(file, "%x", &vendor_id); if (result != 1) { if (ferror(file)) { fprintf(stderr, "failed to read vendor ID from %s: %m\n", vendor_id_path); } else { fprintf(stderr, "failed to parse vendor ID from %s: expected a hexadecimal vendor ID\n", vendor_id_path); } fclose(file); // Skip unverified controllers so other devices can still be enumerated. return 0; } fclose(file); return vendor_id == MICROSOFT_NVME_VENDOR_ID; } /** * Callback for scandir() to filter for Microsoft Azure NVMe controllers. */ int is_azure_nvme_controller(const struct dirent *entry) { unsigned int ctrl; char p; // Check for NVME controller name format: nvme if (sscanf(entry->d_name, "nvme%u%c", &ctrl, &p) != 1) { return false; } return is_microsoft_nvme_device(entry->d_name); } /** * Initialize nvme_controller structure. Read model name from sysfs, * trimming off whitespaces. */ bool initialize_nvme_controller(struct nvme_controller *ctrl, const char *name) { char model_path[sizeof(ctrl->sys_path) + sizeof("/model")]; snprintf(ctrl->name, sizeof(ctrl->name), "%s", name); snprintf(ctrl->dev_path, sizeof(ctrl->dev_path), "/dev/%s", ctrl->name); snprintf(ctrl->sys_path, sizeof(ctrl->sys_path), "%s/%s", SYS_CLASS_NVME_PATH, ctrl->name); snprintf(model_path, sizeof(model_path), "%s/model", ctrl->sys_path); FILE *file = fopen(model_path, "r"); if (file == NULL) { fprintf(stderr, "failed to open %s: %m\n", model_path); return false; } char *result = fgets(ctrl->model, sizeof(ctrl->model), file); if (result == NULL) { if (ferror(file)) { fprintf(stderr, "failed to read model name from %s: %m\n", model_path); } else { fprintf(stderr, "failed to read model name from %s: unexpected end of file\n", model_path); } fclose(file); return false; } fclose(file); trim_trailing_whitespace(ctrl->model); return true; } /** * Enumerate Microsoft Azure NVMe controllers and identify disks. */ int identify_disks(struct context *ctx) { struct dirent **namelist = NULL; int result = EXIT_CODE_SUCCESS; json_object *namespaces_array = json_object_new_array_checked(); int n = scandir(SYS_CLASS_NVME_PATH, &namelist, is_azure_nvme_controller, versionsort); if (n < 0) { // Systems without NVMe devices may have no controller directory. result = errno == ENOENT ? EXIT_CODE_SUCCESS : EXIT_CODE_CONTROLLER_SCAN; fprintf(stderr, "no NVMe devices in %s: %m\n", SYS_CLASS_NVME_PATH); n = 0; } DEBUG_PRINTF("found %d controllers\n", n); for (int i = 0; i < n; i++) { struct nvme_controller ctrl; if (initialize_nvme_controller(&ctrl, namelist[i]->d_name)) { result = enumerate_namespaces_for_controller(&ctrl, ctx, namespaces_array); if (result != EXIT_CODE_SUCCESS) { break; } } else { // Skip controllers with unavailable model metadata and continue enumeration. fprintf(stderr, "failed to initialize NVMe controller %s\n", namelist[i]->d_name); } } for (int i = 0; i < n; i++) { free(namelist[i]); } free(namelist); if (result == EXIT_CODE_SUCCESS && ctx->output_format == JSON) { int flags = JSON_C_TO_STRING_NOSLASHESCAPE | JSON_C_TO_STRING_PRETTY | JSON_C_TO_STRING_SPACED; const char *json_output = json_object_to_json_string_ext_checked(namespaces_array, flags); printf("%s\n", json_output); } json_object_put(namespaces_array); return result; } azure-vm-utils-0.8.0/src/identify_disks.h000066400000000000000000000024251527236310300204000ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #ifndef __IDENTIFY_DISKS_H__ #define __IDENTIFY_DISKS_H__ #include #include #include #define MAX_PATH 4096 #define MICROSOFT_NVME_VENDOR_ID 0x1414 #define SYS_CLASS_NVME_PATH "/sys/class/nvme" #ifdef UNIT_TESTING_SYS_CLASS_NVME extern char *fake_sys_class_nvme_path; #undef SYS_CLASS_NVME_PATH #define SYS_CLASS_NVME_PATH fake_sys_class_nvme_path #endif struct nvme_controller { char name[256]; char dev_path[MAX_PATH]; char sys_path[MAX_PATH]; char model[MAX_PATH]; }; struct context { enum { PLAIN, JSON, } output_format; }; void trim_trailing_whitespace(char *str); char *identify_namespace_without_vs(const char *controller_sys_path, int nsid); int is_microsoft_nvme_device(const char *device_name); int is_nvme_namespace(const struct dirent *entry); int enumerate_namespaces_for_controller(struct nvme_controller *ctrl, struct context *ctx, json_object *namespaces_array); int is_azure_nvme_controller(const struct dirent *entry); int identify_disks(struct context *ctx); #endif // __IDENTIFY_DISKS_H__ azure-vm-utils-0.8.0/src/identify_udev.c000066400000000000000000000051201527236310300202140ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #include #include #include #include #include "exit_code.h" #include "nvme.h" /** * Print a udev key-value pair for environment variable. */ void print_udev_key_value(const char *key, const char *value) { printf("AZURE_DISK_"); while (*key) { putchar(toupper((unsigned char)*key)); key++; } printf("=%s\n", value); } /** * Print udev environment variables for all vs properties. * * Example parsing for vs: * type=local,index=2,name=nvme-600G-2 * * Environment variables printed include: * AZURE_DISK_TYPE=local * AZURE_DISK_INDEX=2 * AZURE_DISK_NAME=nvme-600G-2 */ int print_udev_key_values_for_vs(char *vs) { char *vs_copy = strdup(vs); if (vs_copy == NULL) { fprintf(stderr, "strdup() failed: %m\n"); return EXIT_CODE_NO_MEMORY; } char *outer_saveptr = NULL; char *inner_saveptr = NULL; char *pair = strtok_r(vs_copy, ",", &outer_saveptr); while (pair != NULL) { char *key = strtok_r(pair, "=", &inner_saveptr); char *value = strtok_r(NULL, "=", &inner_saveptr); if (key == NULL || value == NULL) { fprintf(stderr, "failed to parse key-value pair: %s\n", pair); free(vs_copy); return EXIT_CODE_UDEV_INVALID_PROPERTIES; } print_udev_key_value(key, value); pair = strtok_r(NULL, ",", &outer_saveptr); } free(vs_copy); return EXIT_CODE_SUCCESS; } /** * Execute udev mode, printing out environment variables for import. * * The udev rules will trigger for Azure NVMe controllers, so no additional * checks are needed. The device path will be in DEVNAME environment variable. * * Environment variables printed include: * - AZURE_DISK_VS: * - AZURE_DISK_TYPE: * - AZURE_DISK_INDEX: * - AZURE_DISK_NAME: * * @return 0 on success, non-zero on error. */ int identify_udev_device(void) { const char *dev_name = getenv("DEVNAME"); if (dev_name == NULL) { fprintf(stderr, "environment variable 'DEVNAME' not set\n"); return EXIT_CODE_UDEV_MISSING_DEVNAME; } char *vs = nvme_identify_namespace_vs_for_namespace_device(dev_name); printf("AZURE_DISK_VS=%s\n", vs); int result = print_udev_key_values_for_vs(vs); free(vs); return result; } azure-vm-utils-0.8.0/src/identify_udev.h000066400000000000000000000006101527236310300202200ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #ifndef __IDENTIFY_UDEV_H__ #define __IDENTIFY_UDEV_H__ void print_udev_key_value(const char *key, const char *value); int print_udev_key_values_for_vs(char *vs); int identify_udev_device(void); #endif // __IDENTIFY_UDEV_H__ azure-vm-utils-0.8.0/src/main.c000066400000000000000000000055371527236310300163160ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #include #include #include "debug.h" #include "exit_code.h" #include "identify_disks.h" #include "identify_udev.h" #include "version.h" void print_help(const char *program) { printf("Usage: %s [-d|--debug] [-u|--udev|-h|--help|-v|--version]\n", program); printf(" -d, --debug Enable debug mode\n"); printf(" -f, --format {plain|json} Output format (default=plain)\n"); printf(" -h, --help Display this help message\n"); printf(" -u, --udev Enable udev mode\n"); printf(" -v, --version Display the version\n"); } void print_version(const char *program) { printf("%s %s\n", program, VERSION); } void print_invalid_argument(const char *program, const char *argument) { fprintf(stderr, "invalid argument: %s\n", argument); print_help(program); } int main(int argc, char **argv) { bool udev_mode = false; struct context ctx = {.output_format = PLAIN}; int opt; int option_index = 0; // clang-format off static struct option long_options[] = { {"debug", no_argument, 0, 'd'}, {"udev", no_argument, 0, 'u'}, {"version", no_argument, 0, 'v'}, {"help", no_argument, 0, 'h'}, {"format", required_argument, 0, 'f'}, {0, 0, 0, 0}}; // clang-format on while ((opt = getopt_long(argc, argv, "duvhf:", long_options, &option_index)) != -1) { switch (opt) { case 'd': debug = true; break; case 'u': udev_mode = true; break; case 'v': print_version(argv[0]); return EXIT_CODE_SUCCESS; case 'h': print_help(argv[0]); return EXIT_CODE_SUCCESS; case 'f': if (strcmp(optarg, "json") == 0) { ctx.output_format = JSON; } else if (strcmp(optarg, "plain") == 0) { ctx.output_format = PLAIN; } else { print_invalid_argument(argv[0], optarg); return EXIT_CODE_INVALID_ARGUMENT; } break; default: // Error for invalid --args print_invalid_argument(argv[0], argv[optind - 1]); return EXIT_CODE_INVALID_ARGUMENT; } } // Error for unparsed args if (optind < argc) { print_invalid_argument(argv[0], argv[optind]); return EXIT_CODE_INVALID_ARGUMENT; } if (debug) { debug_environment_variables(); } if (udev_mode) { return identify_udev_device(); } return identify_disks(&ctx); } azure-vm-utils-0.8.0/src/nvme.c000066400000000000000000000127011527236310300163260ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #include #include #include #include #include #include #include #include #include "checked.h" #include "debug.h" #include "exit_code.h" #include "nvme.h" /** * Query the kernel for a namespace device's namespace ID. * * Exits with: * - EXIT_CODE_NVME_DEVICE_OPEN when opening the device fails. * - EXIT_CODE_NVME_NAMESPACE_ID_IOCTL when the namespace ID ioctl fails. * * @return Namespace ID. */ int get_nsid_from_namespace_device_path(const char *namespace_path) { int fd = open(namespace_path, O_RDONLY); if (fd < 0) { fprintf(stderr, "failed to open %s: %m\n", namespace_path); exit(EXIT_CODE_NVME_DEVICE_OPEN); } int nsid = ioctl(fd, NVME_IOCTL_ID); if (nsid < 0) { fprintf(stderr, "failed NVME_IOCTL_ID ioctl for %s: %m\n", namespace_path); close(fd); exit(EXIT_CODE_NVME_NAMESPACE_ID_IOCTL); } close(fd); return nsid; } /** * Execute identify namespace command. * * Exits with: * - EXIT_CODE_NVME_DEVICE_OPEN when opening the device fails. * - EXIT_CODE_NVME_PAGE_SIZE when the page-size lookup fails. * - EXIT_CODE_NO_MEMORY when allocating the namespace buffer fails. * - EXIT_CODE_NVME_IDENTIFY_NAMESPACE_IOCTL when the Identify Namespace ioctl fails. * - EXIT_CODE_NVME_IDENTIFY_NAMESPACE_STATUS when Identify Namespace returns a nonzero NVMe status. * * @param device_path Path to the NVMe device, either controller or namespace. * @param nsid Namespace ID to identify. * * @return Pointer to the namespace structure. * */ struct nvme_id_ns *nvme_identify_namespace(const char *device_path, int nsid) { int fd = open(device_path, O_RDONLY); if (fd < 0) { fprintf(stderr, "failed to open %s: %m\n", device_path); exit(EXIT_CODE_NVME_DEVICE_OPEN); } long pagesize = sysconf(_SC_PAGESIZE); if (pagesize < 0) { fprintf(stderr, "failed sysconf: %m\n"); close(fd); exit(EXIT_CODE_NVME_PAGE_SIZE); } struct nvme_id_ns *ns = NULL; int error = posix_memalign((void **)&ns, pagesize, sizeof(struct nvme_id_ns)); if (error != 0) { fprintf(stderr, "failed posix_memalign for %s: %s\n", device_path, strerror(error)); exit(EXIT_CODE_NO_MEMORY); } struct nvme_admin_cmd cmd = { .opcode = 0x06, // Identify namespace command .nsid = nsid, .addr = (unsigned long)ns, .data_len = sizeof(*ns), }; int status = ioctl(fd, NVME_IOCTL_ADMIN_CMD, &cmd); if (status != 0) { if (status < 0) { fprintf(stderr, "failed NVME_IOCTL_ADMIN_CMD ioctl for %s: %m\n", device_path); } else { fprintf(stderr, "failed NVME_IOCTL_ADMIN_CMD ioctl for %s: NVMe status %#x\n", device_path, status); } free(ns); close(fd); exit(status > 0 ? EXIT_CODE_NVME_IDENTIFY_NAMESPACE_STATUS : EXIT_CODE_NVME_IDENTIFY_NAMESPACE_IOCTL); } close(fd); return ns; } /** * Query the NVMe namespace for the vendor specific data. * * For Azure devices, the vendor-specific data is current exposed as a string. * It will include the type of device and various properties in the format: * key1=value1,key2=value2,...\0. * * Anything beyond the terminating null-byte is currently undefined and * consequently ignored. * * Exits with: * - EXIT_CODE_NVME_DEVICE_OPEN when opening the device fails. * - EXIT_CODE_NVME_PAGE_SIZE when the page-size lookup fails. * - EXIT_CODE_NO_MEMORY when allocating the namespace buffer or vendor-specific string fails. * - EXIT_CODE_NVME_IDENTIFY_NAMESPACE_IOCTL when the Identify Namespace ioctl fails. * - EXIT_CODE_NVME_IDENTIFY_NAMESPACE_STATUS when Identify Namespace returns a nonzero NVMe status. * * @param device_path Path to the NVMe device, either controller or namespace. * @param nsid Namespace ID to identify. * * @return Vendor specific data string. */ char *nvme_identify_namespace_vs(const char *device_path, int nsid) { DEBUG_PRINTF("identifying namespace id=%d for device=%s...\n", nsid, device_path); struct nvme_id_ns *ns = nvme_identify_namespace(device_path, nsid); char *vs = strndup_checked((const char *)ns->vs, sizeof(ns->vs)); free(ns); return vs; } /** * Query the NVMe namespace for the vendor specific data. * * This a helper for nvme_identify_namespace_vs that takes a namespace device * path and queries the kernel for its namespace ID. * * Exits with: * - EXIT_CODE_NVME_DEVICE_OPEN when opening the device fails. * - EXIT_CODE_NVME_NAMESPACE_ID_IOCTL when the namespace ID ioctl fails. * - EXIT_CODE_NVME_PAGE_SIZE when the page-size lookup fails. * - EXIT_CODE_NO_MEMORY when allocating the namespace buffer or vendor-specific string fails. * - EXIT_CODE_NVME_IDENTIFY_NAMESPACE_IOCTL when the Identify Namespace ioctl fails. * - EXIT_CODE_NVME_IDENTIFY_NAMESPACE_STATUS when Identify Namespace returns a nonzero NVMe status. * * @param namespace_path Path to the NVMe namespace device. * * @return Vendor specific data string. */ char *nvme_identify_namespace_vs_for_namespace_device(const char *namespace_path) { int nsid = get_nsid_from_namespace_device_path(namespace_path); return nvme_identify_namespace_vs(namespace_path, nsid); } azure-vm-utils-0.8.0/src/nvme.h000066400000000000000000000025171527236310300163370ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #ifndef __NVME_H__ #define __NVME_H__ #include struct nvme_lbaf { __le16 ms; __u8 ds; __u8 rp; }; struct nvme_id_ns { __le64 nsze; __le64 ncap; __le64 nuse; __u8 nsfeat; __u8 nlbaf; __u8 flbas; __u8 mc; __u8 dpc; __u8 dps; __u8 nmic; __u8 rescap; __u8 fpi; __u8 dlfeat; __le16 nawun; __le16 nawupf; __le16 nacwu; __le16 nabsn; __le16 nabo; __le16 nabspf; __le16 noiob; __u8 nvmcap[16]; __le16 npwg; __le16 npwa; __le16 npdg; __le16 npda; __le16 nows; __u8 rsvd74[18]; __le32 anagrpid; __u8 rsvd96[3]; __u8 nsattr; __le16 nvmsetid; __le16 endgid; __u8 nguid[16]; __u8 eui64[8]; struct nvme_lbaf lbaf[16]; __u8 rsvd192[192]; __u8 vs[3712]; }; #define NVME_ADMIN_IDENTFY_NAMESPACE_OPCODE 0x06 struct nvme_id_ns *nvme_identify_namespace(const char *device_path, int nsid); char *nvme_identify_namespace_vs(const char *device_path, int nsid); int get_nsid_from_namespace_device_path(const char *namespace_path); char *nvme_identify_namespace_vs_for_namespace_device(const char *namespace_path); #endif // __NVME_H__ azure-vm-utils-0.8.0/src/version.h.in000066400000000000000000000003701527236310300174570ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license information. */ #ifndef VERSION_H #define VERSION_H #define VERSION "@VERSION@" #endif // VERSION_H azure-vm-utils-0.8.0/tests/000077500000000000000000000000001527236310300155675ustar00rootroot00000000000000azure-vm-utils-0.8.0/tests/capture.c000066400000000000000000000104731527236310300174030ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #include #include #include #include #include #include #include #include #include #include // clange-format off #include // clange-format on #include "capture.h" static int stdout_fd = -1; static int stderr_fd = -1; static int stdout_pipe[2] = {-1, -1}; static int stderr_pipe[2] = {-1, -1}; #define BUFFER_SIZE 128 * 1024 char capture_stdout_buffer[BUFFER_SIZE]; char capture_stderr_buffer[BUFFER_SIZE]; /** * Bypass potentially mocked close using dlsym(). */ static int (*real_close)(int) = NULL; static int capture_close(int fd) { if (!real_close) { real_close = (int (*)(int))dlsym(RTLD_NEXT, "close"); } return real_close(fd); } pid_t capture_fork(void) { assert_int_equal(fflush(NULL), 0); pid_t child = fork(); assert_true(child >= 0); return child; } void capture_wait(pid_t child, int expected_status) { int status; pid_t result; do { result = waitpid(child, &status, 0); } while (result == -1 && errno == EINTR); assert_int_equal(result, child); assert_true(WIFEXITED(status)); assert_int_equal(WEXITSTATUS(status), expected_status); } /** * Capture stdout and stderr using non-blocking pipes. */ int capture_setup(void **state) { (void)state; // Unused parameter memset(capture_stdout_buffer, 0, BUFFER_SIZE); memset(capture_stderr_buffer, 0, BUFFER_SIZE); if (pipe(stdout_pipe) == -1 || pipe(stderr_pipe) == -1) { perror("pipe failed"); exit(EXIT_FAILURE); } stdout_fd = dup(STDOUT_FILENO); stderr_fd = dup(STDERR_FILENO); if (stdout_fd == -1 || stderr_fd == -1) { perror("dup failed"); exit(EXIT_FAILURE); } if (dup2(stdout_pipe[1], STDOUT_FILENO) == -1 || dup2(stderr_pipe[1], STDERR_FILENO) == -1) { perror("dup2 failed"); exit(EXIT_FAILURE); } capture_close(stdout_pipe[1]); capture_close(stderr_pipe[1]); // Set pipes to non-blocking mode fcntl(stdout_pipe[0], F_SETFL, O_NONBLOCK); fcntl(stderr_pipe[0], F_SETFL, O_NONBLOCK); return 0; } /** * Synchronize captured stdout and return buffer. */ const char *capture_stdout() { if (stdout_pipe[0] != -1) { fflush(stdout); // Read data from stdout pipe ssize_t bytes_read; size_t offset = strlen(capture_stdout_buffer); while ((bytes_read = read(stdout_pipe[0], capture_stdout_buffer + offset, BUFFER_SIZE - 1 - offset)) > 0) { offset += bytes_read; if (offset >= BUFFER_SIZE - 1) { break; } } capture_stdout_buffer[offset] = '\0'; } return capture_stdout_buffer; } /** * Synchronize captured stderr and return buffer. */ const char *capture_stderr() { if (stderr_pipe[0] != -1) { fflush(stderr); ssize_t bytes_read; size_t offset = strlen(capture_stderr_buffer); while ((bytes_read = read(stderr_pipe[0], capture_stderr_buffer + offset, BUFFER_SIZE - 1 - offset)) > 0) { offset += bytes_read; if (offset >= BUFFER_SIZE - 1) { break; } } capture_stderr_buffer[offset] = '\0'; } return capture_stderr_buffer; } /** * Restore stdout and stderr after syncing final time. */ int capture_teardown(void **state) { (void)state; // Unused parameter if (stdout_fd != -1) { dup2(stdout_fd, STDOUT_FILENO); capture_close(stdout_fd); stdout_fd = -1; } if (stderr_fd != -1) { dup2(stderr_fd, STDERR_FILENO); capture_close(stderr_fd); stderr_fd = -1; } if (stdout_pipe[0] != -1) { capture_stdout(); capture_close(stdout_pipe[0]); stdout_pipe[0] = -1; } if (stderr_pipe[0] != -1) { capture_stderr(); capture_close(stderr_pipe[0]); stderr_pipe[0] = -1; } fprintf(stderr, ">> TEST STDOUT: %s\n", capture_stdout_buffer); fprintf(stderr, ">> TEST STDERR: %s\n", capture_stderr_buffer); return 0; } azure-vm-utils-0.8.0/tests/capture.h000066400000000000000000000007231527236310300174050ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #ifndef __CAPTURE_H__ #define __CAPTURE_H__ #include pid_t capture_fork(void); void capture_wait(pid_t child, int expected_status); int capture_setup(void **state); int capture_teardown(void **state); const char *capture_stdout(); const char *capture_stderr(); #endif // __CAPTURE_H__ azure-vm-utils-0.8.0/tests/checked_tests.c000066400000000000000000000122021527236310300205400ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #include #include #include #include #include #include // clang-format off #include // clang-format on #include "capture.h" #include "checked.h" #include "exit_code.h" int __wrap_json_object_array_add(json_object *obj, json_object *value) { (void)obj; (void)value; errno = ENOMEM; return -1; } json_object *__wrap_json_object_new_array(void) { errno = ENOMEM; return NULL; } json_object *__wrap_json_object_new_int(int value) { (void)value; errno = ENOMEM; return NULL; } json_object *__wrap_json_object_new_object(void) { errno = ENOMEM; return NULL; } json_object *__wrap_json_object_new_string(const char *value) { (void)value; errno = ENOMEM; return NULL; } int __wrap_json_object_object_add(json_object *obj, const char *key, json_object *value) { (void)obj; (void)key; (void)value; errno = ENOMEM; return -1; } const char *__wrap_json_object_to_json_string_ext(json_object *obj, int flags) { (void)obj; (void)flags; errno = ENOMEM; return NULL; } char *__wrap_strdup(const char *str) { (void)str; errno = ENOMEM; return NULL; } char *__wrap_strndup(const char *str, size_t length) { (void)str; (void)length; errno = ENOMEM; return NULL; } static void test_json_object_array_add_checked_failure(void **state) { (void)state; pid_t child = capture_fork(); if (child == 0) { json_object_array_add_checked(NULL, NULL); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NO_MEMORY); } static void test_json_object_new_array_checked_failure(void **state) { (void)state; pid_t child = capture_fork(); if (child == 0) { json_object_new_array_checked(); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NO_MEMORY); } static void test_json_object_new_int_checked_failure(void **state) { (void)state; pid_t child = capture_fork(); if (child == 0) { json_object_new_int_checked(1); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NO_MEMORY); } static void test_json_object_new_object_checked_failure(void **state) { (void)state; pid_t child = capture_fork(); if (child == 0) { json_object_new_object_checked(); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NO_MEMORY); } static void test_json_object_new_string_checked_failure(void **state) { (void)state; pid_t child = capture_fork(); if (child == 0) { json_object_new_string_checked("test"); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NO_MEMORY); } static void test_json_object_object_add_checked_failure(void **state) { (void)state; pid_t child = capture_fork(); if (child == 0) { json_object_object_add_checked(NULL, "key", NULL); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NO_MEMORY); } static void test_json_object_to_json_string_ext_checked_failure(void **state) { (void)state; pid_t child = capture_fork(); if (child == 0) { json_object_to_json_string_ext_checked(NULL, 0); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NO_MEMORY); } static void test_strdup_checked_failure(void **state) { (void)state; pid_t child = capture_fork(); if (child == 0) { strdup_checked("test"); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NO_MEMORY); } static void test_strndup_checked_failure(void **state) { (void)state; pid_t child = capture_fork(); if (child == 0) { strndup_checked("test", 4); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NO_MEMORY); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test_setup_teardown(test_json_object_array_add_checked_failure, capture_setup, capture_teardown), cmocka_unit_test_setup_teardown(test_json_object_new_array_checked_failure, capture_setup, capture_teardown), cmocka_unit_test_setup_teardown(test_json_object_new_int_checked_failure, capture_setup, capture_teardown), cmocka_unit_test_setup_teardown(test_json_object_new_object_checked_failure, capture_setup, capture_teardown), cmocka_unit_test_setup_teardown(test_json_object_new_string_checked_failure, capture_setup, capture_teardown), cmocka_unit_test_setup_teardown(test_json_object_object_add_checked_failure, capture_setup, capture_teardown), cmocka_unit_test_setup_teardown(test_json_object_to_json_string_ext_checked_failure, capture_setup, capture_teardown), cmocka_unit_test_setup_teardown(test_strdup_checked_failure, capture_setup, capture_teardown), cmocka_unit_test_setup_teardown(test_strndup_checked_failure, capture_setup, capture_teardown), }; return cmocka_run_group_tests(tests, NULL, NULL); } azure-vm-utils-0.8.0/tests/debug_tests.c000066400000000000000000000040601527236310300202430ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #include #include #include #include #include #include #include #include // clange-format off #include // clange-format on #include "capture.h" #include "debug.h" extern char **environ; static const char *environ_test[] = { "ENV1=VALUE1", "ENV2=VALUE2", "ENV3=VALUE3", NULL, }; static char **original_environ = NULL; static int setup(void **state) { (void)state; // Unused parameter original_environ = environ; environ = (char **)environ_test; capture_setup(state); return 0; } static int teardown(void **state) { (void)state; // Unused parameter environ = original_environ; debug = false; capture_teardown(state); return 0; } static void test_debug_environment_variables_with_debug_enabled(void **state) { (void)state; // Unused parameter debug = true; const char *expected_output = "DEBUG: Environment Variables:\n" "DEBUG: ENV1=VALUE1\n" "DEBUG: ENV2=VALUE2\n" "DEBUG: ENV3=VALUE3\n"; debug_environment_variables(); assert_string_equal(capture_stderr(), expected_output); assert_string_equal(capture_stdout(), ""); } static void test_debug_environment_variables_with_debug_disabled(void **state) { (void)state; // Unused parameter debug = false; debug_environment_variables(); assert_string_equal(capture_stderr(), ""); assert_string_equal(capture_stdout(), ""); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test_setup_teardown(test_debug_environment_variables_with_debug_enabled, setup, teardown), cmocka_unit_test_setup_teardown(test_debug_environment_variables_with_debug_disabled, setup, teardown), }; return cmocka_run_group_tests(tests, NULL, NULL); } azure-vm-utils-0.8.0/tests/identify_disks_tests.c000066400000000000000000001064711527236310300221760ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include // clang-format off #include // clang-format on #include "capture.h" #include "exit_code.h" #include "identify_disks.h" #define MICROSOFT_NVME_DIRECT_DISK_V1 "Microsoft NVMe Direct Disk \n" #define MICROSOFT_NVME_DIRECT_DISK_V2 "Microsoft NVMe Direct Disk v2 \n" #define MSFT_NVME_ACCELERATOR_MODEL_V1 "MSFT NVMe Accelerator v1.0 \n" char *fake_sys_class_nvme_path = "/sys/class/nvme"; bool compare_json_strings(const char *json_str1, const char *json_str2); int __wrap_get_nsid_from_namespace_device_path(const char *namespace_path) { check_expected_ptr(namespace_path); return mock_type(int); } char *__wrap_nvme_identify_namespace_vs(const char *namespace_path, int nsid) { check_expected_ptr(namespace_path); check_expected(nsid); return mock_ptr_type(char *); } static void expect_namespace_vs(const char *path, int nsid, char *vs) { expect_string(__wrap_get_nsid_from_namespace_device_path, namespace_path, path); will_return(__wrap_get_nsid_from_namespace_device_path, nsid); expect_string(__wrap_nvme_identify_namespace_vs, namespace_path, path); expect_value(__wrap_nvme_identify_namespace_vs, nsid, nsid); will_return(__wrap_nvme_identify_namespace_vs, vs); } static void remove_temp_dir(const char *path) { struct dirent *entry; DIR *dir = opendir(path); if (dir == NULL) { return; } while ((entry = readdir(dir)) != NULL) { char full_path[MAX_PATH]; if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name); if (entry->d_type == DT_DIR) { remove_temp_dir(full_path); } else { remove(full_path); } } closedir(dir); rmdir(path); } static void create_intermediate_dirs(const char *path) { char temp_path[MAX_PATH]; snprintf(temp_path, sizeof(temp_path), "%s", path); char *p = temp_path; while (*p) { if (*p == '/') { *p = '\0'; mkdir(temp_path, 0755); *p = '/'; } p++; } } static void create_dir(const char *base_path, const char *sub_path) { char full_path[MAX_PATH]; snprintf(full_path, sizeof(full_path), "%s/%s", base_path, sub_path); create_intermediate_dirs(full_path); mkdir(full_path, 0755); } static void create_file(const char *base_path, const char *sub_path, const char *content) { char full_path[MAX_PATH]; snprintf(full_path, sizeof(full_path), "%s/%s", base_path, sub_path); create_intermediate_dirs(full_path); FILE *file = fopen(full_path, "w"); if (file == NULL) { perror("fopen"); return; } fputs(content, file); fclose(file); } static int setup(void **state) { capture_setup(state); char template[] = "/tmp/nvme_test_XXXXXX"; char *temp_path = mkdtemp(template); if (temp_path == NULL) { perror("mkdtemp"); return -1; } *state = strdup(temp_path); fake_sys_class_nvme_path = *state; return 0; } static int teardown(void **state) { capture_teardown(state); if (*state != NULL) { remove_temp_dir(*state); free(*state); *state = NULL; } fake_sys_class_nvme_path = SYS_CLASS_NVME_PATH; return 0; } /** * Setup nvme0: microsoft disk controler, no namespaces. */ static void _setup_nvme0_microsoft_no_name_namespaces(void) { create_file(fake_sys_class_nvme_path, "nvme0/device/vendor", "0x1414"); create_file(fake_sys_class_nvme_path, "nvme0/model", "Unknown model"); // nothing to expect as no namespaces are iterated through. } /** * Setup nvme1: microsoft disk, one namespace. */ static void _setup_nvme1_microsoft_one_namespace(void) { create_file(fake_sys_class_nvme_path, "nvme1/device/vendor", "0x1414"); create_file(fake_sys_class_nvme_path, "nvme1/model", "Unknown model"); create_dir(fake_sys_class_nvme_path, "nvme1/nvme1n1"); expect_namespace_vs("/dev/nvme1n1", 1, strdup("key1=nvme1n1value1,key2=nvme1n1value2")); } /** * Setup nvme2: microsoft disk, two namespaces. */ static void _setup_nvme2_microsoft_two_namespaces(void) { create_file(fake_sys_class_nvme_path, "nvme2/device/vendor", "0x1414"); create_file(fake_sys_class_nvme_path, "nvme2/model", "Unknown model"); create_dir(fake_sys_class_nvme_path, "nvme2/nvme2n1"); create_dir(fake_sys_class_nvme_path, "nvme2/nvme2n2"); expect_namespace_vs("/dev/nvme2n1", 1, strdup("key1=nvme2n1value1,key2=nvme2n1value2")); expect_namespace_vs("/dev/nvme2n2", 2, strdup("key1=nvme2n2value1,key2=nvme2n2value2")); } // No nvme3. /** * Setup nvme4: non-microsoft disk. */ static void _setup_nvme4_non_microsoft(void) { create_file(fake_sys_class_nvme_path, "nvme4/device/vendor", "0x0000"); create_file(fake_sys_class_nvme_path, "nvme4/model", "Unknown model"); create_dir(fake_sys_class_nvme_path, "nvme4/nvme4n1"); create_dir(fake_sys_class_nvme_path, "nvme4/nvme4n2"); } /** * Setup nvme5: microsoft disk, empty vs for nsid=2. */ static void _setup_nvme5_microsoft_mixed_namespaces(void) { create_file(fake_sys_class_nvme_path, "nvme5/device/vendor", "0x1414"); create_file(fake_sys_class_nvme_path, "nvme5/model", "Unknown model"); create_dir(fake_sys_class_nvme_path, "nvme5/nvme5n1"); create_dir(fake_sys_class_nvme_path, "nvme5/nvme5n2"); create_dir(fake_sys_class_nvme_path, "nvme5/nvme5n4"); create_dir(fake_sys_class_nvme_path, "nvme5/nvme5n32"); create_dir(fake_sys_class_nvme_path, "nvme5/nvme5n315"); expect_namespace_vs("/dev/nvme5n1", 1, strdup("key1=nvme5n1value1,key2=nvme5n1value2")); expect_namespace_vs("/dev/nvme5n2", 2, strdup("")); expect_namespace_vs("/dev/nvme5n4", 4, strdup("key1=nvme5n4value1,key2=nvme5n4value2")); expect_namespace_vs("/dev/nvme5n32", 32, strdup("key1=nvme5n32value1")); expect_namespace_vs("/dev/nvme5n315", 315, strdup("key1=nvme5n315value1")); } /** * Setup nvme6: remote accelerator v1 with vs. */ static void _setup_nvme6_remote_accelerator_v1_with_vs(void) { create_file(fake_sys_class_nvme_path, "nvme6/device/vendor", "0x1414"); create_dir(fake_sys_class_nvme_path, "nvme6/nvme6n1"); create_file(fake_sys_class_nvme_path, "nvme6/model", MSFT_NVME_ACCELERATOR_MODEL_V1); expect_namespace_vs("/dev/nvme6n1", 1, strdup("key1=nvme6n1value1,key2=nvme6n1value2")); } /** * Setup nvme7: remote accelerator v1 without vs. */ static void _setup_nvme7_remote_accelerator_v1_without_vs(void) { create_file(fake_sys_class_nvme_path, "nvme7/device/vendor", "0x1414"); create_file(fake_sys_class_nvme_path, "nvme7/model", MSFT_NVME_ACCELERATOR_MODEL_V1); create_dir(fake_sys_class_nvme_path, "nvme7/nvme7n1"); create_dir(fake_sys_class_nvme_path, "nvme7/nvme7n2"); create_dir(fake_sys_class_nvme_path, "nvme7/nvme7n3"); create_dir(fake_sys_class_nvme_path, "nvme7/nvme7n4"); create_dir(fake_sys_class_nvme_path, "nvme7/nvme7n9"); expect_namespace_vs("/dev/nvme7n1", 1, strdup("")); expect_namespace_vs("/dev/nvme7n2", 2, strdup("")); expect_namespace_vs("/dev/nvme7n3", 3, strdup("")); expect_namespace_vs("/dev/nvme7n4", 4, strdup("")); expect_namespace_vs("/dev/nvme7n9", 9, strdup("")); } /** * Setup nvme8: direct disk v1 which doesn't have vs support. */ static void _setup_nvme8_direct_disk_v1_without_vs(void) { create_file(fake_sys_class_nvme_path, "nvme8/device/vendor", "0x1414"); create_file(fake_sys_class_nvme_path, "nvme8/model", MICROSOFT_NVME_DIRECT_DISK_V1); create_dir(fake_sys_class_nvme_path, "nvme8/nvme8n1"); expect_namespace_vs("/dev/nvme8n1", 1, strdup("")); } /** * Setup nvme9: direct disk v2 which has vs support. */ static void _setup_nvme9_direct_disk_v2(void) { create_file(fake_sys_class_nvme_path, "nvme9/device/vendor", "0x1414"); create_file(fake_sys_class_nvme_path, "nvme9/model", MICROSOFT_NVME_DIRECT_DISK_V2); create_dir(fake_sys_class_nvme_path, "nvme9/nvme9n1"); create_dir(fake_sys_class_nvme_path, "nvme9/nvme9n2"); expect_namespace_vs("/dev/nvme9n1", 1, strdup("type=local,index=0,name=nvme-500G-0")); expect_namespace_vs("/dev/nvme9n2", 2, strdup("type=local,index=1,name=nvme-500G-1")); } /** * Setup nvme10: direct disk v2 which is missing vs support (unexpected case). */ static void _setup_nvme10_direct_disk_v2_missing_vs(void) { create_file(fake_sys_class_nvme_path, "nvme10/device/vendor", "0x1414"); create_file(fake_sys_class_nvme_path, "nvme10/model", MICROSOFT_NVME_DIRECT_DISK_V2); create_dir(fake_sys_class_nvme_path, "nvme10/nvme10n1"); expect_namespace_vs("/dev/nvme10n1", 1, strdup("")); } /** * Setup nvme11: disk with non-integer lun and index (unexpected case). */ static void _setup_nvme11_non_integer_lun_and_index(void) { create_file(fake_sys_class_nvme_path, "nvme11/device/vendor", "0x1414"); create_file(fake_sys_class_nvme_path, "nvme11/model", "Unknown model"); create_dir(fake_sys_class_nvme_path, "nvme11/nvme11n1"); expect_namespace_vs("/dev/nvme11n1", 1, strdup("type=local,index=foo,lun=bar")); } static void test_trim_trailing_whitespace(void **state) { (void)state; // Unused parameter struct { char input[MAX_PATH]; char expected[MAX_PATH]; } test_cases[] = {{"NoTrailingWhitespace", "NoTrailingWhitespace"}, {"TrailingSpaces ", "TrailingSpaces"}, {"TrailingTabs\t\t\t", "TrailingTabs"}, {"TrailingNewline\n", "TrailingNewline"}, {"TrailingMixed \t\n", "TrailingMixed"}, {"", ""}, {"\0", "\0"}}; for (size_t i = 0; i < sizeof(test_cases) / sizeof(test_cases[0]); i++) { trim_trailing_whitespace(test_cases[i].input); assert_string_equal(test_cases[i].input, test_cases[i].expected); } } static void test_identify_namespace_without_vs_missing_model(void **state) { (void)state; char *result = identify_namespace_without_vs(fake_sys_class_nvme_path, 1); assert_string_equal(result, ""); free(result); char expected[MAX_PATH + 128]; snprintf(expected, sizeof(expected), "failed to open %s/model: %s\n", fake_sys_class_nvme_path, strerror(ENOENT)); assert_string_equal(capture_stderr(), expected); assert_string_equal(capture_stdout(), ""); } static void test_identify_namespace_without_vs_model_read_error(void **state) { (void)state; create_dir(fake_sys_class_nvme_path, "model"); char *result = identify_namespace_without_vs(fake_sys_class_nvme_path, 1); assert_string_equal(result, ""); free(result); char expected[MAX_PATH + 128]; snprintf(expected, sizeof(expected), "failed to read model name from %s/model: %s\n", fake_sys_class_nvme_path, strerror(EISDIR)); assert_string_equal(capture_stderr(), expected); assert_string_equal(capture_stdout(), ""); } static void test_identify_namespace_without_vs_empty_model(void **state) { (void)state; create_file(fake_sys_class_nvme_path, "model", ""); char *result = identify_namespace_without_vs(fake_sys_class_nvme_path, 1); assert_string_equal(result, ""); free(result); char expected[MAX_PATH + 128]; snprintf(expected, sizeof(expected), "failed to read model name from %s/model: unexpected end of file\n", fake_sys_class_nvme_path); assert_string_equal(capture_stderr(), expected); assert_string_equal(capture_stdout(), ""); } static void test_identify_disks_vendor_failures(void **state) { (void)state; create_dir(fake_sys_class_nvme_path, "nvme0"); create_file(fake_sys_class_nvme_path, "nvme2/device/vendor", "invalid"); create_file(fake_sys_class_nvme_path, "nvme3/device/vendor", ""); create_dir(fake_sys_class_nvme_path, "nvme4/device/vendor"); _setup_nvme1_microsoft_one_namespace(); struct context ctx = {.output_format = PLAIN}; assert_int_equal(identify_disks(&ctx), EXIT_CODE_SUCCESS); char expected[4 * MAX_PATH + 512]; snprintf(expected, sizeof(expected), "failed to open %s/nvme0/device/vendor: %s\n" "failed to parse vendor ID from %s/nvme2/device/vendor: expected a hexadecimal vendor ID\n" "failed to parse vendor ID from %s/nvme3/device/vendor: expected a hexadecimal vendor ID\n" "failed to read vendor ID from %s/nvme4/device/vendor: %s\n", fake_sys_class_nvme_path, strerror(ENOENT), fake_sys_class_nvme_path, fake_sys_class_nvme_path, fake_sys_class_nvme_path, strerror(EISDIR)); const char *stderr_output = capture_stderr(); /* scandir applies its filter before sorting, so diagnostic order is unspecified. */ char *saveptr; for (char *line = strtok_r(expected, "\n", &saveptr); line != NULL; line = strtok_r(NULL, "\n", &saveptr)) { assert_non_null(strstr(stderr_output, line)); } assert_string_equal(capture_stdout(), "/dev/nvme1n1: key1=nvme1n1value1,key2=nvme1n1value2\n"); } static void test_identify_disks_no_sys_class_nvme_present(void **state) { (void)state; // Unused parameter char expected_string[1024]; snprintf(expected_string, sizeof(expected_string), "no NVMe devices in %s: No such file or directory\n", fake_sys_class_nvme_path); remove_temp_dir(fake_sys_class_nvme_path); struct context ctx = {.output_format = PLAIN}; int result = identify_disks(&ctx); assert_int_equal(result, EXIT_CODE_SUCCESS); assert_string_equal(capture_stderr(), expected_string); assert_string_equal(capture_stdout(), ""); } static void test_identify_disks_sys_class_nvme_empty(void **state) { (void)state; // Unused parameter struct context ctx = {.output_format = PLAIN}; int result = identify_disks(&ctx); assert_int_equal(result, EXIT_CODE_SUCCESS); assert_string_equal(capture_stderr(), ""); assert_string_equal(capture_stdout(), ""); } static void test_identify_disks_sys_class_nvme_empty_json(void **state) { (void)state; struct context ctx = {.output_format = JSON}; assert_int_equal(identify_disks(&ctx), EXIT_CODE_SUCCESS); assert_string_equal(capture_stderr(), ""); assert_true(compare_json_strings(capture_stdout(), "[]")); } static void test_identify_disks(void **state) { struct { const char *name; void (*setup_func)(void); const char *expected_stderr; const char *expected_stdout; } test_cases[] = { {"no namespaces", NULL, "", ""}, {"nvme0", _setup_nvme0_microsoft_no_name_namespaces, "", ""}, {"nvme1", _setup_nvme1_microsoft_one_namespace, "", "/dev/nvme1n1: key1=nvme1n1value1,key2=nvme1n1value2\n"}, {"nvme2", _setup_nvme2_microsoft_two_namespaces, "", "/dev/nvme2n1: key1=nvme2n1value1,key2=nvme2n1value2\n" "/dev/nvme2n2: key1=nvme2n2value1,key2=nvme2n2value2\n"}, {"nvme4", _setup_nvme4_non_microsoft, "", ""}, {"nvme5", _setup_nvme5_microsoft_mixed_namespaces, "", "/dev/nvme5n1: key1=nvme5n1value1,key2=nvme5n1value2\n" "/dev/nvme5n2: \n" "/dev/nvme5n4: key1=nvme5n4value1,key2=nvme5n4value2\n" "/dev/nvme5n32: key1=nvme5n32value1\n" "/dev/nvme5n315: key1=nvme5n315value1\n"}, {"nvme6", _setup_nvme6_remote_accelerator_v1_with_vs, "", "/dev/nvme6n1: key1=nvme6n1value1,key2=nvme6n1value2\n"}, {"nvme7", _setup_nvme7_remote_accelerator_v1_without_vs, "", "/dev/nvme7n1: type=os\n" "/dev/nvme7n2: type=data,lun=0\n" "/dev/nvme7n3: type=data,lun=1\n" "/dev/nvme7n4: type=data,lun=2\n" "/dev/nvme7n9: type=data,lun=7\n"}, {"nvme8", _setup_nvme8_direct_disk_v1_without_vs, "", "/dev/nvme8n1: type=local\n"}, {"nvme9", _setup_nvme9_direct_disk_v2, "", "/dev/nvme9n1: type=local,index=0,name=nvme-500G-0\n" "/dev/nvme9n2: type=local,index=1,name=nvme-500G-1\n"}, {"nvme10", _setup_nvme10_direct_disk_v2_missing_vs, "", "/dev/nvme10n1: type=local\n"}, {"nvme11", _setup_nvme11_non_integer_lun_and_index, "failed to parse vs=type=local,index=foo,lun=bar key=index value=foo as int\n" "failed to parse vs=type=local,index=foo,lun=bar key=lun value=bar as int\n", "/dev/nvme11n1: type=local,index=foo,lun=bar\n"}, }; for (size_t i = 0; i < sizeof(test_cases) / sizeof(test_cases[0]); i++) { printf("Running test case: %s\n", test_cases[i].name); teardown(state); setup(state); if (test_cases[i].setup_func) test_cases[i].setup_func(); struct context ctx = {.output_format = PLAIN}; int result = identify_disks(&ctx); assert_int_equal(result, EXIT_CODE_SUCCESS); assert_string_equal(capture_stderr(), test_cases[i].expected_stderr); assert_string_equal(capture_stdout(), test_cases[i].expected_stdout); } } static void test_identify_disks_combined(void **state) { (void)state; // Unused parameter _setup_nvme0_microsoft_no_name_namespaces(); _setup_nvme1_microsoft_one_namespace(); _setup_nvme2_microsoft_two_namespaces(); _setup_nvme4_non_microsoft(); _setup_nvme5_microsoft_mixed_namespaces(); _setup_nvme6_remote_accelerator_v1_with_vs(); _setup_nvme7_remote_accelerator_v1_without_vs(); _setup_nvme8_direct_disk_v1_without_vs(); _setup_nvme9_direct_disk_v2(); _setup_nvme10_direct_disk_v2_missing_vs(); _setup_nvme11_non_integer_lun_and_index(); struct context ctx = {.output_format = PLAIN}; int result = identify_disks(&ctx); assert_int_equal(result, EXIT_CODE_SUCCESS); assert_string_equal(capture_stderr(), "failed to parse vs=type=local,index=foo,lun=bar key=index value=foo as int\n" "failed to parse vs=type=local,index=foo,lun=bar key=lun value=bar as int\n"); assert_string_equal(capture_stdout(), "/dev/nvme1n1: key1=nvme1n1value1,key2=nvme1n1value2\n" "/dev/nvme2n1: key1=nvme2n1value1,key2=nvme2n1value2\n" "/dev/nvme2n2: key1=nvme2n2value1,key2=nvme2n2value2\n" "/dev/nvme5n1: key1=nvme5n1value1,key2=nvme5n1value2\n" "/dev/nvme5n2: \n" "/dev/nvme5n4: key1=nvme5n4value1,key2=nvme5n4value2\n" "/dev/nvme5n32: key1=nvme5n32value1\n" "/dev/nvme5n315: key1=nvme5n315value1\n" "/dev/nvme6n1: key1=nvme6n1value1,key2=nvme6n1value2\n" "/dev/nvme7n1: type=os\n" "/dev/nvme7n2: type=data,lun=0\n" "/dev/nvme7n3: type=data,lun=1\n" "/dev/nvme7n4: type=data,lun=2\n" "/dev/nvme7n9: type=data,lun=7\n" "/dev/nvme8n1: type=local\n" "/dev/nvme9n1: type=local,index=0,name=nvme-500G-0\n" "/dev/nvme9n2: type=local,index=1,name=nvme-500G-1\n" "/dev/nvme10n1: type=local\n" "/dev/nvme11n1: type=local,index=foo,lun=bar\n"); } // clang-format off static const char *expected_combined_json_string = "[{\"path\":\"/dev/nvme1n1\",\"nsid\":1,\"model\":\"Unknown model\",\"properties\":{\"key1\":\"nvme1n1value1\",\"key2\":\"nvme1n1value2\"},\"vs\":\"key1=nvme1n1value1,key2=nvme1n1value2\"}," "{\"path\":\"/dev/nvme2n1\",\"nsid\":1,\"model\":\"Unknown model\",\"properties\":{\"key1\":\"nvme2n1value1\",\"key2\":\"nvme2n1value2\"},\"vs\":\"key1=nvme2n1value1,key2=nvme2n1value2\"}," "{\"path\":\"/dev/nvme2n2\",\"nsid\":2,\"model\":\"Unknown model\",\"properties\":{\"key1\":\"nvme2n2value1\",\"key2\":\"nvme2n2value2\"},\"vs\":\"key1=nvme2n2value1,key2=nvme2n2value2\"}," "{\"path\":\"/dev/nvme5n1\",\"nsid\":1,\"model\":\"Unknown model\",\"properties\":{\"key1\":\"nvme5n1value1\",\"key2\":\"nvme5n1value2\"},\"vs\":\"key1=nvme5n1value1,key2=nvme5n1value2\"}," "{\"path\":\"/dev/nvme5n2\",\"nsid\":2,\"model\":\"Unknown model\",\"properties\":{},\"vs\":\"\"}," "{\"path\":\"/dev/nvme5n4\",\"nsid\":4,\"model\":\"Unknown model\",\"properties\":{\"key1\":\"nvme5n4value1\",\"key2\":\"nvme5n4value2\"},\"vs\":\"key1=nvme5n4value1,key2=nvme5n4value2\"}," "{\"path\":\"/dev/nvme5n32\",\"nsid\":32,\"model\":\"Unknown model\",\"properties\":{\"key1\":\"nvme5n32value1\"},\"vs\":\"key1=nvme5n32value1\"}," "{\"path\":\"/dev/nvme5n315\",\"nsid\":315,\"model\":\"Unknown model\",\"properties\":{\"key1\":\"nvme5n315value1\"},\"vs\":\"key1=nvme5n315value1\"}," "{\"path\":\"/dev/nvme6n1\",\"nsid\":1,\"model\":\"MSFT NVMe Accelerator v1.0\",\"properties\":{\"key1\":\"nvme6n1value1\",\"key2\":\"nvme6n1value2\"},\"vs\":\"key1=nvme6n1value1,key2=nvme6n1value2\"}," "{\"path\":\"/dev/nvme7n1\",\"nsid\":1,\"model\":\"MSFT NVMe Accelerator v1.0\",\"properties\":{\"type\":\"os\"},\"vs\":\"\"}," "{\"path\":\"/dev/nvme7n2\",\"nsid\":2,\"model\":\"MSFT NVMe Accelerator v1.0\",\"properties\":{\"type\":\"data\",\"lun\":0},\"vs\":\"\"}," "{\"path\":\"/dev/nvme7n3\",\"nsid\":3,\"model\":\"MSFT NVMe Accelerator v1.0\",\"properties\":{\"type\":\"data\",\"lun\":1},\"vs\":\"\"}," "{\"path\":\"/dev/nvme7n4\",\"nsid\":4,\"model\":\"MSFT NVMe Accelerator v1.0\",\"properties\":{\"type\":\"data\",\"lun\":2},\"vs\":\"\"}," "{\"path\":\"/dev/nvme7n9\",\"nsid\":9,\"model\":\"MSFT NVMe Accelerator v1.0\",\"properties\":{\"type\":\"data\",\"lun\":7},\"vs\":\"\"}," "{\"path\":\"/dev/nvme8n1\",\"nsid\":1,\"model\":\"Microsoft NVMe Direct Disk\",\"properties\":{\"type\":\"local\"},\"vs\":\"\"}," "{\"path\":\"/dev/nvme9n1\",\"nsid\":1,\"model\":\"Microsoft NVMe Direct Disk v2\",\"properties\":{\"type\":\"local\",\"index\":0,\"name\":\"nvme-500G-0\"},\"vs\":\"type=local,index=0,name=nvme-500G-0\"}," "{\"path\":\"/dev/nvme9n2\",\"nsid\":2,\"model\":\"Microsoft NVMe Direct Disk v2\",\"properties\":{\"type\":\"local\",\"index\":1,\"name\":\"nvme-500G-1\"},\"vs\":\"type=local,index=1,name=nvme-500G-1\"}," "{\"path\":\"/dev/nvme10n1\",\"nsid\":1,\"model\":\"Microsoft NVMe Direct Disk v2\",\"properties\":{\"type\":\"local\"},\"vs\":\"\"}," "{\"path\":\"/dev/nvme11n1\",\"nsid\":1,\"model\":\"Unknown model\",\"properties\":{\"type\":\"local\",\"index\":\"foo\",\"lun\":\"bar\"},\"vs\":\"type=local,index=foo,lun=bar\"}]\n"; // clang-format on bool compare_json_strings(const char *json_str1, const char *json_str2) { // Parse the JSON strings into json_object structures json_object *json_obj1 = json_tokener_parse(json_str1); json_object *json_obj2 = json_tokener_parse(json_str2); // Check if both JSON objects are valid if (json_obj1 == NULL || json_obj2 == NULL) { if (json_obj1) json_object_put(json_obj1); if (json_obj2) json_object_put(json_obj2); return false; } // Compare the JSON objects bool are_equal = json_object_equal(json_obj1, json_obj2); // Free the JSON objects json_object_put(json_obj1); json_object_put(json_obj2); return are_equal; } static void test_identify_disks_combined_json(void **state) { (void)state; // Unused parameter _setup_nvme0_microsoft_no_name_namespaces(); _setup_nvme1_microsoft_one_namespace(); _setup_nvme2_microsoft_two_namespaces(); _setup_nvme4_non_microsoft(); _setup_nvme5_microsoft_mixed_namespaces(); _setup_nvme6_remote_accelerator_v1_with_vs(); _setup_nvme7_remote_accelerator_v1_without_vs(); _setup_nvme8_direct_disk_v1_without_vs(); _setup_nvme9_direct_disk_v2(); _setup_nvme10_direct_disk_v2_missing_vs(); _setup_nvme11_non_integer_lun_and_index(); struct context ctx = {.output_format = JSON}; int result = identify_disks(&ctx); assert_int_equal(result, EXIT_CODE_SUCCESS); assert_string_equal(capture_stderr(), "failed to parse vs=type=local,index=foo,lun=bar key=index value=foo as int\n" "failed to parse vs=type=local,index=foo,lun=bar key=lun value=bar as int\n"); const char *json_output = capture_stdout(); assert_true(compare_json_strings(json_output, expected_combined_json_string)); // Pretty print in all cases will have a newline after the leading bracket. assert_true(strncmp(json_output, "[\n", 2) == 0); } static void test_identify_disks_nonmatching_nsid(void **state) { (void)state; create_file(fake_sys_class_nvme_path, "nvme0/device/vendor", "0x1414"); create_file(fake_sys_class_nvme_path, "nvme0/model", MSFT_NVME_ACCELERATOR_MODEL_V1); create_file(fake_sys_class_nvme_path, "nvme1/device/vendor", "0x1414"); create_file(fake_sys_class_nvme_path, "nvme1/model", MSFT_NVME_ACCELERATOR_MODEL_V1); const struct { const char *path; const char *sys_path; int nsid; } test_cases[] = { {"/dev/nvme0n2", "nvme0/nvme0n2", 1}, {"/dev/nvme1n1", "nvme1/nvme1n1", 5}, {"/dev/nvme1n2", "nvme1/nvme1n2", 3}, {"/dev/nvme1n3", "nvme1/nvme1n3", 7}, {"/dev/nvme1n4", "nvme1/nvme1n4", 9}, }; for (size_t i = 0; i < sizeof(test_cases) / sizeof(test_cases[0]); i++) { create_dir(fake_sys_class_nvme_path, test_cases[i].sys_path); expect_namespace_vs(test_cases[i].path, test_cases[i].nsid, strdup("")); } struct context ctx = {.output_format = PLAIN}; assert_int_equal(identify_disks(&ctx), EXIT_CODE_SUCCESS); assert_string_equal(capture_stdout(), "/dev/nvme0n2: type=os\n" "/dev/nvme1n1: type=data,lun=3\n" "/dev/nvme1n2: type=data,lun=1\n" "/dev/nvme1n3: type=data,lun=5\n" "/dev/nvme1n4: type=data,lun=7\n"); assert_string_equal(capture_stderr(), ""); } static void test_identify_disks_nonmatching_nsid_json(void **state) { (void)state; create_file(fake_sys_class_nvme_path, "nvme0/device/vendor", "0x1414"); create_file(fake_sys_class_nvme_path, "nvme0/model", MSFT_NVME_ACCELERATOR_MODEL_V1); create_dir(fake_sys_class_nvme_path, "nvme0/nvme0n1"); expect_namespace_vs("/dev/nvme0n1", 9, strdup("")); struct context ctx = {.output_format = JSON}; assert_int_equal(identify_disks(&ctx), EXIT_CODE_SUCCESS); assert_true(compare_json_strings(capture_stdout(), "[{\"path\":\"/dev/nvme0n1\",\"nsid\":9,\"model\":\"MSFT NVMe Accelerator v1.0\"," "\"properties\":{\"type\":\"data\",\"lun\":7},\"vs\":\"\"}]")); assert_string_equal(capture_stderr(), ""); } static void test_identify_disks_controller_failure(void **state) { (void)state; create_file(fake_sys_class_nvme_path, "nvme0/device/vendor", "0x1414"); create_dir(fake_sys_class_nvme_path, "nvme0/nvme0n1"); _setup_nvme1_microsoft_one_namespace(); create_file(fake_sys_class_nvme_path, "nvme2/device/vendor", "0x1414"); create_file(fake_sys_class_nvme_path, "nvme2/model", ""); create_dir(fake_sys_class_nvme_path, "nvme2/nvme2n1"); _setup_nvme6_remote_accelerator_v1_with_vs(); struct context ctx = {.output_format = PLAIN}; assert_int_equal(identify_disks(&ctx), EXIT_CODE_SUCCESS); char expected[2 * MAX_PATH + 256]; snprintf(expected, sizeof(expected), "failed to open %s/nvme0/model: %s\n" "failed to initialize NVMe controller nvme0\n" "failed to read model name from %s/nvme2/model: unexpected end of file\n" "failed to initialize NVMe controller nvme2\n", fake_sys_class_nvme_path, strerror(ENOENT), fake_sys_class_nvme_path); assert_string_equal(capture_stderr(), expected); assert_string_equal(capture_stdout(), "/dev/nvme1n1: key1=nvme1n1value1,key2=nvme1n1value2\n" "/dev/nvme6n1: key1=nvme6n1value1,key2=nvme6n1value2\n"); } static void test_identify_disks_controller_failure_json(void **state) { (void)state; create_file(fake_sys_class_nvme_path, "nvme0/device/vendor", "0x1414"); create_dir(fake_sys_class_nvme_path, "nvme0/nvme0n1"); _setup_nvme1_microsoft_one_namespace(); create_file(fake_sys_class_nvme_path, "nvme2/device/vendor", "0x1414"); create_file(fake_sys_class_nvme_path, "nvme2/model", ""); create_dir(fake_sys_class_nvme_path, "nvme2/nvme2n1"); _setup_nvme6_remote_accelerator_v1_with_vs(); struct context ctx = {.output_format = JSON}; assert_int_equal(identify_disks(&ctx), EXIT_CODE_SUCCESS); char expected[2 * MAX_PATH + 256]; snprintf(expected, sizeof(expected), "failed to open %s/nvme0/model: %s\n" "failed to initialize NVMe controller nvme0\n" "failed to read model name from %s/nvme2/model: unexpected end of file\n" "failed to initialize NVMe controller nvme2\n", fake_sys_class_nvme_path, strerror(ENOENT), fake_sys_class_nvme_path); assert_string_equal(capture_stderr(), expected); assert_true(compare_json_strings(capture_stdout(), "[{\"path\":\"/dev/nvme1n1\",\"nsid\":1,\"model\":\"Unknown model\"," "\"properties\":{\"key1\":\"nvme1n1value1\",\"key2\":\"nvme1n1value2\"}," "\"vs\":\"key1=nvme1n1value1,key2=nvme1n1value2\"}," "{\"path\":\"/dev/nvme6n1\",\"nsid\":1,\"model\":\"MSFT NVMe Accelerator v1.0\"," "\"properties\":{\"key1\":\"nvme6n1value1\",\"key2\":\"nvme6n1value2\"}," "\"vs\":\"key1=nvme6n1value1,key2=nvme6n1value2\"}]")); } static void test_enumerate_namespaces_missing_controller(void **state) { (void)state; struct nvme_controller ctrl = {0}; snprintf(ctrl.name, sizeof(ctrl.name), "nvme0"); snprintf(ctrl.sys_path, sizeof(ctrl.sys_path), "%s/nvme0", fake_sys_class_nvme_path); struct context ctx = {.output_format = JSON}; json_object *namespaces = json_object_new_array(); assert_non_null(namespaces); int result = enumerate_namespaces_for_controller(&ctrl, &ctx, namespaces); size_t count = json_object_array_length(namespaces); json_object_put(namespaces); assert_int_equal(result, EXIT_CODE_SUCCESS); assert_int_equal(count, 0); char expected[MAX_PATH + 128]; snprintf(expected, sizeof(expected), "failed scandir for %s: %s\n", ctrl.sys_path, strerror(ENOENT)); assert_string_equal(capture_stderr(), expected); assert_string_equal(capture_stdout(), ""); } static void test_identify_disks_controller_model_read_error(void **state) { (void)state; create_file(fake_sys_class_nvme_path, "nvme0/device/vendor", "0x1414"); create_dir(fake_sys_class_nvme_path, "nvme0/model"); create_dir(fake_sys_class_nvme_path, "nvme0/nvme0n1"); _setup_nvme1_microsoft_one_namespace(); struct context ctx = {.output_format = PLAIN}; assert_int_equal(identify_disks(&ctx), EXIT_CODE_SUCCESS); char expected[MAX_PATH + 256]; snprintf(expected, sizeof(expected), "failed to read model name from %s/nvme0/model: %s\n" "failed to initialize NVMe controller nvme0\n", fake_sys_class_nvme_path, strerror(EISDIR)); assert_string_equal(capture_stderr(), expected); assert_string_equal(capture_stdout(), "/dev/nvme1n1: key1=nvme1n1value1,key2=nvme1n1value2\n"); } static void test_identify_disks_no_sys_class_nvme_present_json(void **state) { (void)state; assert_int_equal(rmdir(fake_sys_class_nvme_path), 0); struct context ctx = {.output_format = JSON}; assert_int_equal(identify_disks(&ctx), EXIT_CODE_SUCCESS); assert_true(compare_json_strings(capture_stdout(), "[]")); assert_non_null(strstr(capture_stderr(), strerror(ENOENT))); } static void test_identify_disks_namespace_permission_denied(void **state) { (void)state; if (geteuid() == 0) { skip(); } _setup_nvme0_microsoft_no_name_namespaces(); char controller_path[MAX_PATH]; snprintf(controller_path, sizeof(controller_path), "%s/nvme0", fake_sys_class_nvme_path); assert_int_equal(chmod(controller_path, 0100), 0); struct context ctx = {.output_format = JSON}; int result = identify_disks(&ctx); assert_int_equal(chmod(controller_path, 0700), 0); assert_int_equal(result, EXIT_CODE_NAMESPACE_SCAN); assert_non_null(strstr(capture_stderr(), strerror(EACCES))); assert_string_equal(capture_stdout(), ""); } static void test_identify_disks_permission_denied(void **state) { (void)state; if (geteuid() == 0) { skip(); } assert_int_equal(chmod(fake_sys_class_nvme_path, 0000), 0); struct context ctx = {.output_format = PLAIN}; int result = identify_disks(&ctx); assert_int_equal(chmod(fake_sys_class_nvme_path, 0700), 0); assert_int_equal(result, EXIT_CODE_CONTROLLER_SCAN); assert_non_null(strstr(capture_stderr(), strerror(EACCES))); assert_string_equal(capture_stdout(), ""); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_trim_trailing_whitespace), cmocka_unit_test_setup_teardown(test_identify_namespace_without_vs_missing_model, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_namespace_without_vs_model_read_error, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_namespace_without_vs_empty_model, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_disks_vendor_failures, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_disks_no_sys_class_nvme_present, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_disks_sys_class_nvme_empty, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_disks_sys_class_nvme_empty_json, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_disks, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_disks_combined, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_disks_combined_json, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_disks_controller_failure, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_disks_controller_failure_json, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_disks_nonmatching_nsid, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_disks_nonmatching_nsid_json, setup, teardown), cmocka_unit_test_setup_teardown(test_enumerate_namespaces_missing_controller, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_disks_controller_model_read_error, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_disks_no_sys_class_nvme_present_json, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_disks_namespace_permission_denied, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_disks_permission_denied, setup, teardown), }; return cmocka_run_group_tests(tests, NULL, NULL); } azure-vm-utils-0.8.0/tests/identify_udev_tests.c000066400000000000000000000135051527236310300220170ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #include #include #include #include #include #include #include #include #include // clang-format off #include // clang-format on #include "capture.h" #include "exit_code.h" #include "identify_udev.h" #include "nvme.h" char *__real_strdup(const char *str); char *__wrap_strdup(const char *str) { check_expected_ptr(str); char *result = mock_ptr_type(char *); if (result == NULL) { errno = mock_type(int); } return result; } char *__wrap_nvme_identify_namespace_vs_for_namespace_device(const char *namespace_path) { check_expected_ptr(namespace_path); return mock_ptr_type(char *); } static int setup(void **state) { capture_setup(state); unsetenv("DEVNAME"); return 0; } static int teardown(void **state) { capture_teardown(state); unsetenv("DEVNAME"); return 0; } static void test_print_udev_key_value(void **state) { (void)state; // Unused parameter print_udev_key_value("type", "local"); assert_string_equal(capture_stdout(), "AZURE_DISK_TYPE=local\n"); assert_string_equal(capture_stderr(), ""); } static void test_print_udev_key_values_allocation_failure(void **state) { (void)state; // Unused parameter char vs[] = "type=local"; expect_string(__wrap_strdup, str, vs); will_return(__wrap_strdup, NULL); will_return(__wrap_strdup, ENOMEM); int result = print_udev_key_values_for_vs(vs); assert_int_equal(result, EXIT_CODE_NO_MEMORY); assert_string_equal(capture_stderr(), "strdup() failed: Cannot allocate memory\n"); assert_string_equal(capture_stdout(), ""); } static void test_print_udev_key_values_for_vs_success(void **state) { (void)state; // Unused parameter char vs[] = "type=local,index=2,name=nvme-600G-2"; expect_string(__wrap_strdup, str, vs); will_return(__wrap_strdup, __real_strdup(vs)); int result = print_udev_key_values_for_vs(vs); assert_int_equal(result, EXIT_CODE_SUCCESS); assert_string_equal(capture_stdout(), "AZURE_DISK_TYPE=local\n" "AZURE_DISK_INDEX=2\n" "AZURE_DISK_NAME=nvme-600G-2\n"); assert_string_equal(capture_stderr(), ""); } static void test_print_udev_key_values_for_vs_failure(void **state) { (void)state; // Unused parameter char vs[] = "type=local,index=2,name"; expect_string(__wrap_strdup, str, vs); will_return(__wrap_strdup, __real_strdup(vs)); int result = print_udev_key_values_for_vs(vs); assert_int_equal(result, EXIT_CODE_UDEV_INVALID_PROPERTIES); assert_string_equal(capture_stderr(), "failed to parse key-value pair: name\n"); assert_string_equal(capture_stdout(), "AZURE_DISK_TYPE=local\n" "AZURE_DISK_INDEX=2\n"); } static void test_identify_udev_device_allocation_failure(void **state) { (void)state; // Unused parameter setenv("DEVNAME", "/dev/nvme0n5", 1); char *vs = __real_strdup("type=local"); expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme0n5"); will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, vs); expect_string(__wrap_strdup, str, vs); will_return(__wrap_strdup, NULL); will_return(__wrap_strdup, ENOMEM); int result = identify_udev_device(); assert_int_equal(result, EXIT_CODE_NO_MEMORY); assert_string_equal(capture_stderr(), "strdup() failed: Cannot allocate memory\n"); assert_string_equal(capture_stdout(), "AZURE_DISK_VS=type=local\n"); } static void test_identify_udev_device_success(void **state) { (void)state; // Unused parameter setenv("DEVNAME", "/dev/nvme0n5", 1); const char *vs = __real_strdup("type=local,index=2,name=nvme-600G-2"); expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme0n5"); will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, vs); expect_string(__wrap_strdup, str, vs); will_return(__wrap_strdup, __real_strdup(vs)); int result = identify_udev_device(); assert_int_equal(result, EXIT_CODE_SUCCESS); assert_string_equal(capture_stdout(), "AZURE_DISK_VS=type=local,index=2,name=nvme-600G-2\n" "AZURE_DISK_TYPE=local\n" "AZURE_DISK_INDEX=2\n" "AZURE_DISK_NAME=nvme-600G-2\n"); assert_string_equal(capture_stderr(), ""); } static void test_identify_udev_device_no_devname(void **state) { (void)state; // Unused parameter unsetenv("DEVNAME"); int result = identify_udev_device(); assert_int_equal(result, EXIT_CODE_UDEV_MISSING_DEVNAME); assert_string_equal(capture_stderr(), "environment variable 'DEVNAME' not set\n"); assert_string_equal(capture_stdout(), ""); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test_setup_teardown(test_print_udev_key_value, setup, teardown), cmocka_unit_test_setup_teardown(test_print_udev_key_values_allocation_failure, setup, teardown), cmocka_unit_test_setup_teardown(test_print_udev_key_values_for_vs_success, setup, teardown), cmocka_unit_test_setup_teardown(test_print_udev_key_values_for_vs_failure, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_udev_device_allocation_failure, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_udev_device_success, setup, teardown), cmocka_unit_test_setup_teardown(test_identify_udev_device_no_devname, setup, teardown), }; return cmocka_run_group_tests(tests, NULL, NULL); } azure-vm-utils-0.8.0/tests/nvme_tests.c000066400000000000000000000646131527236310300201340ustar00rootroot00000000000000/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license * information. */ #include #include #include #include #include #include #include #include #include #include #include #include // clange-format off #include // clange-format on #include "capture.h" #include "exit_code.h" #include "identify_disks.h" #include "identify_udev.h" #include "nvme.h" #define OPEN_FD 14 #define OPEN_PATH "/dev/nvme199n19" #define NSID 19 #include bool debug = false; struct nvme_id_ns empty_ns = { 0, }; char *__real_strndup(const char *value, size_t length); static bool fail_strndup; long __real_sysconf(int name); static bool fail_sysconf; long __wrap_sysconf(int name) { if (fail_sysconf) { assert_int_equal(name, _SC_PAGESIZE); errno = EINVAL; return -1; } return __real_sysconf(name); } int __wrap_open(const char *pathname, int flags, ...) { check_expected(pathname); check_expected(flags); int ret = mock_type(int); if (ret < 0) { errno = mock_type(int); } return ret; } /** * Redirect open64 to open for platforms that use it, e.g. armhf. */ int __wrap_open64(const char *pathname, int flags, ...) { return __wrap_open(pathname, flags); } int __wrap_posix_memalign(void **memptr, size_t alignment, size_t size) { check_expected(memptr); check_expected(alignment); check_expected(size); int ret = mock_type(int); if (ret == 0) { *memptr = malloc(size); } return ret; } char *__wrap_strndup(const char *value, size_t length) { if (fail_strndup) { errno = ENOMEM; return NULL; } return __real_strndup(value, length); } int __wrap_ioctl(int fd, unsigned long request, ...) { check_expected(fd); check_expected(request); if (request == NVME_IOCTL_ID) { int ret = mock_type(int); if (ret < 0) { errno = mock_type(int); } return ret; } va_list args; va_start(args, request); void *arg = va_arg(args, void *); va_end(args); struct nvme_admin_cmd *cmd = (struct nvme_admin_cmd *)arg; assert_non_null(cmd); assert_int_equal(cmd->opcode, NVME_ADMIN_IDENTFY_NAMESPACE_OPCODE); unsigned int nsid = mock_type(unsigned int); assert_int_equal(cmd->nsid, nsid); assert_int_equal(cmd->data_len, sizeof(struct nvme_id_ns)); assert_non_null(cmd->addr); struct nvme_id_ns *ns = mock_type(struct nvme_id_ns *); if (ns != NULL) { memcpy((void *)(uintptr_t)cmd->addr, ns, sizeof(*ns)); ns = (struct nvme_id_ns *)(uintptr_t)cmd->addr; } int ret = mock_type(int); if (ret < 0) { errno = mock_type(int); } return ret; } /** * Redirect __ioctl_time64 to ioctl for platforms that use it, e.g. armhf. */ int __wrap___ioctl_time64(int fd, unsigned long request, ...) { if (request == NVME_IOCTL_ID) { return __wrap_ioctl(fd, request); } va_list args; va_start(args, request); void *arg = va_arg(args, void *); va_end(args); return __wrap_ioctl(fd, request, arg); } int __wrap_close(int fd) { check_expected(fd); return mock_type(int); } static void test_nvme_identify_namespace_command_failure(void **state) { (void)state; pid_t child = capture_fork(); if (child == 0) { expect_string(__wrap_open, pathname, OPEN_PATH); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_any(__wrap_posix_memalign, memptr); expect_value(__wrap_posix_memalign, alignment, sysconf(_SC_PAGESIZE)); expect_value(__wrap_posix_memalign, size, sizeof(struct nvme_id_ns)); will_return(__wrap_posix_memalign, 0); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ADMIN_CMD); will_return(__wrap_ioctl, NSID); will_return(__wrap_ioctl, &empty_ns); will_return(__wrap_ioctl, 0x182); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); nvme_identify_namespace(OPEN_PATH, NSID); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NVME_IDENTIFY_NAMESPACE_STATUS); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), "failed NVME_IOCTL_ADMIN_CMD ioctl for /dev/nvme199n19: NVMe status 0x182\n"); } static void test_nvme_identify_namespace_success(void **state) { (void)state; // Unused parameter expect_string(__wrap_open, pathname, OPEN_PATH); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_any(__wrap_posix_memalign, memptr); expect_value(__wrap_posix_memalign, alignment, sysconf(_SC_PAGESIZE)); expect_value(__wrap_posix_memalign, size, sizeof(struct nvme_id_ns)); will_return(__wrap_posix_memalign, 0); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ADMIN_CMD); will_return(__wrap_ioctl, NSID); will_return(__wrap_ioctl, &empty_ns); will_return(__wrap_ioctl, 0); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); struct nvme_id_ns *result = nvme_identify_namespace(OPEN_PATH, NSID); assert_non_null(result); free(result); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), ""); } static void test_nvme_identify_namespace_failure_to_open(void **state) { (void)state; // Unused parameter pid_t child = capture_fork(); if (child == 0) { expect_string(__wrap_open, pathname, OPEN_PATH); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, -1); will_return(__wrap_open, EOWNERDEAD); nvme_identify_namespace(OPEN_PATH, NSID); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NVME_DEVICE_OPEN); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), "failed to open /dev/nvme199n19: Owner died\n"); } static void test_nvme_identify_namespace_failure_to_sysconf(void **state) { (void)state; pid_t child = capture_fork(); if (child == 0) { expect_string(__wrap_open, pathname, OPEN_PATH); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); fail_sysconf = true; nvme_identify_namespace(OPEN_PATH, NSID); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NVME_PAGE_SIZE); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), "failed sysconf: Invalid argument\n"); } static void test_nvme_identify_namespace_failure_to_posix_memalign(void **state) { (void)state; // Unused parameter pid_t child = capture_fork(); if (child == 0) { expect_string(__wrap_open, pathname, OPEN_PATH); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_any(__wrap_posix_memalign, memptr); expect_value(__wrap_posix_memalign, alignment, sysconf(_SC_PAGESIZE)); expect_value(__wrap_posix_memalign, size, sizeof(struct nvme_id_ns)); will_return(__wrap_posix_memalign, ENOLINK); errno = EINVAL; nvme_identify_namespace(OPEN_PATH, NSID); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NO_MEMORY); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), "failed posix_memalign for /dev/nvme199n19: Link has been severed\n"); } static void test_nvme_identify_namespace_failure_to_ioctl(void **state) { (void)state; // Unused parameter pid_t child = capture_fork(); if (child == 0) { expect_string(__wrap_open, pathname, OPEN_PATH); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_any(__wrap_posix_memalign, memptr); expect_value(__wrap_posix_memalign, alignment, sysconf(_SC_PAGESIZE)); expect_value(__wrap_posix_memalign, size, sizeof(struct nvme_id_ns)); will_return(__wrap_posix_memalign, 0); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ADMIN_CMD); will_return(__wrap_ioctl, NSID); will_return(__wrap_ioctl, NULL); will_return(__wrap_ioctl, -1); will_return(__wrap_ioctl, EUCLEAN); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); nvme_identify_namespace(OPEN_PATH, NSID); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NVME_IDENTIFY_NAMESPACE_IOCTL); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), "failed NVME_IOCTL_ADMIN_CMD ioctl for /dev/nvme199n19: " "Structure needs cleaning\n"); } static void test_nvme_identify_namespace_vs_allocation_failure(void **state) { (void)state; pid_t child = capture_fork(); if (child == 0) { expect_string(__wrap_open, pathname, OPEN_PATH); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_any(__wrap_posix_memalign, memptr); expect_value(__wrap_posix_memalign, alignment, sysconf(_SC_PAGESIZE)); expect_value(__wrap_posix_memalign, size, sizeof(struct nvme_id_ns)); will_return(__wrap_posix_memalign, 0); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ADMIN_CMD); will_return(__wrap_ioctl, NSID); will_return(__wrap_ioctl, &empty_ns); will_return(__wrap_ioctl, 0); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); fail_strndup = true; nvme_identify_namespace_vs(OPEN_PATH, NSID); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NO_MEMORY); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), "strndup() failed: Cannot allocate memory\n"); } static void test_nvme_identify_namespace_vs_success(void **state) { (void)state; // Unused parameter expect_string(__wrap_open, pathname, OPEN_PATH); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_any(__wrap_posix_memalign, memptr); expect_value(__wrap_posix_memalign, alignment, sysconf(_SC_PAGESIZE)); expect_value(__wrap_posix_memalign, size, sizeof(struct nvme_id_ns)); will_return(__wrap_posix_memalign, 0); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ADMIN_CMD); will_return(__wrap_ioctl, NSID); struct nvme_id_ns ns = {.vs = "key1=value1,key2=value2"}; will_return(__wrap_ioctl, &ns); will_return(__wrap_ioctl, 0); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); char *vs = nvme_identify_namespace_vs(OPEN_PATH, NSID); assert_non_null(vs); assert_string_equal(vs, "key1=value1,key2=value2"); free(vs); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), ""); } static void test_nvme_identify_namespace_vs_failure(void **state) { (void)state; // Unused parameter pid_t child = capture_fork(); if (child == 0) { expect_string(__wrap_open, pathname, OPEN_PATH); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, -1); will_return(__wrap_open, EOWNERDEAD); nvme_identify_namespace_vs(OPEN_PATH, NSID); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NVME_DEVICE_OPEN); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), "failed to open /dev/nvme199n19: Owner died\n"); } static void test_get_nsid_from_namespace_device_path_success(void **state) { (void)state; // Unused parameter expect_string(__wrap_open, pathname, "/dev/nvme1n1"); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ID); will_return(__wrap_ioctl, 5); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); assert_int_equal(get_nsid_from_namespace_device_path("/dev/nvme1n1"), 5); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), ""); } static void test_get_nsid_from_namespace_device_path_open_failure(void **state) { (void)state; // Unused parameter pid_t child = capture_fork(); if (child == 0) { expect_string(__wrap_open, pathname, OPEN_PATH); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, -1); will_return(__wrap_open, ENOENT); get_nsid_from_namespace_device_path(OPEN_PATH); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NVME_DEVICE_OPEN); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), "failed to open /dev/nvme199n19: No such file or directory\n"); } static void test_get_nsid_from_namespace_device_path_ioctl_failure(void **state) { (void)state; pid_t child = capture_fork(); if (child == 0) { expect_string(__wrap_open, pathname, OPEN_PATH); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ID); will_return(__wrap_ioctl, -1); will_return(__wrap_ioctl, ENOTTY); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); get_nsid_from_namespace_device_path(OPEN_PATH); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NVME_NAMESPACE_ID_IOCTL); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), "failed NVME_IOCTL_ID ioctl for /dev/nvme199n19: Inappropriate ioctl for device\n"); } static void test_nvme_identify_namespace_vs_for_namespace_device_success(void **state) { (void)state; // Unused parameter const char *path = "/dev/nvme0n5"; const char *vendor_specific_data = "key1=value1,key2=value2"; expect_string(__wrap_open, pathname, path); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ID); will_return(__wrap_ioctl, 7); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); expect_string(__wrap_open, pathname, path); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_any(__wrap_posix_memalign, memptr); expect_value(__wrap_posix_memalign, alignment, sysconf(_SC_PAGESIZE)); expect_value(__wrap_posix_memalign, size, sizeof(struct nvme_id_ns)); will_return(__wrap_posix_memalign, 0); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ADMIN_CMD); will_return(__wrap_ioctl, 7); struct nvme_id_ns ns = {.vs = "key1=value1,key2=value2"}; will_return(__wrap_ioctl, &ns); will_return(__wrap_ioctl, 0); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); char *result = nvme_identify_namespace_vs_for_namespace_device(path); assert_non_null(result); assert_string_equal(result, vendor_specific_data); free(result); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), ""); } static void test_nvme_identify_namespace_vs_for_namespace_device_nsid_failure(void **state) { (void)state; // Unused parameter const char *path = "/dev/nvme0n5"; pid_t child = capture_fork(); if (child == 0) { expect_string(__wrap_open, pathname, path); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ID); will_return(__wrap_ioctl, -1); will_return(__wrap_ioctl, ENOTTY); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); nvme_identify_namespace_vs_for_namespace_device(path); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NVME_NAMESPACE_ID_IOCTL); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), "failed NVME_IOCTL_ID ioctl for /dev/nvme0n5: Inappropriate ioctl for device\n"); } static void test_nvme_identify_namespace_vs_for_namespace_device_vs_failure(void **state) { (void)state; // Unused parameter const char *path = "/dev/nvme0n5"; pid_t child = capture_fork(); if (child == 0) { expect_string(__wrap_open, pathname, path); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ID); will_return(__wrap_ioctl, 7); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); expect_string(__wrap_open, pathname, path); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, -1); will_return(__wrap_open, EOWNERDEAD); nvme_identify_namespace_vs_for_namespace_device(path); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NVME_DEVICE_OPEN); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), "failed to open /dev/nvme0n5: Owner died\n"); } static void test_identify_udev_device_vs_failure(void **state) { (void)state; pid_t child = capture_fork(); if (child == 0) { assert_int_equal(setenv("DEVNAME", "/dev/nvme0n5", 1), 0); expect_string(__wrap_open, pathname, "/dev/nvme0n5"); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ID); will_return(__wrap_ioctl, NSID); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); expect_string(__wrap_open, pathname, "/dev/nvme0n5"); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_any(__wrap_posix_memalign, memptr); expect_value(__wrap_posix_memalign, alignment, sysconf(_SC_PAGESIZE)); expect_value(__wrap_posix_memalign, size, sizeof(struct nvme_id_ns)); will_return(__wrap_posix_memalign, 0); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ADMIN_CMD); will_return(__wrap_ioctl, NSID); will_return(__wrap_ioctl, NULL); will_return(__wrap_ioctl, -1); will_return(__wrap_ioctl, EUCLEAN); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); identify_udev_device(); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NVME_IDENTIFY_NAMESPACE_IOCTL); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), "failed NVME_IOCTL_ADMIN_CMD ioctl for /dev/nvme0n5: Structure needs cleaning\n"); } static void test_enumerate_namespaces_nsid_failure(void **state) { (void)state; char template[] = "/tmp/nvme_test_XXXXXX"; char *controller_path = mkdtemp(template); assert_non_null(controller_path); char first_namespace[MAX_PATH]; char second_namespace[MAX_PATH]; snprintf(first_namespace, sizeof(first_namespace), "%s/nvme0n1", controller_path); snprintf(second_namespace, sizeof(second_namespace), "%s/nvme0n2", controller_path); assert_int_equal(mkdir(first_namespace, 0700), 0); assert_int_equal(mkdir(second_namespace, 0700), 0); pid_t child = capture_fork(); if (child == 0) { expect_string(__wrap_open, pathname, "/dev/nvme0n1"); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ID); will_return(__wrap_ioctl, -1); will_return(__wrap_ioctl, ENOTTY); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); struct nvme_controller ctrl = {0}; snprintf(ctrl.name, sizeof(ctrl.name), "nvme0"); snprintf(ctrl.sys_path, sizeof(ctrl.sys_path), "%s", controller_path); struct context ctx = {.output_format = PLAIN}; json_object *namespaces = json_object_new_array(); assert_non_null(namespaces); enumerate_namespaces_for_controller(&ctrl, &ctx, namespaces); json_object_put(namespaces); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NVME_NAMESPACE_ID_IOCTL); assert_int_equal(rmdir(first_namespace), 0); assert_int_equal(rmdir(second_namespace), 0); assert_int_equal(rmdir(controller_path), 0); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), "failed NVME_IOCTL_ID ioctl for /dev/nvme0n1: Inappropriate ioctl for device\n"); } static void test_enumerate_namespaces_vs_failure(void **state) { (void)state; char template[] = "/tmp/nvme_test_XXXXXX"; char *controller_path = mkdtemp(template); assert_non_null(controller_path); char first_namespace[MAX_PATH]; char second_namespace[MAX_PATH]; snprintf(first_namespace, sizeof(first_namespace), "%s/nvme0n1", controller_path); snprintf(second_namespace, sizeof(second_namespace), "%s/nvme0n2", controller_path); assert_int_equal(mkdir(first_namespace, 0700), 0); assert_int_equal(mkdir(second_namespace, 0700), 0); pid_t child = capture_fork(); if (child == 0) { expect_string(__wrap_open, pathname, "/dev/nvme0n1"); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ID); will_return(__wrap_ioctl, NSID); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); expect_string(__wrap_open, pathname, "/dev/nvme0n1"); expect_value(__wrap_open, flags, O_RDONLY); will_return(__wrap_open, OPEN_FD); expect_any(__wrap_posix_memalign, memptr); expect_value(__wrap_posix_memalign, alignment, sysconf(_SC_PAGESIZE)); expect_value(__wrap_posix_memalign, size, sizeof(struct nvme_id_ns)); will_return(__wrap_posix_memalign, 0); expect_value(__wrap_ioctl, fd, OPEN_FD); expect_value(__wrap_ioctl, request, NVME_IOCTL_ADMIN_CMD); will_return(__wrap_ioctl, NSID); will_return(__wrap_ioctl, NULL); will_return(__wrap_ioctl, -1); will_return(__wrap_ioctl, EUCLEAN); expect_value(__wrap_close, fd, OPEN_FD); will_return(__wrap_close, 0); struct nvme_controller ctrl = {0}; snprintf(ctrl.name, sizeof(ctrl.name), "nvme0"); snprintf(ctrl.sys_path, sizeof(ctrl.sys_path), "%s", controller_path); struct context ctx = {.output_format = PLAIN}; json_object *namespaces = json_object_new_array(); assert_non_null(namespaces); enumerate_namespaces_for_controller(&ctrl, &ctx, namespaces); json_object_put(namespaces); _exit(EXIT_CODE_SUCCESS); } capture_wait(child, EXIT_CODE_NVME_IDENTIFY_NAMESPACE_IOCTL); assert_int_equal(rmdir(first_namespace), 0); assert_int_equal(rmdir(second_namespace), 0); assert_int_equal(rmdir(controller_path), 0); assert_string_equal(capture_stdout(), ""); assert_string_equal(capture_stderr(), "failed NVME_IOCTL_ADMIN_CMD ioctl for /dev/nvme0n1: Structure needs cleaning\n"); } static int setup(void **state) { (void)state; // Unused parameter fail_strndup = false; fail_sysconf = false; capture_setup(state); return 0; } static int teardown(void **state) { (void)state; // Unused parameter capture_teardown(state); return 0; } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test_setup_teardown(test_identify_udev_device_vs_failure, setup, teardown), cmocka_unit_test_setup_teardown(test_enumerate_namespaces_nsid_failure, setup, teardown), cmocka_unit_test_setup_teardown(test_enumerate_namespaces_vs_failure, setup, teardown), cmocka_unit_test_setup_teardown(test_nvme_identify_namespace_command_failure, setup, teardown), cmocka_unit_test_setup_teardown(test_nvme_identify_namespace_success, setup, teardown), cmocka_unit_test_setup_teardown(test_nvme_identify_namespace_failure_to_open, setup, teardown), cmocka_unit_test_setup_teardown(test_nvme_identify_namespace_failure_to_sysconf, setup, teardown), cmocka_unit_test_setup_teardown(test_nvme_identify_namespace_failure_to_posix_memalign, setup, teardown), cmocka_unit_test_setup_teardown(test_nvme_identify_namespace_failure_to_ioctl, setup, teardown), cmocka_unit_test_setup_teardown(test_nvme_identify_namespace_vs_allocation_failure, setup, teardown), cmocka_unit_test_setup_teardown(test_nvme_identify_namespace_vs_success, setup, teardown), cmocka_unit_test_setup_teardown(test_nvme_identify_namespace_vs_failure, setup, teardown), cmocka_unit_test_setup_teardown(test_get_nsid_from_namespace_device_path_success, setup, teardown), cmocka_unit_test_setup_teardown(test_get_nsid_from_namespace_device_path_open_failure, setup, teardown), cmocka_unit_test_setup_teardown(test_get_nsid_from_namespace_device_path_ioctl_failure, setup, teardown), cmocka_unit_test_setup_teardown(test_nvme_identify_namespace_vs_for_namespace_device_success, setup, teardown), cmocka_unit_test_setup_teardown(test_nvme_identify_namespace_vs_for_namespace_device_nsid_failure, setup, teardown), cmocka_unit_test_setup_teardown(test_nvme_identify_namespace_vs_for_namespace_device_vs_failure, setup, teardown), }; return cmocka_run_group_tests(tests, NULL, NULL); } azure-vm-utils-0.8.0/udev/000077500000000000000000000000001527236310300153705ustar00rootroot00000000000000azure-vm-utils-0.8.0/udev/10-azure-unmanaged-sriov.rules000066400000000000000000000012001527236310300230760ustar00rootroot00000000000000# Azure VMs with accelerated networking may have MANA, mlx4, or mlx5 SR-IOV devices which are transparently bonded to a synthetic # hv_netvsc device. Mark devices with the IFF_SLAVE bit set as unmanaged devices: # AZURE_UNMANAGED_SRIOV=1 for 01-azure-unmanaged-sriov.network # ID_NET_MANAGED_BY=unmanaged for systemd-networkd >= 255 # NM_UNMANAGED=1 for NetworkManager # # ATTR{flags}=="0x?[89ABCDEF]??" checks the IFF_SLAVE bit (0x800). SUBSYSTEM=="net", ACTION!="remove", DRIVERS=="mana|mlx4_core|mlx5_core", ATTR{flags}=="0x?[89ABCDEF]??", ENV{AZURE_UNMANAGED_SRIOV}="1", ENV{ID_NET_MANAGED_BY}="unmanaged", ENV{NM_UNMANAGED}="1" azure-vm-utils-0.8.0/udev/80-azure-disk.rules000066400000000000000000000110301527236310300207420ustar00rootroot00000000000000ACTION!="add|change", GOTO="azure_disk_end" SUBSYSTEM!="block", GOTO="azure_disk_end" KERNEL=="nvme*", ATTRS{nsid}=="?*", ENV{ID_MODEL}=="Microsoft NVMe Direct Disk", GOTO="azure_disk_nvme_direct_v1" KERNEL=="nvme*", ATTRS{nsid}=="?*", ENV{ID_MODEL}=="Microsoft NVMe Direct Disk v2", GOTO="azure_disk_nvme_direct_v2" KERNEL=="nvme*", ATTRS{nsid}=="?*", ENV{ID_MODEL}=="MSFT NVMe Accelerator v1.0", GOTO="azure_disk_nvme_remote_v1" ENV{ID_VENDOR}=="Msft", ENV{ID_MODEL}=="Virtual_Disk", GOTO="azure_disk_scsi" GOTO="azure_disk_end" LABEL="azure_disk_scsi" ATTRS{device_id}=="?00000000-0000-*", ENV{AZURE_DISK_TYPE}="os", GOTO="azure_disk_symlink" ENV{DEVTYPE}=="partition", PROGRAM="/bin/sh -c 'readlink /sys/class/block/%k/../device|cut -d: -f4'", ENV{AZURE_DISK_LUN}="$result" ENV{DEVTYPE}=="disk", PROGRAM="/bin/sh -c 'readlink /sys/class/block/%k/device|cut -d: -f4'", ENV{AZURE_DISK_LUN}="$result" ATTRS{device_id}=="{f8b3781a-1e82-4818-a1c3-63d806ec15bb}", ENV{AZURE_DISK_LUN}=="0", ENV{AZURE_DISK_TYPE}="os", ENV{AZURE_DISK_LUN}="", GOTO="azure_disk_symlink" ATTRS{device_id}=="{f8b3781b-1e82-4818-a1c3-63d806ec15bb}", ENV{AZURE_DISK_TYPE}="data", GOTO="azure_disk_symlink" ATTRS{device_id}=="{f8b3781c-1e82-4818-a1c3-63d806ec15bb}", ENV{AZURE_DISK_TYPE}="data", GOTO="azure_disk_symlink" ATTRS{device_id}=="{f8b3781d-1e82-4818-a1c3-63d806ec15bb}", ENV{AZURE_DISK_TYPE}="data", GOTO="azure_disk_symlink" # Use "resource" type for local SCSI because some VM skus offer NVMe local disks in addition to a SCSI resource disk, e.g. LSv3 family. # This logic is already in walinuxagent rules but we duplicate it here to avoid an unnecessary dependency for anyone requiring it. ATTRS{device_id}=="?00000000-0001-*", ENV{AZURE_DISK_TYPE}="resource", ENV{AZURE_DISK_LUN}="", GOTO="azure_disk_symlink" ATTRS{device_id}=="{f8b3781a-1e82-4818-a1c3-63d806ec15bb}", ENV{AZURE_DISK_LUN}=="1", ENV{AZURE_DISK_TYPE}="resource", ENV{AZURE_DISK_LUN}="", GOTO="azure_disk_symlink" GOTO="azure_disk_end" LABEL="azure_disk_nvme_direct_v1" LABEL="azure_disk_nvme_direct_v2" ATTRS{nsid}=="?*", ENV{AZURE_DISK_TYPE}="local", ENV{AZURE_DISK_SERIAL}="$env{ID_SERIAL_SHORT}" GOTO="azure_disk_nvme_id" LABEL="azure_disk_nvme_remote_v1" # Azure hosts will retry remote I/O requests for up to 120 seconds. If I/O times out for longer than that, host will reboot OS. # Set timeout for remote disks to 240 seconds, giving host time to handle the retry or reboot the VM. ENV{DEVTYPE}=="disk", ATTRS{nsid}=="?*", ATTR{queue/io_timeout}="240000" # For remote disks, namespace ID=1 is OS disk, ID=2+ are data disks with customer-configured lun=ID-2 (e.g. lun=0 will have nsid=2). ATTRS{nsid}=="1", ENV{AZURE_DISK_TYPE}="os", GOTO="azure_disk_nvme_id" ATTRS{nsid}=="?*", ENV{AZURE_DISK_TYPE}="data", PROGRAM="/bin/sh -ec 'echo $$((%s{nsid}-2))'", ENV{AZURE_DISK_LUN}="$result" LABEL="azure_disk_nvme_id" ATTRS{nsid}=="?*", IMPORT{program}="/usr/sbin/azure-nvme-id --udev" LABEL="azure_disk_symlink" # systemd v254 ships an updated 60-persistent-storage.rules that would allow # these to be deduplicated using $env{.PART_SUFFIX} ENV{DEVTYPE}=="disk", ENV{AZURE_DISK_TYPE}=="os|resource|root", SYMLINK+="disk/azure/$env{AZURE_DISK_TYPE}" ENV{DEVTYPE}=="disk", ENV{AZURE_DISK_TYPE}=="?*", ENV{AZURE_DISK_INDEX}=="?*", SYMLINK+="disk/azure/$env{AZURE_DISK_TYPE}/by-index/$env{AZURE_DISK_INDEX}" ENV{DEVTYPE}=="disk", ENV{AZURE_DISK_TYPE}=="?*", ENV{AZURE_DISK_LUN}=="?*", SYMLINK+="disk/azure/$env{AZURE_DISK_TYPE}/by-lun/$env{AZURE_DISK_LUN}" ENV{DEVTYPE}=="disk", ENV{AZURE_DISK_TYPE}=="?*", ENV{AZURE_DISK_NAME}=="?*", SYMLINK+="disk/azure/$env{AZURE_DISK_TYPE}/by-name/$env{AZURE_DISK_NAME}" ENV{DEVTYPE}=="disk", ENV{AZURE_DISK_TYPE}=="?*", ENV{AZURE_DISK_SERIAL}=="?*", SYMLINK+="disk/azure/$env{AZURE_DISK_TYPE}/by-serial/$env{AZURE_DISK_SERIAL}" ENV{DEVTYPE}=="partition", ENV{AZURE_DISK_TYPE}=="os|resource|root", SYMLINK+="disk/azure/$env{AZURE_DISK_TYPE}-part%n" ENV{DEVTYPE}=="partition", ENV{AZURE_DISK_TYPE}=="?*", ENV{AZURE_DISK_INDEX}=="?*", SYMLINK+="disk/azure/$env{AZURE_DISK_TYPE}/by-index/$env{AZURE_DISK_INDEX}-part%n" ENV{DEVTYPE}=="partition", ENV{AZURE_DISK_TYPE}=="?*", ENV{AZURE_DISK_LUN}=="?*", SYMLINK+="disk/azure/$env{AZURE_DISK_TYPE}/by-lun/$env{AZURE_DISK_LUN}-part%n" ENV{DEVTYPE}=="partition", ENV{AZURE_DISK_TYPE}=="?*", ENV{AZURE_DISK_NAME}=="?*", SYMLINK+="disk/azure/$env{AZURE_DISK_TYPE}/by-name/$env{AZURE_DISK_NAME}-part%n" ENV{DEVTYPE}=="partition", ENV{AZURE_DISK_TYPE}=="?*", ENV{AZURE_DISK_SERIAL}=="?*", SYMLINK+="disk/azure/$env{AZURE_DISK_TYPE}/by-serial/$env{AZURE_DISK_SERIAL}-part%n" LABEL="azure_disk_end"