diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..d4f347b --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +BasedOnStyle: Microsoft diff --git a/.github/workflows/debs.yml b/.github/workflows/debs.yml index 5ea75ac..152eca1 100644 --- a/.github/workflows/debs.yml +++ b/.github/workflows/debs.yml @@ -7,7 +7,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04, ubuntu-22.04] + os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04] defaults: run: shell: bash @@ -24,10 +24,6 @@ jobs: 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 devscripts debhelper -y - name: Build debs run: | DEBEMAIL="Azure VM Utils CI " ./scripts/build-deb.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 993edc7..585a5ad 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04, ubuntu-22.04] + os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04] defaults: run: shell: bash @@ -27,7 +27,7 @@ jobs: - name: Setup run: | sudo apt update - sudo apt install gcc pandoc cmake -y + sudo apt install gcc pandoc clang-format cmake -y - name: Build & install project with cmake run: | cmake -B build -S . @@ -57,3 +57,18 @@ jobs: cmake -DGENERATE_MANPAGES=1 -B build -S . make -C build if ! grep -c "Pandoc" build/doc/azure-nvme-id.8; then echo "manpage not generated by pandoc"; exit 1; fi + - name: Run tests + run: | + set -x + sudo apt install -y libcmocka-dev cppcheck clang-format + rm -rf build + cmake -DENABLE_TESTS=1 -B build -S . + cd build + make + 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 diff --git a/CMakeLists.txt b/CMakeLists.txt index e630b1c..52bf4bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,7 @@ include(CTest) enable_testing() add_compile_options(-Wextra -Wall $<$:-Werror> -std=gnu11 -D_GNU_SOURCE=1) -add_executable(azure-nvme-id src/main.c) +add_executable(azure-nvme-id src/debug.c src/identify_disks.c src/identify_udev.c src/main.c src/nvme.c src/util.c) set(AZURE_NVME_ID_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/sbin") set(DRACUT_MODULES_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/lib/dracut/modules.d/97azure-disk" CACHE PATH "dracut modules installation directory") @@ -59,3 +59,8 @@ endif() set(CPACK_PROJECT_NAME ${PROJECT_NAME}) set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) include(CPack) + +option(ENABLE_TESTS "Enable unit tests" OFF) +if(ENABLE_TESTS) + include(cmake/tests.cmake) +endif() diff --git a/cmake/clangformat.cmake b/cmake/clangformat.cmake index a5437bc..e3cbe20 100644 --- a/cmake/clangformat.cmake +++ b/cmake/clangformat.cmake @@ -1,9 +1,11 @@ -file(GLOB_RECURSE ALL_SOURCE_FILES *.c *.h) +file(GLOB ALL_SOURCE_FILES src/*.[ch] tests/*.[ch]) -add_custom_target( - clangformat - COMMAND /usr/bin/clang-format - -style=Microsoft - -i - ${ALL_SOURCE_FILES} +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" ) diff --git a/cmake/cppcheck.cmake b/cmake/cppcheck.cmake index da5158d..1e4f9e2 100644 --- a/cmake/cppcheck.cmake +++ b/cmake/cppcheck.cmake @@ -1,9 +1,14 @@ -file(GLOB_RECURSE ALL_SOURCE_FILES *.c *.h) +file(GLOB ALL_SOURCE_FILES src/*.[ch] tests/*.[ch]) -add_custom_target( - cppcheck - COMMAND /usr/bin/cppcheck - --enable=all - --suppress=missingIncludeSystem - ${ALL_SOURCE_FILES} +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" ) diff --git a/cmake/tests.cmake b/cmake/tests.cmake new file mode 100644 index 0000000..f1c416c --- /dev/null +++ b/cmake/tests.cmake @@ -0,0 +1,50 @@ +find_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 ${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(debug_tests + WRAPPED_FUNCTIONS + SOURCES src/debug.c tests/debug_tests.c +) + +add_test_executable(identify_disks_tests + WRAPPED_FUNCTIONS nvme_identify_namespace_vs_for_namespace_device + SOURCES src/debug.c src/identify_disks.c src/nvme.c src/util.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 + SOURCES src/debug.c src/identify_udev.c src/nvme.c tests/identify_udev_tests.c +) + +add_test_executable(nvme_tests + WRAPPED_FUNCTIONS open posix_memalign ioctl close + SOURCES src/nvme.c tests/nvme_tests.c +) + +add_test_executable(util_tests + WRAPPED_FUNCTIONS fopen fseek ftell fread fclose malloc + SOURCES src/util.c tests/util_tests.c +) diff --git a/packaging/debian/control b/packaging/debian/control index 5d357dc..fcf864f 100644 --- a/packaging/debian/control +++ b/packaging/debian/control @@ -2,7 +2,7 @@ Source: azure-vm-utils Section: utils Priority: optional Maintainer: Chris Patterson -Build-Depends: cmake, pandoc, debhelper-compat (= 12) +Build-Depends: cmake, pandoc, debhelper-compat (= 12), libcmocka-dev Standards-Version: 4.5.0 Homepage: https://github.com/Azure/azure-vm-utils Rules-Requires-Root: no diff --git a/packaging/debian/rules b/packaging/debian/rules index f238a1f..fd5ca82 100755 --- a/packaging/debian/rules +++ b/packaging/debian/rules @@ -6,3 +6,9 @@ export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed %: dh $@ + +override_dh_auto_configure: + dh_auto_configure -- -DENABLE_TESTS=1 + +override_dh_auto_test: + CTEST_OUTPUT_ON_FAILURE=1 dh_auto_test diff --git a/scripts/build-deb.sh b/scripts/build-deb.sh index 440e020..fe0f5f8 100755 --- a/scripts/build-deb.sh +++ b/scripts/build-deb.sh @@ -2,6 +2,10 @@ set -eux -o pipefail +# Ensure dependencies are installed and up-to-date. +sudo apt update +sudo apt install gcc pandoc cmake devscripts debhelper libcmocka-dev build-essential clang-format cppcheck -y + 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/')" diff --git a/src/debug.c b/src/debug.c new file mode 100644 index 0000000..f525191 --- /dev/null +++ b/src/debug.c @@ -0,0 +1,25 @@ +/** + * 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++; + } +} diff --git a/src/debug.h b/src/debug.h new file mode 100644 index 0000000..0189668 --- /dev/null +++ b/src/debug.h @@ -0,0 +1,26 @@ +/** + * 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__ diff --git a/src/identify_disks.c b/src/identify_disks.c new file mode 100644 index 0000000..114ae03 --- /dev/null +++ b/src/identify_disks.c @@ -0,0 +1,124 @@ + +#include +#include +#include +#include +#include + +#include "debug.h" +#include "identify_disks.h" +#include "nvme.h" +#include "util.h" + +/** + * 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; +} + +/** + * Enumerate namespaces for a controller. + */ +int enumerate_namespaces_for_controller(struct nvme_controller *ctrl) +{ + struct dirent **namelist; + + int n = scandir(ctrl->sys_path, &namelist, is_nvme_namespace, alphasort); + if (n < 0) + { + fprintf(stderr, "failed scandir for %s: %m\n", ctrl->sys_path); + return 1; + } + + DEBUG_PRINTF("found %d namespace(s) for controller=%s:\n", n, ctrl->name); + for (int i = 0; i < n; i++) + { + char namespace_path[MAX_PATH]; + snprintf(namespace_path, sizeof(namespace_path), "/dev/%s", namelist[i]->d_name); + + char *vs = nvme_identify_namespace_vs_for_namespace_device(namespace_path); + if (vs != NULL) + { + printf("%s: %s\n", namespace_path, vs); + free(vs); + } + free(namelist[i]); + } + + free(namelist); + return 0; +} + +/** + * 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); + + char *vendor_id_string = read_file_as_string(vendor_id_path); + if (vendor_id_string == NULL) + { + return false; + } + + long int vendor_id = strtol(vendor_id_string, NULL, 16); + free(vendor_id_string); + + 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); +} + +/** + * Enumerate Microsoft Azure NVMe controllers and identify disks. + */ +int identify_disks(void) +{ + struct dirent **namelist; + + int n = scandir(SYS_CLASS_NVME_PATH, &namelist, is_azure_nvme_controller, alphasort); + if (n < 0) + { + fprintf(stderr, "no NVMe devices in %s: %m\n", SYS_CLASS_NVME_PATH); + return 0; + } + + DEBUG_PRINTF("found %d controllers\n", n); + for (int i = 0; i < n; i++) + { + struct nvme_controller ctrl; + + strncpy(ctrl.name, namelist[i]->d_name, sizeof(ctrl.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); + + enumerate_namespaces_for_controller(&ctrl); + free(namelist[i]); + } + + free(namelist); + return 0; +} diff --git a/src/identify_disks.h b/src/identify_disks.h new file mode 100644 index 0000000..b59fa4a --- /dev/null +++ b/src/identify_disks.h @@ -0,0 +1,37 @@ +/** + * 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 + +#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]; +}; + +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); +int is_azure_nvme_controller(const struct dirent *entry); +int identify_disks(void); + +#endif // __IDENTIFY_DISKS_H__ diff --git a/src/identify_udev.c b/src/identify_udev.c new file mode 100644 index 0000000..c403c80 --- /dev/null +++ b/src/identify_udev.c @@ -0,0 +1,101 @@ +/** + * 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 "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); + 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 1; + } + + print_udev_key_value(key, value); + pair = strtok_r(NULL, ",", &outer_saveptr); + } + + free(vs_copy); + return 0; +} + +/** + * 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 1; + } + + char *vs = nvme_identify_namespace_vs_for_namespace_device(dev_name); + if (vs == NULL) + { + fprintf(stderr, "failed to query namespace vendor-specific data: %s\n", dev_name); + return 1; + } + + printf("AZURE_DISK_VS=%s\n", vs); + print_udev_key_values_for_vs(vs); + free(vs); + + return 0; +} diff --git a/src/identify_udev.h b/src/identify_udev.h new file mode 100644 index 0000000..3ef7d3b --- /dev/null +++ b/src/identify_udev.h @@ -0,0 +1,14 @@ +/** + * 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__ diff --git a/src/main.c b/src/main.c index 318f97b..932b1fd 100644 --- a/src/main.c +++ b/src/main.c @@ -1,403 +1,21 @@ /** * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE in the project root for license information. + * 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 -#include -#include -#include -#include "nvme.h" +#include "debug.h" +#include "identify_disks.h" +#include "identify_udev.h" +#include "util.h" #include "version.h" -#define MAX_PATH 4096 -#define MICROSOFT_NVME_VENDOR_ID 0x1414 -#define SYS_CLASS_NVME_PATH "/sys/class/nvme" - -struct nvme_controller -{ - char name[256]; - char dev_path[MAX_PATH]; - char sys_path[MAX_PATH]; - char model[MAX_PATH]; -}; - -static bool debug = false; -static bool udev_mode = false; - -#define DEBUG_PRINTF(fmt, ...) \ - do \ - { \ - if (debug == true) \ - { \ - fprintf(stderr, "DEBUG: " fmt, ##__VA_ARGS__); \ - } \ - } while (0) - -/** - * Given the path to a namespace, determine the namespace id. - */ -int get_namespace_id_for_path(const char *namespace_path) -{ - unsigned int ctrl, nsid; - - if (sscanf(namespace_path, "/dev/nvme%un%u", &ctrl, &nsid) != 2) - { - return -1; - } - - return nsid; -} - -/** - * 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++; - } -} - -/** - * Read file contents as a null-terminated string. - * - * For sysfs entries, etc. where the file contents are not null-terminated and - * we know the contents are just a simple string (e.g. sysfs attributes). - */ -char *read_file_as_string(const char *path) -{ - DEBUG_PRINTF("reading %s...\n", path); - - FILE *file = fopen(path, "r"); - if (file == NULL) - { - fprintf(stderr, "failed to fopen %s: %m\n", path); - return NULL; - } - - // Determine the file size - if (fseek(file, 0, SEEK_END) < 0) - { - fprintf(stderr, "failed to fseek on %s: %m\n", path); - fclose(file); - return NULL; - } - - long length = ftell(file); - if (length < 0) - { - fprintf(stderr, "failed to ftell on %s: %m\n", path); - fclose(file); - return NULL; - } - - if (fseek(file, 0, SEEK_SET) < 0) - { - fprintf(stderr, "failed to fseek on %s: %m\n", path); - fclose(file); - return NULL; - } - - // Allocate size of file plus one byte for null. - char *contents = malloc(length + 1); - if (contents == NULL) - { - fprintf(stderr, "failed to malloc for %s: %m\n", path); - fclose(file); - return NULL; - } - - // Read file contents. - size_t bytes_read = fread(contents, 1, length, file); - if ((long)bytes_read < length) - { - DEBUG_PRINTF("short read on %s, contents probably changed", path); - } - - fclose(file); - - // Null-terminate the string. - contents[bytes_read] = '\0'; - - DEBUG_PRINTF("%s => %s\n", path, contents); - return contents; -} - -/** - * 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); - - char *vendor_id_string = read_file_as_string(vendor_id_path); - if (vendor_id_string == NULL) - { - return false; - } - - long int vendor_id = strtol(vendor_id_string, NULL, 16); - free(vendor_id_string); - - return vendor_id == MICROSOFT_NVME_VENDOR_ID; -} - -/** - * 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. - */ -char *query_namespace_vs(const char *namespace_path) -{ - int nsid = get_namespace_id_for_path(namespace_path); - if (nsid < 0) - { - fprintf(stderr, "failed to parse namespace id: %s\n", namespace_path); - return NULL; - } - - int fd = open(namespace_path, O_RDONLY); - if (fd < 0) - { - fprintf(stderr, "failed to open %s: %m\n", namespace_path); - return NULL; - } - - DEBUG_PRINTF("Opened device: %s with namespace id: %d...\n", namespace_path, nsid); - - struct nvme_id_ns *ns = NULL; - if (posix_memalign((void **)&ns, sysconf(_SC_PAGESIZE), sizeof(struct nvme_id_ns)) != 0) - { - fprintf(stderr, "failed posix_memalign for %s: %m\n", namespace_path); - close(fd); - return NULL; - } - - struct nvme_admin_cmd cmd = { - .opcode = 0x06, // Identify command - .nsid = nsid, - .addr = (unsigned long)ns, - .data_len = sizeof(struct nvme_id_ns), - }; - - if (ioctl(fd, NVME_IOCTL_ADMIN_CMD, &cmd) < 0) - { - fprintf(stderr, "failed NVME_IOCTL_ADMIN_CMD ioctl for %s: %m\n", namespace_path); - free(ns); - close(fd); - return NULL; - } - - char *vs = strndup((const char *)ns->vs, sizeof(ns->vs)); - - free(ns); - close(fd); - - return vs; -} - -/** - * 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; -} - -/** - * Enumerate namespaces for a controller. - */ -int enumerate_namespaces_for_controller(struct nvme_controller *ctrl) -{ - struct dirent **namelist; - - int n = scandir(ctrl->sys_path, &namelist, is_nvme_namespace, alphasort); - if (n < 0) - { - fprintf(stderr, "failed scandir for %s: %m\n", ctrl->sys_path); - return 1; - } - - DEBUG_PRINTF("found %d namespace(s) for controller=%s:\n", n, ctrl->name); - for (int i = 0; i < n; i++) - { - char namespace_path[MAX_PATH]; - snprintf(namespace_path, sizeof(namespace_path), "/dev/%s", namelist[i]->d_name); - - char *vs = query_namespace_vs(namespace_path); - if (vs != NULL) { - printf("%s: %s\n", namespace_path, vs); - free(vs); - } - free(namelist[i]); - } - - free(namelist); - return 0; -} - -/** - * 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); -} - -/** - * Enumerate Microsoft Azure NVMe controllers. - */ -int enumerate_azure_nvme_controllers() -{ - struct dirent **namelist; - - int n = scandir(SYS_CLASS_NVME_PATH, &namelist, is_azure_nvme_controller, alphasort); - if (n < 0) - { - fprintf(stderr, "no NVMe devices in %s: %m\n", SYS_CLASS_NVME_PATH); - return 0; - } - - DEBUG_PRINTF("found %d controllers\n", n); - for (int i = 0; i < n; i++) - { - struct nvme_controller ctrl; - - strncpy(ctrl.name, namelist[i]->d_name, sizeof(ctrl.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); - - enumerate_namespaces_for_controller(&ctrl); - free(namelist[i]); - } - - free(namelist); - return 0; -} - -/** - * 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 parse_vs_for_udev_import(char *vs) -{ - char *vs_copy = strdup(vs); - 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 1; - } - - print_udev_key_value(key, value); - pair = strtok_r(NULL, ",", &outer_saveptr); - } - - free(vs_copy); - return 0; -} - -/** - * Execute udev mode, printing out environment variables for import. - * - * AZURE_DISK_VS: - * AZURE_DISK_TYPE: - * AZURE_DISK_INDEX: - * AZURE_DISK_NAME: - * etc... - */ -int execute_udev_import(void) -{ - const char *dev_name = getenv("DEVNAME"); - if (dev_name == NULL) - { - fprintf(stderr, "environment variable 'DEVNAME' not set\n"); - return 1; - } - - char *vs = query_namespace_vs(dev_name); - if (vs == NULL) - { - fprintf(stderr, "failed to query namespace vendor-specific data: %s\n", dev_name); - return 1; - } - - printf("AZURE_DISK_VS=%s\n", vs); - parse_vs_for_udev_import(vs); - free(vs); - - return 0; -} - int main(int argc, const char **argv) { + bool udev_mode = false; + for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--debug") == 0) @@ -429,8 +47,8 @@ int main(int argc, const char **argv) if (udev_mode) { - return execute_udev_import(); + return identify_udev_device(); } - return enumerate_azure_nvme_controllers(); + return identify_disks(); } diff --git a/src/nvme.c b/src/nvme.c new file mode 100644 index 0000000..36ef199 --- /dev/null +++ b/src/nvme.c @@ -0,0 +1,136 @@ +/** + * 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 "debug.h" +#include "nvme.h" + +/** + * Given the path to a namespace device, determine the namespace id. + * + * Examples: + * - /dev/nvme0n5 -> returns 5 + * - /dev/nvme2n12 -> returns 12 + * - /dev/nvme100n1 -> returns 1 + * + * @return Namespace ID or -1 on failure. + */ +int get_nsid_from_namespace_device_path(const char *namespace_path) +{ + unsigned int ctrl, nsid; + + if (sscanf(namespace_path, "/dev/nvme%un%u", &ctrl, &nsid) != 2) + { + return -1; + } + + return nsid; +} + +/** + * Execute identify namespace command. + * + * @param device_path Path to the NVMe device, either controller or namespace. + * @param nsid Namespace ID to identify. + * + * @return Pointer to the namespace structure or NULL on failure. + * + */ +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); + return NULL; + } + + struct nvme_id_ns *ns = NULL; + if (posix_memalign((void **)&ns, sysconf(_SC_PAGESIZE), sizeof(struct nvme_id_ns)) != 0) + { + fprintf(stderr, "failed posix_memalign for %s: %m\n", device_path); + close(fd); + return NULL; + } + + struct nvme_admin_cmd cmd = { + .opcode = 0x06, // Identify namespace command + .nsid = nsid, + .addr = (unsigned long)ns, + .data_len = sizeof(struct nvme_id_ns), + }; + + if (ioctl(fd, NVME_IOCTL_ADMIN_CMD, &cmd) < 0) + { + fprintf(stderr, "failed NVME_IOCTL_ADMIN_CMD ioctl for %s: %m\n", device_path); + free(ns); + close(fd); + return NULL; + } + + 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. + * + * @param device_path Path to the NVMe device, either controller or namespace. + * @param nsid Namespace ID to identify. + * + * @return Vendor specific data string or NULL on failure. + */ +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); + if (ns == NULL) + { + fprintf(stderr, "failed to identify namespace for device=%s\n", device_path); + return NULL; + } + + char *vs = strndup((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 extracts the namespace id from it. + * + * @param namespace_path Path to the NVMe namespace device. + * + * @return Vendor specific data string or NULL on failure. + */ +char *nvme_identify_namespace_vs_for_namespace_device(const char *namespace_path) +{ + int nsid = get_nsid_from_namespace_device_path(namespace_path); + if (nsid < 0) + { + fprintf(stderr, "failed to parse namespace id: %s\n", namespace_path); + return NULL; + } + + return nvme_identify_namespace_vs(namespace_path, nsid); +} diff --git a/src/nvme.h b/src/nvme.h index 9c07a58..cab9259 100644 --- a/src/nvme.h +++ b/src/nvme.h @@ -1,8 +1,12 @@ /** * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE in the project root for license information. + * Licensed under the MIT License. See LICENSE in the project root for license + * information. */ +#ifndef __NVME_H__ +#define __NVME_H__ + #include struct nvme_lbaf @@ -52,3 +56,14 @@ struct nvme_id_ns __u8 rsvd192[192]; __u8 vs[3712]; }; + +#define NVME_ADMIN_IDENTFY_NAMESPACE_OPCODE 0x06 + +int nvme_nsid_from_namespace_device_path(const char *namespace_path); +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__ diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..a826ed5 --- /dev/null +++ b/src/util.c @@ -0,0 +1,75 @@ +/** + * 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" + +/** + * Read file contents as a null-terminated string. + * + * For sysfs entries, etc. where the file contents are not null-terminated and + * we know the contents are just a simple string (e.g. sysfs attributes). + */ +char *read_file_as_string(const char *path) +{ + DEBUG_PRINTF("reading %s...\n", path); + + FILE *file = fopen(path, "r"); + if (file == NULL) + { + fprintf(stderr, "failed to fopen %s: %m\n", path); + return NULL; + } + + // Determine the file size + if (fseek(file, 0, SEEK_END) < 0) + { + fprintf(stderr, "failed to fseek on %s: %m\n", path); + fclose(file); + return NULL; + } + + long length = ftell(file); + if (length < 0) + { + fprintf(stderr, "failed to ftell on %s: %m\n", path); + fclose(file); + return NULL; + } + + if (fseek(file, 0, SEEK_SET) < 0) + { + fprintf(stderr, "failed to fseek on %s: %m\n", path); + fclose(file); + return NULL; + } + + // Allocate size of file plus one byte for null. + char *contents = malloc(length + 1); + if (contents == NULL) + { + fprintf(stderr, "failed to malloc for %s: %m\n", path); + fclose(file); + return NULL; + } + + // Read file contents. + size_t bytes_read = fread(contents, 1, length, file); + if ((long)bytes_read < length) + { + DEBUG_PRINTF("short read on %s, contents probably changed", path); + } + + fclose(file); + + // Null-terminate the string. + contents[bytes_read] = '\0'; + + DEBUG_PRINTF("%s => %s\n", path, contents); + return contents; +} diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..c78c57a --- /dev/null +++ b/src/util.h @@ -0,0 +1,12 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license + * information. + */ + +#ifndef __UTIL_H__ +#define __UTIL_H__ + +char *read_file_as_string(const char *path); + +#endif // __UTIL_H__ diff --git a/tests/capture.c b/tests/capture.c new file mode 100644 index 0000000..1f088e5 --- /dev/null +++ b/tests/capture.c @@ -0,0 +1,171 @@ +/** + * 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" + +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); +} + +/** + * Capture stdout and stderr using non-blocking pipes. + */ +void 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); +} + +/** + * 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. + */ +void 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); +} diff --git a/tests/capture.h b/tests/capture.h new file mode 100644 index 0000000..e68d188 --- /dev/null +++ b/tests/capture.h @@ -0,0 +1,15 @@ +/** + * 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__ + +void capture_setup(void **state); +void capture_teardown(void **state); +const char *capture_stdout(); +const char *capture_stderr(); + +#endif // __CAPTURE_H__ diff --git a/tests/debug_tests.c b/tests/debug_tests.c new file mode 100644 index 0000000..8fc035b --- /dev/null +++ b/tests/debug_tests.c @@ -0,0 +1,90 @@ +/** + * 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 + +// 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); +} diff --git a/tests/identify_disks_tests.c b/tests/identify_disks_tests.c new file mode 100644 index 0000000..faffb15 --- /dev/null +++ b/tests/identify_disks_tests.c @@ -0,0 +1,338 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// clang-format off +#include +// clang-format on + +#include "capture.h" +#include "identify_disks.h" + +char *fake_sys_class_nvme_path = "/sys/class/nvme"; + +char *__wrap_nvme_identify_namespace_vs_for_namespace_device(const char *namespace_path) +{ + check_expected_ptr(namespace_path); + return mock_ptr_type(char *); +} + +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; +} + +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); + + int result = identify_disks(); + + assert_int_equal(result, 0); + assert_string_equal(capture_stderr(), expected_string); + assert_string_equal(capture_stdout(), ""); +} + +static void test_identify_disks_no_nvme_devices(void **state) +{ + (void)state; // Unused parameter + + int result = identify_disks(); + + assert_int_equal(result, 0); + assert_string_equal(capture_stderr(), ""); + assert_string_equal(capture_stdout(), ""); +} + +static void test_identify_disks_vs_error(void **state) +{ + (void)state; // Unused parameter + + // nvme5: microsoft disk, empty vs for nsid=2, error on nsid=3 (should be + // skipped) + create_file(fake_sys_class_nvme_path, "nvme5/device/vendor", "0x1414"); + 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/nvme5n3"); + create_dir(fake_sys_class_nvme_path, "nvme5/nvme5n4"); + + expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme5n1"); + will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, + strdup("key1=nvme5n1value1,key2=nvme5n1value2")); + expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme5n2"); + will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, strdup("")); + expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme5n3"); + will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, NULL); + expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme5n4"); + will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, + strdup("key1=nvme5n4value1,key2=nvme5n4value2")); + + int result = identify_disks(); + + assert_int_equal(result, 0); + assert_string_equal(capture_stderr(), ""); + assert_string_equal(capture_stdout(), "/dev/nvme5n1: key1=nvme5n1value1,key2=nvme5n1value2\n" + "/dev/nvme5n2: \n" + "/dev/nvme5n4: key1=nvme5n4value1,key2=nvme5n4value2\n"); +} + +static void test_identify_disks_success_no_namespaces(void **state) +{ + (void)state; // Unused parameter + + create_file(fake_sys_class_nvme_path, "nvme0/device/vendor", "0x1414"); + + int result = identify_disks(); + + assert_int_equal(result, 0); + assert_string_equal(capture_stderr(), ""); + assert_string_equal(capture_stdout(), ""); +} + +static void test_identify_disks_success_one_namespace(void **state) +{ + (void)state; // Unused parameter + + create_file(fake_sys_class_nvme_path, "nvme1/device/vendor", "0x1414"); + create_dir(fake_sys_class_nvme_path, "nvme1/nvme1n1"); + + expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme1n1"); + will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, + strdup("key1=nvme1n1value1,key2=nvme1n1value2")); + + int result = identify_disks(); + + assert_int_equal(result, 0); + assert_string_equal(capture_stderr(), ""); + assert_string_equal(capture_stdout(), "/dev/nvme1n1: key1=nvme1n1value1,key2=nvme1n1value2\n"); +} + +static void test_identify_disks_success_two_namespaces(void **state) +{ + (void)state; // Unused parameter + + create_file(fake_sys_class_nvme_path, "nvme2/device/vendor", "0x1414"); + create_dir(fake_sys_class_nvme_path, "nvme2/nvme2n1"); + create_dir(fake_sys_class_nvme_path, "nvme2/nvme2n2"); + + expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme2n1"); + will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, + strdup("key1=nvme2n1value1,key2=nvme2n1value2")); + expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme2n2"); + will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, + strdup("key1=nvme2n2value1,key2=nvme2n2value2")); + + int result = identify_disks(); + + assert_int_equal(result, 0); + assert_string_equal(capture_stderr(), ""); + assert_string_equal(capture_stdout(), "/dev/nvme2n1: key1=nvme2n1value1,key2=nvme2n1value2\n" + "/dev/nvme2n2: key1=nvme2n2value1,key2=nvme2n2value2\n"); +} + +static void test_identify_disks_success_non_microsoft_controller(void **state) +{ + (void)state; // Unused parameter + + create_file(fake_sys_class_nvme_path, "nvme4/device/vendor", "0x0000"); + create_dir(fake_sys_class_nvme_path, "nvme4/nvme4n1"); + create_dir(fake_sys_class_nvme_path, "nvme4/nvme4n2"); + + int result = identify_disks(); + + assert_int_equal(result, 0); + assert_string_equal(capture_stderr(), ""); + assert_string_equal(capture_stdout(), ""); +} + +static void test_identify_disks_combined(void **state) +{ + (void)state; // Unused parameter + + // nvme0: microsoft disk, no namespaces + create_file(fake_sys_class_nvme_path, "nvme0/device/vendor", "0x1414"); + + // nvme1: microsoft disk, one namespace + create_file(fake_sys_class_nvme_path, "nvme1/device/vendor", "0x1414"); + create_dir(fake_sys_class_nvme_path, "nvme1/nvme1n1"); + + expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme1n1"); + will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, + strdup("key1=nvme1n1value1,key2=nvme1n1value2")); + + // nvme2: microsoft disk, two namespaces + create_file(fake_sys_class_nvme_path, "nvme2/device/vendor", "0x1414"); + create_dir(fake_sys_class_nvme_path, "nvme2/nvme2n1"); + create_dir(fake_sys_class_nvme_path, "nvme2/nvme2n2"); + + expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme2n1"); + will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, + strdup("key1=nvme2n1value1,key2=nvme2n1value2")); + expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme2n2"); + will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, + strdup("key1=nvme2n2value1,key2=nvme2n2value2")); + + // nvme4: non-microsoft disk + create_file(fake_sys_class_nvme_path, "nvme4/device/vendor", "0x0000"); + create_dir(fake_sys_class_nvme_path, "nvme4/nvme4n1"); + create_dir(fake_sys_class_nvme_path, "nvme4/nvme4n2"); + + // nvme5: microsoft disk, empty vs for nsid=2, error on nsid=3 (should be + // skipped) + create_file(fake_sys_class_nvme_path, "nvme5/device/vendor", "0x1414"); + 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/nvme5n3"); + create_dir(fake_sys_class_nvme_path, "nvme5/nvme5n4"); + + expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme5n1"); + will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, + strdup("key1=nvme5n1value1,key2=nvme5n1value2")); + expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme5n2"); + will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, strdup("")); + expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme5n3"); + will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, NULL); + expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme5n4"); + will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, + strdup("key1=nvme5n4value1,key2=nvme5n4value2")); + + int result = identify_disks(); + + assert_int_equal(result, 0); + assert_string_equal(capture_stderr(), ""); + 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"); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(test_identify_disks_no_sys_class_nvme_present, setup, teardown), + cmocka_unit_test_setup_teardown(test_identify_disks_no_nvme_devices, setup, teardown), + cmocka_unit_test_setup_teardown(test_identify_disks_vs_error, setup, teardown), + cmocka_unit_test_setup_teardown(test_identify_disks_success_no_namespaces, setup, teardown), + cmocka_unit_test_setup_teardown(test_identify_disks_success_one_namespace, setup, teardown), + cmocka_unit_test_setup_teardown(test_identify_disks_success_two_namespaces, setup, teardown), + cmocka_unit_test_setup_teardown(test_identify_disks_success_non_microsoft_controller, setup, teardown), + cmocka_unit_test_setup_teardown(test_identify_disks_combined, setup, teardown), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/tests/identify_udev_tests.c b/tests/identify_udev_tests.c new file mode 100644 index 0000000..59b8e54 --- /dev/null +++ b/tests/identify_udev_tests.c @@ -0,0 +1,143 @@ +/** + * 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 + +// clang-format off +#include +// clang-format on + +#include "capture.h" +#include "identify_udev.h" +#include "nvme.h" + +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_for_vs_success(void **state) +{ + (void)state; // Unused parameter + + char vs[] = "type=local,index=2,name=nvme-600G-2"; + + int result = print_udev_key_values_for_vs(vs); + + assert_int_equal(result, 0); + 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"; + + int result = print_udev_key_values_for_vs(vs); + + assert_int_equal(result, 1); + 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_success(void **state) +{ + (void)state; // Unused parameter + + setenv("DEVNAME", "/dev/nvme0n5", 1); + const char *vs = 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); + + int result = identify_udev_device(); + + assert_int_equal(result, 0); + 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, 1); + assert_string_equal(capture_stderr(), "environment variable 'DEVNAME' not set\n"); + assert_string_equal(capture_stdout(), ""); +} + +static void test_identify_udev_device_vs_failure(void **state) +{ + (void)state; // Unused parameter + + setenv("DEVNAME", "/dev/nvme0n5", 1); + + expect_string(__wrap_nvme_identify_namespace_vs_for_namespace_device, namespace_path, "/dev/nvme0n5"); + will_return(__wrap_nvme_identify_namespace_vs_for_namespace_device, NULL); + + int result = identify_udev_device(); + + assert_int_equal(result, 1); + assert_string_equal(capture_stderr(), "failed to query namespace vendor-specific data: /dev/nvme0n5\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_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_success, setup, teardown), + cmocka_unit_test_setup_teardown(test_identify_udev_device_no_devname, setup, teardown), + cmocka_unit_test_setup_teardown(test_identify_udev_device_vs_failure, setup, teardown), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/tests/nvme_tests.c b/tests/nvme_tests.c new file mode 100644 index 0000000..b8f5a54 --- /dev/null +++ b/tests/nvme_tests.c @@ -0,0 +1,396 @@ +/** + * 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" +#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, +}; + +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; +} + +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) + { + errno = mock_type(int); + } + else if (ret == 0) + { + *memptr = malloc(size); + } + + return ret; +} + +int __wrap_ioctl(int fd, unsigned long request, ...) +{ + check_expected(fd); + check_expected(request); + + 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 *)cmd->addr, ns, sizeof(struct nvme_id_ns)); + ns = (struct nvme_id_ns *)cmd->addr; + } + + int ret = mock_type(int); + if (ret < 0) + { + errno = mock_type(int); + } + + return ret; +} + +int __wrap_close(int fd) +{ + check_expected(fd); + return mock_type(int); +} + +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); + + 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 + + expect_string(__wrap_open, pathname, OPEN_PATH); + expect_value(__wrap_open, flags, O_RDONLY); + will_return(__wrap_open, -1); + will_return(__wrap_open, EOWNERDEAD); + + struct nvme_id_ns *result = nvme_identify_namespace(OPEN_PATH, NSID); + + assert_null(result); + + 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_posix_memalign(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, -1); + will_return(__wrap_posix_memalign, ENOLINK); + + expect_value(__wrap_close, fd, OPEN_FD); + will_return(__wrap_close, 0); + + struct nvme_id_ns *result = nvme_identify_namespace(OPEN_PATH, NSID); + + assert_null(result); + 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 + + 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); + + struct nvme_id_ns *result = nvme_identify_namespace(OPEN_PATH, NSID); + + assert_null(result); + + 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_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 + + expect_string(__wrap_open, pathname, OPEN_PATH); + expect_value(__wrap_open, flags, O_RDONLY); + will_return(__wrap_open, -1); + will_return(__wrap_open, EOWNERDEAD); + + char *vs = nvme_identify_namespace_vs(OPEN_PATH, NSID); + + assert_null(vs); + + assert_string_equal(capture_stdout(), ""); + assert_string_equal(capture_stderr(), "failed to open /dev/nvme199n19: Owner died\n" + "failed to identify namespace for device=/dev/nvme199n19\n"); +} + +static void test_get_nsid_from_namespace_device_path_success(void **state) +{ + (void)state; // Unused parameter + + assert_int_equal(get_nsid_from_namespace_device_path("/dev/nvme0n5"), 5); + assert_int_equal(get_nsid_from_namespace_device_path("/dev/nvme2n12"), 12); + assert_int_equal(get_nsid_from_namespace_device_path("/dev/nvme100n1"), 1); + assert_int_equal(get_nsid_from_namespace_device_path("/dev/nvme100000n1"), 1); + assert_int_equal(get_nsid_from_namespace_device_path("/dev/nvme55n999"), 999); + + assert_string_equal(capture_stdout(), ""); + assert_string_equal(capture_stderr(), ""); +} + +static void test_get_nsid_from_namespace_device_path_failure(void **state) +{ + (void)state; // Unused parameter + + assert_int_equal(get_nsid_from_namespace_device_path("bad"), -1); + assert_int_equal(get_nsid_from_namespace_device_path("bad1n1"), -1); + assert_int_equal(get_nsid_from_namespace_device_path("/dev/bad1n1"), -1); + assert_int_equal(get_nsid_from_namespace_device_path("/dev/nvme0"), -1); + assert_int_equal(get_nsid_from_namespace_device_path("/dev/nvme0n"), -1); + assert_int_equal(get_nsid_from_namespace_device_path("/dev/nvme0nX"), -1); + + assert_string_equal(capture_stdout(), ""); + assert_string_equal(capture_stderr(), ""); +} + +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_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, 5); + 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); + + 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/nvme0nX"; + + char *result = nvme_identify_namespace_vs_for_namespace_device(path); + + assert_null(result); + assert_string_equal(capture_stdout(), ""); + assert_string_equal(capture_stderr(), "failed to parse namespace id: /dev/nvme0nX\n"); +} + +static void test_nvme_identify_namespace_vs_for_namespace_device_vs_failure(void **state) +{ + (void)state; // Unused parameter + + const char *path = "/dev/nvme0n5"; + + expect_string(__wrap_open, pathname, path); + expect_value(__wrap_open, flags, O_RDONLY); + will_return(__wrap_open, -1); + will_return(__wrap_open, EOWNERDEAD); + + char *result = nvme_identify_namespace_vs_for_namespace_device(path); + + assert_null(result); + assert_string_equal(capture_stdout(), ""); + assert_string_equal(capture_stderr(), "failed to open /dev/nvme0n5: Owner died\n" + "failed to identify namespace for device=/dev/nvme0n5\n"); +} + +static int setup(void **state) +{ + (void)state; // Unused parameter + + 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_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_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_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_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); +} diff --git a/tests/util_tests.c b/tests/util_tests.c new file mode 100644 index 0000000..aa1bae6 --- /dev/null +++ b/tests/util_tests.c @@ -0,0 +1,241 @@ +/** + * 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 + +// clange-format off +#include +// clange-format on + +#include "debug.h" +#include "util.h" + +bool debug = false; +bool use_mocks = true; // Global variable to control whether mocks are enabled + +FILE *__real_fopen(const char *path, const char *mode); +FILE *__wrap_fopen(const char *path, const char *mode) +{ + if (!use_mocks) + { + return __real_fopen(path, mode); + } + check_expected_ptr(path); + check_expected_ptr(mode); + return mock_ptr_type(FILE *); +} + +int __real_fseek(FILE *stream, long offset, int whence); +int __wrap_fseek(FILE *stream, long offset, int whence) +{ + if (!use_mocks) + { + return __real_fseek(stream, offset, whence); + } + check_expected_ptr(stream); + check_expected(offset); + check_expected(whence); + return mock_type(int); +} + +long __real_ftell(FILE *stream); +long __wrap_ftell(FILE *stream) +{ + if (!use_mocks) + { + return __real_ftell(stream); + } + check_expected_ptr(stream); + return mock_type(long); +} + +size_t __real_fread(void *ptr, size_t size, size_t nmemb, FILE *stream); +size_t __wrap_fread(void *ptr, size_t size, size_t nmemb, FILE *stream) +{ + if (!use_mocks) + { + return __real_fread(ptr, size, nmemb, stream); + } + check_expected_ptr(ptr); + check_expected(size); + check_expected(nmemb); + check_expected_ptr(stream); + + size_t ret = mock_type(size_t); + if (ret > 0) + { + char *src_contents = mock_ptr_type(char *); + strncpy(ptr, src_contents, ret); + } + + return ret; +} + +int __real_fclose(FILE *stream); +int __wrap_fclose(FILE *stream) +{ + if (!use_mocks) + { + return __real_fclose(stream); + } + check_expected_ptr(stream); + return mock_type(int); +} + +void *__real_malloc(size_t size); +void *__wrap_malloc(size_t size) +{ + if (!use_mocks) + { + return __real_malloc(size); + } + check_expected(size); + return mock_ptr_type(void *); +} + +// Setup function to reset use_mocks to true +static int setup(void **state) +{ + (void)state; // Unused parameter + use_mocks = true; + return 0; +} + +static void test_read_file_as_string_success(void **state) +{ + (void)state; // Unused parameter + + const char *path = "/path/to/file"; + const char *file_contents = "file contents"; + char *malloc_buffer = __real_malloc(sizeof(file_contents)); + + expect_string(__wrap_fopen, path, path); + expect_string(__wrap_fopen, mode, "r"); + will_return(__wrap_fopen, (FILE *)0x1); + + expect_any(__wrap_fseek, stream); + expect_value(__wrap_fseek, offset, 0); + expect_value(__wrap_fseek, whence, SEEK_END); + will_return(__wrap_fseek, 0); + + expect_any(__wrap_ftell, stream); + will_return(__wrap_ftell, strlen(file_contents)); + + expect_any(__wrap_fseek, stream); + expect_value(__wrap_fseek, offset, 0); + expect_value(__wrap_fseek, whence, SEEK_SET); + will_return(__wrap_fseek, 0); + + expect_value(__wrap_malloc, size, strlen(file_contents) + 1); + will_return(__wrap_malloc, (void *)malloc_buffer); + + expect_any(__wrap_fread, ptr); + expect_value(__wrap_fread, size, 1); + expect_value(__wrap_fread, nmemb, strlen(file_contents)); + expect_any(__wrap_fread, stream); + will_return(__wrap_fread, strlen(file_contents)); + will_return(__wrap_fread, file_contents); + + expect_any(__wrap_fclose, stream); + will_return(__wrap_fclose, 0); + + char *result = read_file_as_string(path); + assert_non_null(result); + assert_string_equal(result, file_contents); + + free(malloc_buffer); +} + +static void test_read_file_as_string_fopen_failure(void **state) +{ + (void)state; // Unused parameter + + const char *path = "/path/to/nonexistent/file"; + + expect_string(__wrap_fopen, path, path); + expect_string(__wrap_fopen, mode, "r"); + will_return(__wrap_fopen, NULL); + + char *result = read_file_as_string(path); + assert_null(result); +} + +static void test_read_file_as_string_malloc_failure(void **state) +{ + (void)state; // Unused parameter + + const char *path = "/path/to/file"; + const char *file_contents = "file contents"; + + expect_string(__wrap_fopen, path, path); + expect_string(__wrap_fopen, mode, "r"); + will_return(__wrap_fopen, (FILE *)0x1); + + expect_any(__wrap_fseek, stream); + expect_value(__wrap_fseek, offset, 0); + expect_value(__wrap_fseek, whence, SEEK_END); + will_return(__wrap_fseek, 0); + + expect_any(__wrap_ftell, stream); + will_return(__wrap_ftell, strlen(file_contents)); + + expect_any(__wrap_fseek, stream); + expect_value(__wrap_fseek, offset, 0); + expect_value(__wrap_fseek, whence, SEEK_SET); + will_return(__wrap_fseek, 0); + + expect_value(__wrap_malloc, size, strlen(file_contents) + 1); + will_return(__wrap_malloc, NULL); + + expect_any(__wrap_fclose, stream); + will_return(__wrap_fclose, 0); + + char *result = read_file_as_string(path); + assert_null(result); +} + +static void test_read_file_as_string_no_mocks(void **state) +{ + (void)state; // Unused parameter + + use_mocks = false; // Disable mocks + + const char *path = "test_file.txt"; + const char *file_contents = "file contents"; + + // Create a temporary file and write contents to it + FILE *file = fopen(path, "w"); + assert_non_null(file); + fwrite(file_contents, 1, strlen(file_contents), file); + fclose(file); + + // Read the file using the function + char *result = read_file_as_string(path); + assert_non_null(result); + assert_string_equal(result, file_contents); + + // Clean up + free(result); + remove(path); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup(test_read_file_as_string_success, setup), + cmocka_unit_test_setup(test_read_file_as_string_fopen_failure, setup), + cmocka_unit_test_setup(test_read_file_as_string_malloc_failure, setup), + cmocka_unit_test_setup(test_read_file_as_string_no_mocks, setup), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +}