diff --git a/client-lite/CMakeLists.txt b/client-lite/CMakeLists.txt index d0a72836..35d95007 100644 --- a/client-lite/CMakeLists.txt +++ b/client-lite/CMakeLists.txt @@ -68,8 +68,16 @@ file (GLOB files_docs_common src/util/*.cpp ) -# Enable easy debugging in devdebug mode by not requiring running as root -if (DO_DEV_DEBUG) +if (DO_BUILD_FOR_SNAP) + # Use relative paths here. Agent will prefix them with $SNAP_DATA environment variable at runtime. + # This should match the slots declared in snapcraft-agent.yaml. + # These cmake vars are used in Debian maintainer scripts like postrm/postinst but snap does not use those scripts. + message("Agent: Build to create an Ubuntu Snap package") + set(docs_svc_config_dir_path "etc") + set(docs_svc_log_dir_path "log") + set(docs_svc_run_dir_path "run") +elseif (DO_DEV_DEBUG) + # Enable easy debugging in devdebug mode by not requiring running as root message("Agent: Dev debug mode") set(docs_svc_config_dir_path "/tmp/etc/${DOSVC_BIN_NAME}") set(docs_svc_log_dir_path "/tmp/log/${DOSVC_BIN_NAME}") diff --git a/client-lite/src/exe/docs.cpp b/client-lite/src/exe/docs.cpp index 23fc5a56..8360ce00 100644 --- a/client-lite/src/exe/docs.cpp +++ b/client-lite/src/exe/docs.cpp @@ -139,15 +139,17 @@ HRESULT Run() try RestPortAdvertiser portAdvertiser(controller.Port()); DoLogInfo("Port number written to %s", portAdvertiser.OutFilePath().data()); - + #ifndef DO_BUILD_FOR_SNAP DropPermissions(); #endif - DOLog::Init(docli::GetLogDirectory(), DOLog::Level::Verbose); DoLogInfo("Started, %s", msdoutil::ComponentVersion().c_str()); + DoLogInfo("**Paths**\nLog: %s\nRun: %s\nConfig: %s\nSdkConfig: %s\nAdminConfig: %s", + docli::GetLogDirectory().c_str(), docli::GetRuntimeDirectory().c_str(), docli::GetConfigDirectory().c_str(), + docli::GetSDKConfigFilePath().c_str(), docli::GetAdminConfigFilePath().c_str()); ProcessController procController([&downloadManager]() { diff --git a/client-lite/src/util/do_persistence.cpp b/client-lite/src/util/do_persistence.cpp index 68cac515..9e677e9d 100644 --- a/client-lite/src/util/do_persistence.cpp +++ b/client-lite/src/util/do_persistence.cpp @@ -4,36 +4,60 @@ #include "do_common.h" #include "do_persistence.h" +#include // std::getenv + namespace docli { +static std::string ConstructPath(const char* suffix) +{ + std::string outputPath; +#ifdef DO_BUILD_FOR_SNAP + const char* snapDataRoot = std::getenv("SNAP_DATA"); + THROW_HR_IF(E_UNEXPECTED, (snapDataRoot == nullptr) || (*snapDataRoot == '\0')); + outputPath = snapDataRoot; + if (outputPath.back() != '/') + { + outputPath.push_back('/'); + } + if (*suffix == '/') + { + ++suffix; + } + outputPath += suffix; +#else + outputPath = suffix; +#endif + return outputPath; +} + const std::string& GetLogDirectory() { - static std::string logDirectory(DO_AGENT_LOG_DIRECTORY_PATH); + static std::string logDirectory(ConstructPath(DO_AGENT_LOG_DIRECTORY_PATH)); return logDirectory; } const std::string& GetRuntimeDirectory() { - static std::string runDirectory(DO_RUN_DIRECTORY_PATH); + static std::string runDirectory(ConstructPath(DO_RUN_DIRECTORY_PATH)); return runDirectory; } const std::string& GetConfigDirectory() { - static std::string configDirectory(DO_CONFIG_DIRECTORY_PATH); + static std::string configDirectory(ConstructPath(DO_CONFIG_DIRECTORY_PATH)); return configDirectory; } const std::string& GetSDKConfigFilePath() { - static std::string configFilePath(DO_CONFIG_DIRECTORY_PATH "/sdk-config.json"); + static std::string configFilePath(ConstructPath(DO_CONFIG_DIRECTORY_PATH "/sdk-config.json")); return configFilePath; } const std::string& GetAdminConfigFilePath() { - static std::string configFilePath(DO_CONFIG_DIRECTORY_PATH "/admin-config.json"); + static std::string configFilePath(ConstructPath(DO_CONFIG_DIRECTORY_PATH "/admin-config.json")); return configFilePath; } diff --git a/client-lite/src/util/proc_launch_helper.h b/client-lite/src/util/proc_launch_helper.h index 2917dd64..6c4abfde 100644 --- a/client-lite/src/util/proc_launch_helper.h +++ b/client-lite/src/util/proc_launch_helper.h @@ -57,8 +57,8 @@ inline void InitializePath(const std::string& path, mode_t mode = 0) try boost::filesystem::path dirPath(path); if (!boost::filesystem::exists(dirPath)) { - DoLogInfo("Creating directory at %s", path.c_str()); - boost::filesystem::create_directory(dirPath); + DoLogInfo("Creating directories for %s", path.c_str()); + boost::filesystem::create_directories(dirPath); if (mode != 0) { @@ -80,8 +80,6 @@ inline void InitializeDOPaths() inline void DropPermissions() { - uid_t userid = GetUserIdByName("do"); - // process is running as root, drop privileges if (getuid() == 0) { @@ -94,6 +92,8 @@ inline void DropPermissions() { THROW_HR_MSG(E_FAIL, "setgid: Unable to drop group privileges: %u, errno: %d", groupid, errno); } + + uid_t userid = GetUserIdByName("do"); if (setuid(userid) != 0) { THROW_HR_MSG(E_FAIL, "setuid: Unable to drop user privileges: %u, errno: %d", userid, errno); diff --git a/sdk-cpp/CMakeLists.txt b/sdk-cpp/CMakeLists.txt index f43227d5..8c3070a1 100644 --- a/sdk-cpp/CMakeLists.txt +++ b/sdk-cpp/CMakeLists.txt @@ -124,6 +124,9 @@ target_compile_definitions(${DO_SDK_LIB_NAME} if (DO_DEV_DEBUG) target_compile_definitions(${DO_SDK_LIB_NAME} PRIVATE DO_DEV_DEBUG) endif () +if (DO_BUILD_FOR_SNAP) + target_compile_definitions(${DO_SDK_LIB_NAME} PRIVATE DO_BUILD_FOR_SNAP) +endif () if (DO_PLATFORM_LINUX OR DO_PLATFORM_MAC) # TODO(shishirb) Remove internal use of exceptions on these platforms target_compile_definitions(${DO_SDK_LIB_NAME} PRIVATE DO_ENABLE_EXCEPTIONS) diff --git a/sdk-cpp/src/internal/rest/util/do_persistence.cpp b/sdk-cpp/src/internal/rest/util/do_persistence.cpp index 0ca80df3..025dd655 100644 --- a/sdk-cpp/src/internal/rest/util/do_persistence.cpp +++ b/sdk-cpp/src/internal/rest/util/do_persistence.cpp @@ -2,9 +2,11 @@ // Licensed under the MIT License. #include - #include "do_persistence.h" +#include "do_errors.h" +#include "do_error_helpers.h" + namespace microsoft { namespace deliveryoptimization @@ -12,9 +14,33 @@ namespace deliveryoptimization namespace details { +#ifdef DO_BUILD_FOR_SNAP +static std::string ConstructPath(const char* suffix) +{ + const char* snapDataRoot = std::getenv("SNAP_DATA"); + if ((snapDataRoot == nullptr) || (*snapDataRoot == '\0')) + { + ThrowException(microsoft::deliveryoptimization::errc::unexpected); + } + std::string outputPath = snapDataRoot; + if (outputPath.back() != '/') + { + outputPath.push_back('/'); + } + if (*suffix == '/') + { + ++suffix; + } + outputPath += suffix; + return outputPath; +} +#endif + const std::string& GetRuntimeDirectory() { -#ifdef DO_DEV_DEBUG +#ifdef DO_BUILD_FOR_SNAP + static std::string runDirectory(ConstructPath("do-port-numbers")); +#elif defined(DO_DEV_DEBUG) static std::string runDirectory("/tmp/run/deliveryoptimization-agent"); #else static std::string runDirectory("/var/run/deliveryoptimization-agent"); @@ -24,7 +50,9 @@ const std::string& GetRuntimeDirectory() const std::string& GetConfigFilePath() { -#ifdef DO_DEV_DEBUG +#ifdef DO_BUILD_FOR_SNAP + static std::string configFilePath(ConstructPath("do-configs/sdk-config.json")); +#elif defined(DO_DEV_DEBUG) static std::string configFilePath("/tmp/etc/deliveryoptimization-agent/sdk-config.json"); #else static std::string configFilePath("/etc/deliveryoptimization-agent/sdk-config.json"); @@ -35,7 +63,9 @@ const std::string& GetConfigFilePath() // TODO(shishirb): this is used only in test const std::string& GetAdminConfigFilePath() { -#ifdef DO_DEV_DEBUG +#ifdef DO_BUILD_FOR_SNAP + static std::string configFilePath(ConstructPath("do-configs/admin-config.json")); +#elif defined(DO_DEV_DEBUG) static std::string configFilePath("/tmp/etc/deliveryoptimization-agent/admin-config.json"); #else static std::string configFilePath("/etc/deliveryoptimization-agent/admin-config.json"); diff --git a/sdk-cpp/tests/download_tests_common.cpp b/sdk-cpp/tests/download_tests_common.cpp index e8b9b1ca..616b07ab 100644 --- a/sdk-cpp/tests/download_tests_common.cpp +++ b/sdk-cpp/tests/download_tests_common.cpp @@ -588,14 +588,15 @@ TEST_F(DownloadTests, FileDeletionAfterPause) #endif } -#if defined(DO_INTERFACE_REST) +// SNAP: tests only have read permissions into the 'port numbers' directory +#if defined(DO_INTERFACE_REST) and !defined(DO_BUILD_FOR_SNAP) TEST_F(DownloadTests, SimpleBlockingDownloadTest_ClientNotRunning) { - TestHelpers::StopService("deliveryoptimization-agent.service"); + TestHelpers::StopService(g_docsSvcName.c_str()); auto startService = dotest::util::scope_exit([]() { - TestHelpers::StartService("deliveryoptimization-agent.service"); + TestHelpers::StartService(g_docsSvcName.c_str()); }); TestHelpers::DeleteRestPortFiles(); // can be removed if docs deletes file on shutdown @@ -614,11 +615,10 @@ TEST_F(DownloadTests, SimpleBlockingDownloadTest_ClientNotRunning) TEST_F(DownloadTests, SimpleBlockingDownloadTest_ClientNotRunningPortFilePresent) { - // TODO(shishirb) Service name should come from cmake - TestHelpers::StopService("deliveryoptimization-agent.service"); + TestHelpers::StopService(g_docsSvcName.c_str()); auto startService = dotest::util::scope_exit([]() { - TestHelpers::StartService("deliveryoptimization-agent.service"); + TestHelpers::StartService(g_docsSvcName.c_str()); }); TestHelpers::DeleteRestPortFiles(); TestHelpers::CreateRestPortFiles(1); @@ -638,10 +638,10 @@ TEST_F(DownloadTests, SimpleBlockingDownloadTest_ClientNotRunningPortFilePresent TEST_F(DownloadTests, MultipleRestPortFileExists_Download) { - TestHelpers::StopService("deliveryoptimization-agent.service"); + TestHelpers::StopService(g_docsSvcName.c_str()); auto startService = dotest::util::scope_exit([]() { - TestHelpers::StartService("deliveryoptimization-agent.service"); + TestHelpers::StartService(g_docsSvcName.c_str()); }); TestHelpers::CreateRestPortFiles(5); ASSERT_GE(TestHelpers::CountRestPortFiles(), 5u); diff --git a/sdk-cpp/tests/rest/port_discovery_tests.cpp b/sdk-cpp/tests/rest/port_discovery_tests.cpp index 3ad0d690..6bd2214d 100644 --- a/sdk-cpp/tests/rest/port_discovery_tests.cpp +++ b/sdk-cpp/tests/rest/port_discovery_tests.cpp @@ -52,8 +52,13 @@ void PortDiscoveryTests::TearDown() TestHelpers::StartService(g_docsSvcName); } +// SNAP: tests only have read permissions into the 'port numbers' directory +#ifndef DO_BUILD_FOR_SNAP + TEST_F(PortDiscoveryTests, DiscoverPortTest) { std::string foundPort = msdod::CPortFinder::GetDOPort(false); ASSERT_EQ(foundPort, samplePortNumber); } + +#endif diff --git a/sdk-cpp/tests/rest/test_helpers.cpp b/sdk-cpp/tests/rest/test_helpers.cpp index 02d62c5b..62c58d11 100644 --- a/sdk-cpp/tests/rest/test_helpers.cpp +++ b/sdk-cpp/tests/rest/test_helpers.cpp @@ -46,21 +46,24 @@ int TestHelpers::ShutdownProcess(std::string procName) void TestHelpers::RestartService(const std::string& name) { +#ifndef DO_BUILD_FOR_SNAP + // 'snap' does not support reset-failed const auto resetCmd = dtu::FormatString("systemctl reset-failed %s", name.c_str()); dtu::ExecuteSystemCommand(resetCmd.data()); // avoids hitting start-limit-hit error in systemd +#endif - const auto restartCmd = dtu::FormatString("systemctl restart %s", name.c_str()); + const auto restartCmd = dtu::FormatString(DO_SERVICE_CONTROLLER " restart %s", name.c_str()); dtu::ExecuteSystemCommand(restartCmd.data()); } void TestHelpers::StartService(const std::string& name) { - dtu::ExecuteSystemCommand(dtu::FormatString("systemctl start %s", name.c_str()).data()); + dtu::ExecuteSystemCommand(dtu::FormatString(DO_SERVICE_CONTROLLER " start %s", name.c_str()).data()); } void TestHelpers::StopService(const std::string& name) { - dtu::ExecuteSystemCommand(dtu::FormatString("systemctl stop %s", name.c_str()).data()); + dtu::ExecuteSystemCommand(dtu::FormatString(DO_SERVICE_CONTROLLER " stop %s", name.c_str()).data()); } int TestHelpers::_KillProcess(int pid, int signal) diff --git a/sdk-cpp/tests/test_data.cpp b/sdk-cpp/tests/test_data.cpp index 577c2b0b..cbc01004 100644 --- a/sdk-cpp/tests/test_data.cpp +++ b/sdk-cpp/tests/test_data.cpp @@ -11,9 +11,10 @@ const uint64_t g_largeFileSizeBytes = 536870440u; const uint64_t g_prodFileSizeBytes = 25006511u; #ifdef DO_BUILD_FOR_SNAP - std::string downloadPath = "/var/lib/deliveryoptimization-snap-downloads-root"; +// For testing production scenario, use the same path DU agent will use +static const std::string downloadPath = "/var/lib/deviceupdate-agent-downloads"; #else - std::string downloadPath = (boost::filesystem::temp_directory_path()).string(); +static const std::string downloadPath = boost::filesystem::temp_directory_path().string(); #endif const std::string g_largeFileUrl = "http://main.oremdl.microsoft.com.nsatc.net/dotc/ReplacementDCATFile.txt"; @@ -35,9 +36,14 @@ const std::chrono::seconds g_smallFileWaitTime = 10s; const std::chrono::seconds g_largeFileWaitTime = 5min; #if defined(DO_INTERFACE_REST) -const std::string g_docsProcName = "deliveryoptimization-agent"; + +#ifdef DO_BUILD_FOR_SNAP +const std::string g_docsSvcName = "deliveryoptimization-client.agent"; +#else const std::string g_docsSvcName = "deliveryoptimization-agent.service"; #endif +#endif + // This MCC instance only works within our test lab azure VMs. Can be overriden via cmdline. std::string g_mccHostName = "10.1.0.70"; diff --git a/sdk-cpp/tests/test_data.h b/sdk-cpp/tests/test_data.h index da6eec6a..6ffeba93 100644 --- a/sdk-cpp/tests/test_data.h +++ b/sdk-cpp/tests/test_data.h @@ -7,12 +7,19 @@ #include #include +#ifdef DO_BUILD_FOR_SNAP +// Using systemctl within snap package results in "Running in chroot, ignoring request". +// Use the snap command instead. +#define DO_SERVICE_CONTROLLER "snap" +#else +#define DO_SERVICE_CONTROLLER "systemctl" +#endif + extern const uint64_t g_smallFileSizeBytes; extern const uint64_t g_largeFileSizeBytes; extern const uint64_t g_prodFileSizeBytes; #if defined(DO_INTERFACE_REST) -extern const std::string g_docsProcName; extern const std::string g_docsSvcName; #elif defined(DO_INTERFACE_COM) extern const std::string g_smallFilePhfInfoJson; diff --git a/snapcraft-options/connect-snaps.sh b/snapcraft-options/connect-snaps.sh new file mode 100755 index 00000000..3ba11251 --- /dev/null +++ b/snapcraft-options/connect-snaps.sh @@ -0,0 +1,15 @@ +#! /bin/bash + +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# $ snap connect : : + +set -e + +# Plug agent snap into sdk-tests snap downloads slot +sudo snap connect deliveryoptimization-client:deviceupdate-agent-downloads deliveryoptimization-sdk-tests:downloads-folder + +# Plug sdk-tests snap into agent's run and config slots +sudo snap connect deliveryoptimization-sdk-tests:do-port-numbers deliveryoptimization-client:do-port-numbers +sudo snap connect deliveryoptimization-sdk-tests:do-configs deliveryoptimization-client:do-configs diff --git a/snapcraft-options/snapcraft-agent.yaml b/snapcraft-options/snapcraft-agent.yaml index ba61fcd0..83874071 100644 --- a/snapcraft-options/snapcraft-agent.yaml +++ b/snapcraft-options/snapcraft-agent.yaml @@ -75,23 +75,29 @@ apps: - network-bind slots: - port-number: + do-port-numbers: interface: content - content: port-number - read: [ $SNAP_DATA/var/run/deliveryoptimization-agent ] + content: do-port-numbers + read: [ $SNAP_DATA/run ] - config-file: + do-configs: interface: content - content: config-file - write: [ $SNAP_DATA/etc/deliveryoptimization-agent/sdk-config.json ] + content: do-configs + write: [ $SNAP_DATA/etc ] plugs: - client-downloads-folder: + deviceupdate-agent-downloads: interface: content - content: client-downloads-folder - target: $SNAP_DATA/deliveryoptimization-snap-downloads-root + content: deviceupdate-agent-downloads + target: $SNAP_DATA/deviceupdate-agent-downloads layout: -# adu_data_dir - /var/lib/deliveryoptimization-snap-downloads-root: - symlink: $SNAP_DATA/deliveryoptimization-snap-downloads-root \ No newline at end of file + # DU agent will provide the /var/lib path as download location. + # Map it to the correct path within this snap. + /var/lib/deviceupdate-agent-downloads: + symlink: $SNAP_DATA/deviceupdate-agent-downloads + + # Not using layout for /var/run because /var/run is already a symlink in Ubuntu 20.04, + # and package startup fails if we want another symlink there. + # Not using /var/log and /etc just for consistency: snap adn non-snap versions do not use + # the same directory paths. diff --git a/snapcraft-options/snapcraft-sdk.yaml b/snapcraft-options/snapcraft-sdk.yaml index a2ee89d0..95fa8cd5 100644 --- a/snapcraft-options/snapcraft-sdk.yaml +++ b/snapcraft-options/snapcraft-sdk.yaml @@ -53,23 +53,29 @@ apps: command: bin/deliveryoptimization-sdk-tests plugs: - do-port-number: + do-port-numbers: interface: content - content: do-port-number - target: $SNAP_DATA/do-port-number + content: do-port-numbers + target: $SNAP_DATA/do-port-numbers - do-config-file: + do-configs: interface: content - content: do-config-file - target: $SNAP_DATA/do-config-file + content: do-configs + target: $SNAP_DATA/do-configs slots: downloads-folder: interface: content content: downloads-folder - write: - - $SNAP_DATA/var/lib/deliveryoptimization-snap-downloads-root + write: [ $SNAP_DATA/var/lib/deviceupdate-agent-downloads ] layout: - /var/lib/deliveryoptimization-snap-downloads-root: - symlink: $SNAP_DATA/var/lib/deliveryoptimization-snap-downloads-root \ No newline at end of file + # For testing production scenario, use the same path DU agent will use. + # The path is referred to in sdk-cpp/tests/test_data.cpp. + /var/lib/deviceupdate-agent-downloads: + symlink: $SNAP_DATA/var/lib/deviceupdate-agent-downloads + + # Not using layout for agent's restport files because /var/run is already a symlink in Ubuntu 20.04, + # and package startup fails if we want another symlink there. + # Not using layout for the configs, the /etc directory, just for consistency: snap and non-snap versions use + # different directory paths. diff --git a/snapcraft-options/ubuntu-snap.md b/snapcraft-options/ubuntu-snap.md new file mode 100644 index 00000000..a256bb48 --- /dev/null +++ b/snapcraft-options/ubuntu-snap.md @@ -0,0 +1,58 @@ +# Ubuntu Snap packages - What is it, how to build, and how to test + +## What +https://snapcraft.io/docs + +## Building +Run the commands in the root of the repo. + +- First time build or after modifying corresponding yaml file: + - `$ ./build/build-snaps.sh agent` + - `$ ./build/build-snaps.sh sdk-tests` + +- Subsequent builds of the same component without modifying the corresponding yaml file: + - `$ snapcraft` + +- The build will generate *.snap files in the current working directory. Example: **deliveryoptimization-sdk-tests_0.1_amd64.snap**. + +## Installing +- `$ sudo snap install --devmode ./deliveryoptimization-sdk-tests_0.1_amd64.snap` +- `$ sudo snap install --devmode ./deliveryoptimization-client_0.1_amd64.snap` + +## The snap environment +It is a fruitful exercise to look around in the host file system and see how snap structures the installed snaps. Look at these paths to start with: +- /snap/ +- /var/snap/ + +## Connecting plugs and slots +- For the SDK test snap to work, the plugs and slots must be connected. Run the **connect-snaps.sh** script after both snaps are installed. +- The connections can be listed using this command: + - `$ snap connections | grep delivery` + +## Running or executing +- Agent + - The agent is declared as a daemon/service, so it starts running immediately after successful installation. + - Stop service: `$ sudo snap stop deliveryoptimization-client.agent` + - Start service: `$ sudo snap start deliveryoptimization-client.agent` + - Read service journal: `$ sudo snap logs deliveryoptimization-client.agent` + - systemctl can also be used with the correct service unit name: `$ systemctl status snap.deliveryoptimization-client.agent.service` + - **journalctl** can also be used to follow logs: `$ journalctl -f -u snap.deliveryoptimization-client.agent.service` + +- SDK tests + - This snap declares the **deliveryoptimization-sdk-tests** binary as an app. + - One way to run the tests is: `$ sudo snap run deliveryoptimization-sdk-tests.sdk-tests --gtest_filter=*SimpleDownloadTest` + - Advanced usage: get a shell into the snap and invoke the binary directly. This also allows us to inspect the runtime environment + more easily, like listing environment variables, listing files/dirs, etc.
+ Use `$ sudo snap run --shell deliveryoptimization-sdk-tests.sdk-tests` to open a shell into the snap.
+ Then we can use regular shell commands like `printenv | grep SNAP`, `cd $SNAP`, `ls -l $SNAP_DATA/do-port-numbers/`
+ We can also run the test binary like so: `cd $SNAP` and then `bin/deliveryoptimization-sdk-tests --gtest_filter=*SimpleDownloadTest` + +## Uninstalling or cleaning up +- Uninstall installed snap packages + - `$ sudo snap remove deliveryoptimization-client` + - `$ sudo snap remove deliveryoptimization-sdk-tests` + +- Cleaning up build environment: Building sometimes fails with strange errors from **multipass**. Cleaning up the multipass instance helps. + - List the available instances: `$ multipass list` + - Delete an instance, example: `$ multipass delete snapcraft-deliveryoptimization-client` + - Purge deleted instances: `$ multipass purge`