diff --git a/builds/misc/images.yaml b/builds/misc/images.yaml index 55fe94da8c0..ff849669018 100644 --- a/builds/misc/images.yaml +++ b/builds/misc/images.yaml @@ -19,15 +19,39 @@ jobs: - script: scripts/linux/installPrereqs.sh -u $(NetCorePackageUri) name: install displayName: Install dependencies + + - bash: 'docker login $(registry.address) --username $(registry.user) --password $(registry.password)' + displayName: 'Docker Login' + + - script: edgelet/build/linux/install.sh --package-arm + displayName: Install Rust + + - bash: 'echo "##vso[task.setvariable variable=PATH;]$HOME/.cargo/bin:$PATH"' + displayName: Modify path + + - bash: 'cargo install --git https://github.com/myagley/cross.git --branch set-path' + displayName: 'Install cross (fork with docker fix)' + - script: scripts/linux/buildBranch.sh -c Release --no-rocksdb-bin name: build displayName: Build (release) + + - script: scripts/linux/buildDiagnostics.sh + displayName: Build iotedge-diagnostics + - task: PublishBuildArtifacts@1 displayName: 'Publish Artifacts' inputs: PathtoPublish: '$(Build.BinariesDirectory)/publish' ArtifactName: 'core-linux' + # azureiotedge-diagnostics + - template: templates/image-linux.yaml + parameters: + name: azureiotedge-diagnostics + imageName: azureiotedge-diagnostics + project: azureiotedge-diagnostics + # Edge Agent - template: templates/image-linux.yaml parameters: @@ -106,16 +130,30 @@ jobs: - powershell: scripts/windows/setup/Install-Prerequisites.ps1 -DotnetSdkUrl $(NetCorePackageUri) -Dotnet -Nuget name: install displayName: Install + + - script: echo $(registry.password)|docker login "edgebuilds.azurecr.io" -u "$(registry.user)" --password-stdin + displayName: Docker Login + - powershell: scripts/windows/build/Publish-Branch.ps1 -Configuration:"Release" -PublishTests:$False -UpdateVersion name: build displayName: Build + + - powershell: edgelet/build/windows/build-diagnostics.ps1 + displayName: Build iotedge-diagnostics + - task: PublishBuildArtifacts@1 displayName: 'Publish Artifacts' inputs: PathtoPublish: '$(Build.BinariesDirectory)/publish' ArtifactName: 'core-windows' - - script: echo $(registry.password)|docker login "edgebuilds.azurecr.io" -u "$(registry.user)" --password-stdin - displayName: Docker Login + + # azureiotedge-diagnostics + - template: templates/image-windows.yaml + parameters: + name: azureiotedge-diagnostics + imageName: azureiotedge-diagnostics + project: azureiotedge-diagnostics + arm32v7: 'false' # Edge Agent - template: templates/image-windows.yaml @@ -201,4 +239,6 @@ jobs: - script: scripts/linux/buildManifest.sh -r $(registry.address) -u $(registry.user) -p $(registry.password) -v $(Build.BuildNumber) -t $(System.DefaultWorkingDirectory)/edge-hub/docker/manifest.yaml.template -n microsoft --tags "$(tags)" displayName: 'Publish Edge Hub Manifest' - script: scripts/linux/buildManifest.sh -r $(registry.address) -u $(registry.user) -p $(registry.password) -v $(Build.BuildNumber) -t $(System.DefaultWorkingDirectory)/edge-modules/SimulatedTemperatureSensor/docker/manifest.yaml.template -n microsoft --tags "$(tags)" - displayName: 'Publish Temperature Sensor Manifest' \ No newline at end of file + displayName: 'Publish Temperature Sensor Manifest' + - script: scripts/linux/buildManifest.sh -r $(registry.address) -u $(registry.user) -p $(registry.password) -v $(Build.BuildNumber) -t $(System.DefaultWorkingDirectory)/edgelet/iotedge-diagnostics/docker/manifest.yaml.template -n microsoft --tags "$(tags)" + displayName: 'Publish azureiotedge-diagnostics Manifest' diff --git a/edgelet/Cargo.lock b/edgelet/Cargo.lock index 12386b68c2c..83b079c5755 100755 --- a/edgelet/Cargo.lock +++ b/edgelet/Cargo.lock @@ -323,6 +323,27 @@ name = "dtoa" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "edgelet-config" +version = "0.1.0" +dependencies = [ + "base64 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "config 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "edgelet-core 0.1.0", + "edgelet-docker 0.1.0", + "edgelet-utils 0.1.0", + "failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "url_serde 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "edgelet-core" version = "0.1.0" @@ -837,15 +858,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "iotedge" version = "0.1.0" dependencies = [ + "atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "chrono-humanize 0.0.11 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)", + "docker 0.1.0", + "edgelet-config 0.1.0", "edgelet-core 0.1.0", + "edgelet-docker 0.1.0", + "edgelet-http 0.1.0", "edgelet-http-mgmt 0.1.0", "failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.12.17 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "management 0.1.0", + "mini-sntp 0.1.0", + "native-tls 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl 0.10.12 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", "tabwriter 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "iotedge-diagnostics" +version = "0.1.0" +dependencies = [ + "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)", + "edgelet-core 0.1.0", + "futures 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.12.17 (registry+https://github.com/rust-lang/crates.io-index)", + "hyperlocal 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hyperlocal-windows 0.1.0 (git+https://github.com/Azure/hyperlocal-windows)", + "management 0.1.0", + "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -856,9 +910,9 @@ version = "0.1.0" dependencies = [ "base64 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)", - "config 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "docker 0.1.0", "dps 0.1.0", + "edgelet-config 0.1.0", "edgelet-core 0.1.0", "edgelet-docker 0.1.0", "edgelet-hsm 0.1.0", @@ -876,15 +930,12 @@ dependencies = [ "iothubservice 0.1.0", "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "provisioning 0.1.0", - "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", "sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-signal 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "url_serde 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "win-logger 0.1.0", "windows-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1085,6 +1136,14 @@ dependencies = [ "unicase 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "mini-sntp" +version = "0.1.0" +dependencies = [ + "chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "mio" version = "0.6.14" @@ -1298,9 +1357,9 @@ dependencies = [ name = "provisioning" version = "0.1.0" dependencies = [ - "base64 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "dps 0.1.0", + "edgelet-config 0.1.0", "edgelet-core 0.1.0", "edgelet-hsm 0.1.0", "edgelet-http 0.1.0", @@ -1309,7 +1368,6 @@ dependencies = [ "futures 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", "hsm 0.1.0", "log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/edgelet/Cargo.toml b/edgelet/Cargo.toml index bae7759297d..f86c77d66d5 100644 --- a/edgelet/Cargo.toml +++ b/edgelet/Cargo.toml @@ -2,6 +2,7 @@ members = [ "docker-rs", "dps", + "edgelet-config", "edgelet-core", "edgelet-docker", "edgelet-hsm", @@ -19,8 +20,10 @@ members = [ "iotedge", "iotedged", "iotedged-eventlog-messages", + "iotedge-diagnostics", "iothubservice", "management", + "mini-sntp", "provisioning", "systemd", "tokio-named-pipe", diff --git a/edgelet/Cross.toml b/edgelet/Cross.toml index 2dd31063a3e..045c5ee96e7 100644 --- a/edgelet/Cross.toml +++ b/edgelet/Cross.toml @@ -7,3 +7,8 @@ passthrough = [ [target.armv7-unknown-linux-gnueabihf] image = "azureiotedge/gcc-linaro-7.2.1-2017.11-x86_64_arm-linux-gnueabihf:0.2" + +[target.armv7-unknown-linux-musleabihf] +# Built from rust-embedded/cross#718a19cd68fb09428532d1317515fe7303692b47 with `./build-docker-image.sh armv7-unknown-linux-musleabihf` +# because the image in Docker hub is outdated and broken +image = "azureiotedge/armv7-unknown-linux-musleabihf:0.1" diff --git a/edgelet/build/linux/install.sh b/edgelet/build/linux/install.sh index 68fd196fd12..cfcb0aa983d 100755 --- a/edgelet/build/linux/install.sh +++ b/edgelet/build/linux/install.sh @@ -117,7 +117,8 @@ if [[ -n "$ARM_PACKAGE" ]]; then gcc-4.8-arm-linux-gnueabihf=4.8.2-16ubuntu4cross0.11 \ gcc-4.8-multilib-arm-linux-gnueabihf=4.8.2-16ubuntu4cross0.11 \ libc6-armhf-cross=2.19-0ubuntu2cross1.104 \ - gcc-arm-linux-gnueabihf=4:4.8.2-1 + gcc-arm-linux-gnueabihf=4:4.8.2-1 \ + binutils-aarch64-linux-gnu # For future reference: # ubuntu systems (host) sets openssl library version to 1.0.0, diff --git a/edgelet/build/windows/build-diagnostics.ps1 b/edgelet/build/windows/build-diagnostics.ps1 new file mode 100644 index 00000000000..fcddf9a3e4f --- /dev/null +++ b/edgelet/build/windows/build-diagnostics.ps1 @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft. All rights reserved. + +<# + # Builds and publishes to target/publish/ the iotedge-diagnostics binary and its associated dockerfile + #> + +$ErrorActionPreference = 'Continue' + +. (Join-Path $PSScriptRoot 'util.ps1') + +Assert-Rust + +$cargo = Get-CargoCommand +$ManifestPath = Get-Manifest + +$versionInfoFilePath = Join-Path $env:BUILD_REPOSITORY_LOCALPATH 'versionInfo.json' +$env:VERSION = Get-Content $versionInfoFilePath | ConvertFrom-JSON | % version +$env:NO_VALGRIND = 'true' + +$originalRustflags = $env:RUSTFLAGS +$env:RUSTFLAGS += ' -C target-feature=+crt-static' +Write-Host "$cargo build -p iotedge-diagnostics --release --manifest-path $ManifestPath" +Invoke-Expression "$cargo build -p iotedge-diagnostics --release --manifest-path $ManifestPath" +if ($originalRustflags -eq '') { + Remove-Item Env:\RUSTFLAGS +} +else { + $env:RUSTFLAGS = $originalRustflags +} +if ($LastExitCode) { + Throw "cargo build failed with exit code $LastExitCode" +} + +$ErrorActionPreference = 'Stop' + +$publishFolder = [IO.Path]::Combine($env:BUILD_BINARIESDIRECTORY, 'publish', 'azureiotedge-diagnostics') + +New-Item -Type Directory $publishFolder + +Copy-Item -Recurse ` + ([IO.Path]::Combine($env:BUILD_REPOSITORY_LOCALPATH, 'edgelet', 'iotedge-diagnostics', 'docker')) ` + ([IO.Path]::Combine($publishFolder, 'docker')) + +Copy-Item ` + ([IO.Path]::Combine($env:BUILD_REPOSITORY_LOCALPATH, 'edgelet', 'target', 'release', 'iotedge-diagnostics.exe')) ` + ([IO.Path]::Combine($publishFolder, 'docker', 'windows', 'amd64')) diff --git a/edgelet/build/windows/iotedge.wm.xml b/edgelet/build/windows/iotedge.wm.xml index 9590823f55c..0560150680c 100644 --- a/edgelet/build/windows/iotedge.wm.xml +++ b/edgelet/build/windows/iotedge.wm.xml @@ -15,6 +15,7 @@ + diff --git a/edgelet/build/windows/util.ps1 b/edgelet/build/windows/util.ps1 index 4dd6ad376c3..cced3ea9677 100644 --- a/edgelet/build/windows/util.ps1 +++ b/edgelet/build/windows/util.ps1 @@ -1,5 +1,8 @@ # Copyright (c) Microsoft. All rights reserved. +New-Item -Type Directory -Force '~/.cargo/bin' +$env:PATH += ";$(Resolve-Path '~/.cargo/bin')" + function Test-RustUp { (get-command -Name rustup.exe -ErrorAction SilentlyContinue) -ne $null @@ -35,7 +38,7 @@ function Assert-Rust Throw "Failed to download rustup with exit code $LastExitCode" } - Write-Host "Running rustup.exe" + Write-Host "Running rustup-init.exe" ./rustup-init.exe -y --default-toolchain stable-x86_64-pc-windows-msvc if ($LastExitCode) { diff --git a/edgelet/contrib/config/linux/config.yaml b/edgelet/contrib/config/linux/config.yaml index 5d3af0e6bf5..4e59b5b8f94 100644 --- a/edgelet/contrib/config/linux/config.yaml +++ b/edgelet/contrib/config/linux/config.yaml @@ -102,7 +102,7 @@ hostname: "" ############################################################################### # # -#Configures URIs used by clients of the management and workload APIs +# Configures URIs used by clients of the management and workload APIs # management_uri - used by the Edge Agent and 'iotedge' CLI to start, # stop, and manage modules # workload_uri - used by modules to retrieve tokens and certificates @@ -111,6 +111,11 @@ hostname: "" # http - connect over TCP # unix - connect over Unix domain socket # +# Note: When using the fd:// scheme for listen.management_uri or listen.workload_uri, +# the path of connect.management_uri and connect.workload_uri must match +# the path of the underlying socket in the systemd socket files +# (iotedge.socket and iotedge.mgmt.socket). +# ############################################################################### connect: @@ -136,6 +141,11 @@ connect: # listen address is fd://iotedge.workload, # connect address is unix:///var/run/iotedge/workload.sock # +# Note: When using the fd:// scheme for listen.management_uri or listen.workload_uri, +# the path of connect.management_uri and connect.workload_uri must match +# the path of the underlying socket in the systemd socket files +# (iotedge.socket and iotedge.mgmt.socket). +# ############################################################################### listen: diff --git a/edgelet/edgelet-config/Cargo.toml b/edgelet/edgelet-config/Cargo.toml new file mode 100644 index 00000000000..f434e951147 --- /dev/null +++ b/edgelet/edgelet-config/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "edgelet-config" +version = "0.1.0" +authors = ["Azure IoT Edge Devs"] +publish = false + +[dependencies] +base64 = "0.9" +config = "0.8" +failure = "0.1" +log = "0.4" +regex = "0.2" +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" +sha2 = "0.7.0" +url = "1.7" +url_serde = "0.2" + +edgelet-core = { path = "../edgelet-core" } +edgelet-utils = { path = "../edgelet-utils" } + +[dev_dependencies] +tempdir = "0.3.7" + +edgelet-docker = { path = "../edgelet-docker" } diff --git a/edgelet/iotedged/src/config/unix/default.yaml b/edgelet/edgelet-config/config/unix/default.yaml similarity index 100% rename from edgelet/iotedged/src/config/unix/default.yaml rename to edgelet/edgelet-config/config/unix/default.yaml diff --git a/edgelet/iotedged/src/config/windows/default.yaml b/edgelet/edgelet-config/config/windows/default.yaml similarity index 100% rename from edgelet/iotedged/src/config/windows/default.yaml rename to edgelet/edgelet-config/config/windows/default.yaml diff --git a/edgelet/iotedged/src/settings.rs b/edgelet/edgelet-config/src/lib.rs similarity index 57% rename from edgelet/iotedged/src/settings.rs rename to edgelet/edgelet-config/src/lib.rs index ed47a294668..e2969b782cd 100644 --- a/edgelet/iotedged/src/settings.rs +++ b/edgelet/edgelet-config/src/lib.rs @@ -1,25 +1,53 @@ // Copyright (c) Microsoft. All rights reserved. -use std::fs::{File as FsFile, OpenOptions}; +#![deny(unused_extern_crates, warnings)] +#![deny(clippy::all, clippy::pedantic)] +#![allow( + clippy::default_trait_access, + clippy::doc_markdown, // clippy want the "IoT" of "IoT Hub" in a code fence + clippy::module_name_repetitions, + clippy::shadow_unrelated, + clippy::use_self, +)] + +extern crate base64; +extern crate config; +extern crate failure; +#[macro_use] +extern crate log; +extern crate regex; +extern crate serde; +extern crate sha2; +#[macro_use] +extern crate serde_derive; +extern crate serde_json; +#[cfg(test)] +extern crate tempdir; +extern crate url; +extern crate url_serde; + +extern crate edgelet_core; +#[cfg(test)] +extern crate edgelet_docker; +extern crate edgelet_utils; + +use std::fs::OpenOptions; use std::io::Read; use std::path::{Path, PathBuf}; -use base64; use config::{Config, Environment, File, FileFormat}; -use failure::{Fail, ResultExt}; +use failure::{Context, Fail}; use log::Level; +use regex::Regex; use serde::de::DeserializeOwned; use serde::Serialize; -use serde_json; use sha2::{Digest, Sha256}; use url::Url; -use url_serde; +use edgelet_core::crypto::MemoryKey; use edgelet_core::ModuleSpec; use edgelet_utils::log_failure; -use error::{Error, ErrorKind, InitializeErrorReason}; - /// This is the name of the network created by the iotedged const DEFAULT_NETWORKID: &str = "azure-iot-edge"; @@ -27,10 +55,17 @@ const DEFAULT_NETWORKID: &str = "azure-iot-edge"; pub const DEFAULT_CONNECTION_STRING: &str = ""; #[cfg(unix)] -static DEFAULTS: &str = include_str!("config/unix/default.yaml"); +const DEFAULTS: &str = include_str!("../config/unix/default.yaml"); #[cfg(windows)] -static DEFAULTS: &str = include_str!("config/windows/default.yaml"); +const DEFAULTS: &str = include_str!("../config/windows/default.yaml"); + +const DEVICEID_KEY: &str = "DeviceId"; +const HOSTNAME_KEY: &str = "HostName"; +const SHAREDACCESSKEY_KEY: &str = "SharedAccessKey"; + +const DEVICEID_REGEX: &str = r"^[A-Za-z0-9\-:.+%_#*?!(),=@;$']{1,128}$"; +const HOSTNAME_REGEX: &str = r"^[a-zA-Z0-9_\-\.]+$"; #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] @@ -39,9 +74,74 @@ pub struct Manual { } impl Manual { + pub fn new(device_connection_string: String) -> Self { + Manual { + device_connection_string, + } + } + pub fn device_connection_string(&self) -> &str { &self.device_connection_string } + + pub fn parse_device_connection_string( + &self, + ) -> Result<(MemoryKey, String, String), ParseManualDeviceConnectionStringError> { + if self.device_connection_string.is_empty() { + return Err(ParseManualDeviceConnectionStringError::Empty); + } + + let mut key = None; + let mut device_id = None; + let mut hub = None; + + let parts: Vec<&str> = self.device_connection_string.split(';').collect(); + for p in parts { + let s: Vec<&str> = p.split('=').collect(); + match s[0] { + SHAREDACCESSKEY_KEY => key = Some(s[1].to_string()), + DEVICEID_KEY => device_id = Some(s[1].to_string()), + HOSTNAME_KEY => hub = Some(s[1].to_string()), + _ => (), // Ignore extraneous component in the connection string + } + } + + let key = key.ok_or( + ParseManualDeviceConnectionStringError::MissingRequiredParameter(SHAREDACCESSKEY_KEY), + )?; + if key.is_empty() { + return Err(ParseManualDeviceConnectionStringError::MalformedParameter( + SHAREDACCESSKEY_KEY, + )); + } + let key = MemoryKey::new(base64::decode(&key).map_err(|_| { + ParseManualDeviceConnectionStringError::MalformedParameter(SHAREDACCESSKEY_KEY) + })?); + + let device_id = device_id.ok_or( + ParseManualDeviceConnectionStringError::MissingRequiredParameter(DEVICEID_KEY), + )?; + let device_id_regex = + Regex::new(DEVICEID_REGEX).expect("This hard-coded regex is expected to be valid."); + if !device_id_regex.is_match(&device_id) { + return Err(ParseManualDeviceConnectionStringError::MalformedParameter( + DEVICEID_KEY, + )); + } + + let hub = hub.ok_or( + ParseManualDeviceConnectionStringError::MissingRequiredParameter(HOSTNAME_KEY), + )?; + let hub_regex = + Regex::new(HOSTNAME_REGEX).expect("This hard-coded regex is expected to be valid."); + if !hub_regex.is_match(&hub) { + return Err(ParseManualDeviceConnectionStringError::MalformedParameter( + HOSTNAME_KEY, + )); + } + + Ok((key, device_id.to_owned(), hub.to_owned())) + } } #[derive(Debug, Deserialize, Serialize)] @@ -175,24 +275,24 @@ impl Settings where T: DeserializeOwned + Serialize, { - pub fn new(filename: Option<&str>) -> Result { + pub fn new(filename: Option<&Path>) -> Result { + let filename = filename.map(|filename| { + filename.to_str().unwrap_or_else(|| { + panic!( + "cannot load config from {} because it is not a utf-8 path", + filename.display() + ) + }) + }); let mut config = Config::default(); - config - .merge(File::from_str(DEFAULTS, FileFormat::Yaml)) - .context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))?; + config.merge(File::from_str(DEFAULTS, FileFormat::Yaml))?; if let Some(file) = filename { - config - .merge(File::with_name(file).required(true)) - .context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))?; + config.merge(File::with_name(file).required(true))?; } - config - .merge(Environment::with_prefix("iotedge")) - .context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))?; + config.merge(Environment::with_prefix("iotedge"))?; - let settings: Self = config - .try_into() - .context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))?; + let settings: Self = config.try_into()?; Ok(settings) } @@ -233,39 +333,85 @@ where self.certificates.as_ref() } - pub fn diff_with_cached(&self, path: PathBuf) -> Result { - OpenOptions::new() - .read(true) - .open(path) - .map_err(|err| err.context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))) - .and_then(|mut file: FsFile| { - let mut buffer = String::new(); - file.read_to_string(&mut buffer) - .context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))?; - let s = serde_json::to_string(self) - .context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))?; - let s = Sha256::digest_str(&s); - let encoded = base64::encode(&s); - if encoded == buffer { - debug!("Config state matches supplied config."); - Ok(false) - } else { - Ok(true) - } - }) - .or_else(|err| { + pub fn diff_with_cached(&self, path: &Path) -> bool { + fn diff_with_cached_inner( + cached_settings: &Settings, + path: &Path, + ) -> Result + where + T: DeserializeOwned + Serialize, + { + let mut file = OpenOptions::new().read(true).open(path)?; + let mut buffer = String::new(); + file.read_to_string(&mut buffer)?; + let s = serde_json::to_string(cached_settings)?; + let s = Sha256::digest_str(&s); + let encoded = base64::encode(&s); + if encoded == buffer { + debug!("Config state matches supplied config."); + Ok(false) + } else { + Ok(true) + } + } + + match diff_with_cached_inner(self, path) { + Ok(result) => result, + + Err(err) => { log_failure(Level::Debug, &err); debug!("Error reading config backup."); - Ok(true) - }) + true + } + } } } +#[derive(Debug, Fail)] +#[fail(display = "Could not load settings")] +pub struct LoadSettingsError(#[cause] Context>); + +impl From for LoadSettingsError { + fn from(err: std::io::Error) -> Self { + LoadSettingsError(Context::new(Box::new(err))) + } +} + +impl From for LoadSettingsError { + fn from(err: config::ConfigError) -> Self { + LoadSettingsError(Context::new(Box::new(err))) + } +} + +impl From for LoadSettingsError { + fn from(err: serde_json::Error) -> Self { + LoadSettingsError(Context::new(Box::new(err))) + } +} + +#[derive(Clone, Copy, Debug, Fail)] +pub enum ParseManualDeviceConnectionStringError { + #[fail( + display = "The Connection String is empty. Please update the config.yaml and provide the IoTHub connection information." + )] + Empty, + + #[fail(display = "The Connection String is missing required parameter {}", _0)] + MissingRequiredParameter(&'static str), + + #[fail( + display = "The Connection String has a malformed value for parameter {}.", + _0 + )] + MalformedParameter(&'static str), +} + #[cfg(test)] mod tests { use super::*; use config::{Config, File, FileFormat}; use edgelet_docker::DockerConfig; + use std::fs::File as FsFile; use std::io::Write; use tempdir::TempDir; @@ -320,19 +466,19 @@ mod tests { #[test] fn no_file_gets_error() { - let settings = Settings::::new(Some("garbage")); + let settings = Settings::::new(Some(Path::new("garbage"))); assert!(settings.is_err()); } #[test] fn bad_file_gets_error() { - let settings = Settings::::new(Some(BAD_SETTINGS)); + let settings = Settings::::new(Some(Path::new(BAD_SETTINGS))); assert!(settings.is_err()); } #[test] fn manual_file_gets_sample_connection_string() { - let settings = Settings::::new(Some(GOOD_SETTINGS)); + let settings = Settings::::new(Some(Path::new(GOOD_SETTINGS))); println!("{:?}", settings); assert!(settings.is_ok()); let s = settings.unwrap(); @@ -340,13 +486,13 @@ mod tests { let connection_string = unwrap_manual_provisioning(p); assert_eq!( connection_string, - "HostName=something.something.com;DeviceId=something;SharedAccessKey=something" + "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U=" ); } #[test] fn manual_file_gets_sample_tg_paths() { - let settings = Settings::::new(Some(GOOD_SETTINGS_TG)); + let settings = Settings::::new(Some(Path::new(GOOD_SETTINGS_TG))); println!("{:?}", settings); assert!(settings.is_ok()); let s = settings.unwrap(); @@ -365,7 +511,7 @@ mod tests { #[test] fn dps_prov_symmetric_key_get_settings() { - let settings = Settings::::new(Some(GOOD_SETTINGS_DPS_SYM_KEY)); + let settings = Settings::::new(Some(Path::new(GOOD_SETTINGS_DPS_SYM_KEY))); println!("{:?}", settings); assert!(settings.is_ok()); let s = settings.unwrap(); @@ -385,58 +531,53 @@ mod tests { fn diff_with_same_cached_returns_false() { let tmp_dir = TempDir::new("blah").unwrap(); let path = tmp_dir.path().join("cache"); - let settings = Settings::::new(Some(GOOD_SETTINGS)).unwrap(); + let settings = Settings::::new(Some(Path::new(GOOD_SETTINGS))).unwrap(); let settings_to_write = serde_json::to_string(&settings).unwrap(); let sha_to_write = Sha256::digest_str(&settings_to_write); let base64_to_write = base64::encode(&sha_to_write); - FsFile::create(path.clone()) + FsFile::create(&path) .unwrap() .write_all(base64_to_write.as_bytes()) .unwrap(); - assert_eq!(settings.diff_with_cached(path).unwrap(), false); + assert_eq!(settings.diff_with_cached(&path), false); } #[test] fn diff_with_same_cached_env_var_unordered_returns_false() { let tmp_dir = TempDir::new("blah").unwrap(); let path = tmp_dir.path().join("cache"); - let settings1 = Settings::::new(Some(GOOD_SETTINGS2)).unwrap(); + let settings1 = Settings::::new(Some(Path::new(GOOD_SETTINGS2))).unwrap(); let settings_to_write = serde_json::to_string(&settings1).unwrap(); let sha_to_write = Sha256::digest_str(&settings_to_write); let base64_to_write = base64::encode(&sha_to_write); - FsFile::create(path.clone()) + FsFile::create(&path) .unwrap() .write_all(base64_to_write.as_bytes()) .unwrap(); - let settings = Settings::::new(Some(GOOD_SETTINGS)).unwrap(); - assert_eq!(settings.diff_with_cached(path).unwrap(), false); + let settings = Settings::::new(Some(Path::new(GOOD_SETTINGS))).unwrap(); + assert_eq!(settings.diff_with_cached(&path), false); } #[test] fn diff_with_different_cached_returns_true() { let tmp_dir = TempDir::new("blah").unwrap(); let path = tmp_dir.path().join("cache"); - let settings1 = Settings::::new(Some(GOOD_SETTINGS1)).unwrap(); + let settings1 = Settings::::new(Some(Path::new(GOOD_SETTINGS1))).unwrap(); let settings_to_write = serde_json::to_string(&settings1).unwrap(); let sha_to_write = Sha256::digest_str(&settings_to_write); let base64_to_write = base64::encode(&sha_to_write); - FsFile::create(path.clone()) + FsFile::create(&path) .unwrap() .write_all(base64_to_write.as_bytes()) .unwrap(); - let settings = Settings::::new(Some(GOOD_SETTINGS)).unwrap(); - assert_eq!(settings.diff_with_cached(path).unwrap(), true); + let settings = Settings::::new(Some(Path::new(GOOD_SETTINGS))).unwrap(); + assert_eq!(settings.diff_with_cached(&path), true); } #[test] fn diff_with_no_file_returns_true() { - let settings = Settings::::new(Some(GOOD_SETTINGS)).unwrap(); - assert_eq!( - settings - .diff_with_cached(PathBuf::from("i dont exist")) - .unwrap(), - true - ); + let settings = Settings::::new(Some(Path::new(GOOD_SETTINGS))).unwrap(); + assert_eq!(settings.diff_with_cached(Path::new("i dont exist")), true); } #[test] diff --git a/edgelet/iotedged/test/linux/bad_sample_settings.yaml b/edgelet/edgelet-config/test/linux/bad_sample_settings.yaml similarity index 100% rename from edgelet/iotedged/test/linux/bad_sample_settings.yaml rename to edgelet/edgelet-config/test/linux/bad_sample_settings.yaml diff --git a/edgelet/iotedged/test/linux/sample_settings.dps.sym.yaml b/edgelet/edgelet-config/test/linux/sample_settings.dps.sym.yaml similarity index 100% rename from edgelet/iotedged/test/linux/sample_settings.dps.sym.yaml rename to edgelet/edgelet-config/test/linux/sample_settings.dps.sym.yaml diff --git a/edgelet/iotedged/test/linux/sample_settings.tg.yaml b/edgelet/edgelet-config/test/linux/sample_settings.tg.yaml similarity index 94% rename from edgelet/iotedged/test/linux/sample_settings.tg.yaml rename to edgelet/edgelet-config/test/linux/sample_settings.tg.yaml index 6e35137cc61..bc48a498273 100644 --- a/edgelet/iotedged/test/linux/sample_settings.tg.yaml +++ b/edgelet/edgelet-config/test/linux/sample_settings.tg.yaml @@ -2,7 +2,7 @@ # Configures the provisioning mode provisioning: source: "manual" - device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=something" + device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U=" certificates: device_ca_cert: "device_ca_cert.pem" diff --git a/edgelet/iotedged/test/linux/sample_settings.yaml b/edgelet/edgelet-config/test/linux/sample_settings.yaml similarity index 93% rename from edgelet/iotedged/test/linux/sample_settings.yaml rename to edgelet/edgelet-config/test/linux/sample_settings.yaml index e2ae2d07f46..7dcf7f2ef60 100644 --- a/edgelet/iotedged/test/linux/sample_settings.yaml +++ b/edgelet/edgelet-config/test/linux/sample_settings.yaml @@ -2,7 +2,7 @@ # Configures the provisioning mode provisioning: source: "manual" - device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=something" + device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U=" agent: name: "edgeAgent" type: "docker" diff --git a/edgelet/iotedged/test/linux/sample_settings1.yaml b/edgelet/edgelet-config/test/linux/sample_settings1.yaml similarity index 92% rename from edgelet/iotedged/test/linux/sample_settings1.yaml rename to edgelet/edgelet-config/test/linux/sample_settings1.yaml index c2698274fdd..bbac6df76b1 100644 --- a/edgelet/iotedged/test/linux/sample_settings1.yaml +++ b/edgelet/edgelet-config/test/linux/sample_settings1.yaml @@ -2,7 +2,7 @@ # Configures the provisioning mode provisioning: source: "manual" - device_connection_string: "HostName=something1.something1.com;DeviceId=something;SharedAccessKey=something" + device_connection_string: "HostName=something1.something1.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U=" agent: name: "edgeAgent" type: "docker" diff --git a/edgelet/iotedged/test/linux/sample_settings2.yaml b/edgelet/edgelet-config/test/linux/sample_settings2.yaml similarity index 93% rename from edgelet/iotedged/test/linux/sample_settings2.yaml rename to edgelet/edgelet-config/test/linux/sample_settings2.yaml index 3172340b8bc..24f70b096f1 100644 --- a/edgelet/iotedged/test/linux/sample_settings2.yaml +++ b/edgelet/edgelet-config/test/linux/sample_settings2.yaml @@ -2,7 +2,7 @@ # Configures the provisioning mode provisioning: source: "manual" - device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=something" + device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U=" agent: name: "edgeAgent" type: "docker" diff --git a/edgelet/iotedged/test/windows/bad_sample_settings.yaml b/edgelet/edgelet-config/test/windows/bad_sample_settings.yaml similarity index 91% rename from edgelet/iotedged/test/windows/bad_sample_settings.yaml rename to edgelet/edgelet-config/test/windows/bad_sample_settings.yaml index 3d0ccb3bc28..a0325d3994f 100644 --- a/edgelet/iotedged/test/windows/bad_sample_settings.yaml +++ b/edgelet/edgelet-config/test/windows/bad_sample_settings.yaml @@ -17,5 +17,5 @@ listen: management_uri: "http://0.0.0.0:8080" homedir: "C:\\Temp" moby_runtime: - uri: "http://localhost:2375" + uri: "npipe://./pipe/iotedge_moby_engine" network: "azure-iot-edge" diff --git a/edgelet/iotedged/test/windows/sample_settings.dps.sym.yaml b/edgelet/edgelet-config/test/windows/sample_settings.dps.sym.yaml similarity index 90% rename from edgelet/iotedged/test/windows/sample_settings.dps.sym.yaml rename to edgelet/edgelet-config/test/windows/sample_settings.dps.sym.yaml index 7c11575c667..08068ccb9c3 100644 --- a/edgelet/iotedged/test/windows/sample_settings.dps.sym.yaml +++ b/edgelet/edgelet-config/test/windows/sample_settings.dps.sym.yaml @@ -28,6 +28,7 @@ connect: listen: workload_uri: "http://0.0.0.0:8081" management_uri: "http://0.0.0.0:8080" -docker_uri: "http://localhost:2375" homedir: "C:\\Temp" -network: "azure-iot-edge" +moby_runtime: + uri: "npipe://./pipe/iotedge_moby_engine" + network: "azure-iot-edge" diff --git a/edgelet/iotedged/test/windows/sample_settings.tg.yaml b/edgelet/edgelet-config/test/windows/sample_settings.tg.yaml similarity index 85% rename from edgelet/iotedged/test/windows/sample_settings.tg.yaml rename to edgelet/edgelet-config/test/windows/sample_settings.tg.yaml index a364f59199a..96d53be3b1b 100644 --- a/edgelet/iotedged/test/windows/sample_settings.tg.yaml +++ b/edgelet/edgelet-config/test/windows/sample_settings.tg.yaml @@ -2,7 +2,7 @@ # Configures the provisioning mode provisioning: source: "manual" - device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=something" + device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U=" certificates: device_ca_cert: "device_ca_cert.pem" @@ -30,6 +30,7 @@ connect: listen: workload_uri: "http://0.0.0.0:8081" management_uri: "http://0.0.0.0:8080" -docker_uri: "http://localhost:2375" homedir: "C:\\Temp" -network: "azure-iot-edge" +moby_runtime: + uri: "npipe://./pipe/iotedge_moby_engine" + network: "azure-iot-edge" diff --git a/edgelet/edgelet-config/test/windows/sample_settings.yaml b/edgelet/edgelet-config/test/windows/sample_settings.yaml new file mode 100644 index 00000000000..5c25c3d952a --- /dev/null +++ b/edgelet/edgelet-config/test/windows/sample_settings.yaml @@ -0,0 +1,31 @@ + +# Configures the provisioning mode +provisioning: + source: "manual" + device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U=" +agent: + name: "edgeAgent" + type: "docker" + env: + abc: "value1" + acd: "value2" + config: + image: "microsoft/azureiotedge-agent:1.0" + auth: {} +hostname: "localhost" + +# Sets the connection uris for clients +connect: + workload_uri: "http://localhost:8081" + management_uri: "http://localhost:8080" + +# Sets the uris to listen on +# These can be different than the connect uris. +# For instance, when using the fd:// scheme for systemd +listen: + workload_uri: "http://0.0.0.0:8081" + management_uri: "http://0.0.0.0:8080" +homedir: "C:\\Temp" +moby_runtime: + uri: "npipe://./pipe/iotedge_moby_engine" + network: "azure-iot-edge" diff --git a/edgelet/iotedged/test/windows/sample_settings1.yaml b/edgelet/edgelet-config/test/windows/sample_settings1.yaml similarity index 86% rename from edgelet/iotedged/test/windows/sample_settings1.yaml rename to edgelet/edgelet-config/test/windows/sample_settings1.yaml index 3cf62c602df..76071a9bca1 100644 --- a/edgelet/iotedged/test/windows/sample_settings1.yaml +++ b/edgelet/edgelet-config/test/windows/sample_settings1.yaml @@ -2,7 +2,7 @@ # Configures the provisioning mode provisioning: source: "manual" - device_connection_string: "HostName=something1.something1.com;DeviceId=something;SharedAccessKey=something" + device_connection_string: "HostName=something1.something1.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U=" agent: name: "edgeAgent" type: "docker" @@ -25,4 +25,4 @@ listen: management_uri: "http://0.0.0.0:8080" homedir: "C:\\Temp" moby_runtime: - uri: "http://localhost:2375" + uri: "npipe://./pipe/iotedge_moby_engine" diff --git a/edgelet/iotedged/test/windows/sample_settings2.yaml b/edgelet/edgelet-config/test/windows/sample_settings2.yaml similarity index 87% rename from edgelet/iotedged/test/windows/sample_settings2.yaml rename to edgelet/edgelet-config/test/windows/sample_settings2.yaml index 7f20f746893..0c848370cb8 100644 --- a/edgelet/iotedged/test/windows/sample_settings2.yaml +++ b/edgelet/edgelet-config/test/windows/sample_settings2.yaml @@ -2,7 +2,7 @@ # Configures the provisioning mode provisioning: source: "manual" - device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=something" + device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U=" agent: name: "edgeAgent" type: "docker" @@ -27,5 +27,5 @@ listen: management_uri: "http://0.0.0.0:8080" homedir: "C:\\Temp" moby_runtime: - uri: "http://localhost:2375" + uri: "npipe://./pipe/iotedge_moby_engine" network: "azure-iot-edge" diff --git a/edgelet/iotedged/test/windows/sample_settings.yaml b/edgelet/edgelet-config/test/windows/sample_settings_notmoby.yaml similarity index 93% rename from edgelet/iotedged/test/windows/sample_settings.yaml rename to edgelet/edgelet-config/test/windows/sample_settings_notmoby.yaml index d748c107e61..c1b70d30f1d 100644 --- a/edgelet/iotedged/test/windows/sample_settings.yaml +++ b/edgelet/edgelet-config/test/windows/sample_settings_notmoby.yaml @@ -2,7 +2,7 @@ # Configures the provisioning mode provisioning: source: "manual" - device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=something" + device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U=" agent: name: "edgeAgent" type: "docker" diff --git a/edgelet/edgelet-core/src/error.rs b/edgelet/edgelet-core/src/error.rs index 5b8ffa2c2e8..c4e00f36c98 100644 --- a/edgelet/edgelet-core/src/error.rs +++ b/edgelet/edgelet-core/src/error.rs @@ -42,6 +42,9 @@ pub enum ErrorKind { #[fail(display = "Invalid module type {:?}", _0)] InvalidModuleType(String), + #[fail(display = "Invalid URL {:?}", _0)] + InvalidUrl(String), + #[fail(display = "Item not found.")] KeyStoreItemNotFound, diff --git a/edgelet/edgelet-core/src/lib.rs b/edgelet/edgelet-core/src/lib.rs index f0d74e38d64..92611a84d38 100644 --- a/edgelet/edgelet-core/src/lib.rs +++ b/edgelet/edgelet-core/src/lib.rs @@ -21,9 +21,15 @@ extern crate serde_derive; extern crate serde_json; extern crate sha2; extern crate tokio; +extern crate url; extern crate edgelet_utils; +use std::path::{Path, PathBuf}; + +use failure::ResultExt; +use url::Url; + mod authorization; mod certificate_properties; pub mod crypto; @@ -54,9 +60,50 @@ lazy_static! { .map(|version| option_env!("BUILD_SOURCEVERSION") .map(|sha| format!("{} ({})", version, sha)) .unwrap_or_else(|| version.to_string())) - .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); + .unwrap_or_else(|| include_str!("../../version.txt").trim().to_string()); } pub fn version() -> &'static str { &VERSION } + +pub trait UrlExt { + fn to_uds_file_path(&self) -> Result; + fn to_base_path(&self) -> Result; +} + +impl UrlExt for Url { + fn to_uds_file_path(&self) -> Result { + debug_assert_eq!(self.scheme(), UNIX_SCHEME); + + if cfg!(windows) { + // We get better handling of Windows file syntax if we parse a + // unix:// URL as a file:// URL. Specifically: + // - On Unix, `Url::parse("unix:///path")?.to_file_path()` succeeds and + // returns "/path". + // - On Windows, `Url::parse("unix:///C:/path")?.to_file_path()` fails + // with Err(()). + // - On Windows, `Url::parse("file:///C:/path")?.to_file_path()` succeeds + // and returns "C:\\path". + debug_assert_eq!(self.scheme(), UNIX_SCHEME); + let mut s = self.to_string(); + s.replace_range(..4, "file"); + let url = Url::parse(&s).with_context(|_| ErrorKind::InvalidUrl(s.clone()))?; + let path = url + .to_file_path() + .map_err(|()| ErrorKind::InvalidUrl(url.to_string()))?; + Ok(path) + } else { + Ok(Path::new(self.path()).to_path_buf()) + } + } + + fn to_base_path(&self) -> Result { + match self.scheme() { + "unix" => Ok(self.to_uds_file_path()?), + _ => Ok(self.as_str().into()), + } + } +} + +pub const UNIX_SCHEME: &str = "unix"; diff --git a/edgelet/edgelet-docker/src/runtime.rs b/edgelet/edgelet-docker/src/runtime.rs index f74f63b9df4..c136f65122a 100644 --- a/edgelet/edgelet-docker/src/runtime.rs +++ b/edgelet/edgelet-docker/src/runtime.rs @@ -21,9 +21,9 @@ use docker::apis::configuration::Configuration; use docker::models::{ContainerCreateBody, InlineResponse200, InlineResponse2001, NetworkConfig}; use edgelet_core::{ pid::Pid, LogOptions, Module, ModuleRegistry, ModuleRuntime, ModuleRuntimeState, ModuleSpec, - ModuleTop, RegistryOperation, RuntimeOperation, SystemInfo as CoreSystemInfo, + ModuleTop, RegistryOperation, RuntimeOperation, SystemInfo as CoreSystemInfo, UrlExt, }; -use edgelet_http::{UrlConnector, UrlExt}; +use edgelet_http::UrlConnector; use edgelet_utils::{ensure_not_empty_with_context, log_failure}; use error::{Error, ErrorKind, Result}; diff --git a/edgelet/edgelet-http-mgmt/src/client/module.rs b/edgelet/edgelet-http-mgmt/src/client/module.rs index 3c158e81a56..5628aa80e19 100644 --- a/edgelet/edgelet-http-mgmt/src/client/module.rs +++ b/edgelet/edgelet-http-mgmt/src/client/module.rs @@ -17,9 +17,9 @@ use serde_json; use url::Url; use edgelet_core::*; -use edgelet_core::{ModuleOperation, RuntimeOperation, SystemInfo as CoreSystemInfo}; +use edgelet_core::{ModuleOperation, RuntimeOperation, SystemInfo as CoreSystemInfo, UrlExt}; use edgelet_docker::{self, DockerConfig}; -use edgelet_http::{UrlConnector, UrlExt, API_VERSION}; +use edgelet_http::{UrlConnector, API_VERSION}; use error::{Error, ErrorKind}; diff --git a/edgelet/edgelet-http/src/lib.rs b/edgelet/edgelet-http/src/lib.rs index fe308f28501..03d44b5f1d1 100644 --- a/edgelet/edgelet-http/src/lib.rs +++ b/edgelet/edgelet-http/src/lib.rs @@ -65,7 +65,6 @@ use std::net; use std::net::ToSocketAddrs; #[cfg(unix)] use std::os::unix::io::FromRawFd; -use std::path::{Path, PathBuf}; use std::sync::Arc; use failure::{Fail, ResultExt}; @@ -81,6 +80,7 @@ use tokio::net::TcpListener; use tokio_uds::UnixListener; use url::Url; +use edgelet_core::{UrlExt, UNIX_SCHEME}; use edgelet_utils::log_failure; pub mod authorization; @@ -102,8 +102,9 @@ use self::pid::PidService; use self::util::incoming::Incoming; const HTTP_SCHEME: &str = "http"; +#[cfg(windows)] +const PIPE_SCHEME: &str = "npipe"; const TCP_SCHEME: &str = "tcp"; -const UNIX_SCHEME: &str = "unix"; #[cfg(unix)] const FD_SCHEME: &str = "fd"; @@ -237,7 +238,9 @@ impl HyperExt for Http { Incoming::Tcp(listener) } UNIX_SCHEME => { - let path = url.to_uds_file_path()?; + let path = url + .to_uds_file_path() + .map_err(|_| ErrorKind::InvalidUrl(url.to_string()))?; unix::listener(path)? } #[cfg(unix)] @@ -295,42 +298,3 @@ impl HyperExt for Http { }) } } - -pub trait UrlExt { - fn to_uds_file_path(&self) -> Result; - fn to_base_path(&self) -> Result; -} - -impl UrlExt for Url { - fn to_uds_file_path(&self) -> Result { - debug_assert_eq!(self.scheme(), UNIX_SCHEME); - - if cfg!(windows) { - // We get better handling of Windows file syntax if we parse a - // unix:// URL as a file:// URL. Specifically: - // - On Unix, `Url::parse("unix:///path")?.to_file_path()` succeeds and - // returns "/path". - // - On Windows, `Url::parse("unix:///C:/path")?.to_file_path()` fails - // with Err(()). - // - On Windows, `Url::parse("file:///C:/path")?.to_file_path()` succeeds - // and returns "C:\\path". - debug_assert_eq!(self.scheme(), UNIX_SCHEME); - let mut s = self.to_string(); - s.replace_range(..4, "file"); - let url = Url::parse(&s).with_context(|_| ErrorKind::InvalidUrl(s.clone()))?; - let path = url - .to_file_path() - .map_err(|()| ErrorKind::InvalidUrl(url.to_string()))?; - Ok(path) - } else { - Ok(Path::new(self.path()).to_path_buf()) - } - } - - fn to_base_path(&self) -> Result { - match self.scheme() { - "unix" => Ok(self.to_uds_file_path()?), - _ => Ok(self.as_str().into()), - } - } -} diff --git a/edgelet/edgelet-http/src/util/connector.rs b/edgelet/edgelet-http/src/util/connector.rs index d3629e59873..24464cac810 100644 --- a/edgelet/edgelet-http/src/util/connector.rs +++ b/edgelet/edgelet-http/src/util/connector.rs @@ -28,14 +28,13 @@ use hyperlocal::{UnixConnector, Uri as HyperlocalUri}; use hyperlocal_windows::{UnixConnector, Uri as HyperlocalUri}; use url::{ParseError, Url}; +use edgelet_core::UrlExt; + use error::{Error, ErrorKind, InvalidUrlReason}; use util::{socket_file_exists, StreamSelector}; -use UrlExt; - -const UNIX_SCHEME: &str = "unix"; #[cfg(windows)] -const PIPE_SCHEME: &str = "npipe"; -const HTTP_SCHEME: &str = "http"; +use PIPE_SCHEME; +use {HTTP_SCHEME, UNIX_SCHEME}; pub enum UrlConnector { Http(HttpConnector), @@ -51,7 +50,9 @@ impl UrlConnector { PIPE_SCHEME => Ok(UrlConnector::Pipe(PipeConnector)), UNIX_SCHEME => { - let file_path = url.to_uds_file_path()?; + let file_path = url + .to_uds_file_path() + .map_err(|_| ErrorKind::InvalidUrl(url.to_string()))?; if socket_file_exists(&file_path) { Ok(UrlConnector::Unix(UnixConnector::new())) } else { diff --git a/edgelet/iotedge-diagnostics/Cargo.toml b/edgelet/iotedge-diagnostics/Cargo.toml new file mode 100644 index 00000000000..188914a6c06 --- /dev/null +++ b/edgelet/iotedge-diagnostics/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "iotedge-diagnostics" +version = "0.1.0" +authors = ["Azure IoT Edge Devs"] +publish = false +edition = "2018" + +[dependencies] +clap = "2.31" +futures = "0.1" +hyper = "0.12" +serde_json = "1.0" +tokio = "0.1" +url = "1.7" + +edgelet-core = { path = "../edgelet-core" } +management = { path = "../management" } + +[target.'cfg(unix)'.dependencies] +hyperlocal = "0.6" + +[target.'cfg(windows)'.dependencies] +hyperlocal-windows = { git = "https://github.com/Azure/hyperlocal-windows" } diff --git a/edgelet/iotedge-diagnostics/docker/linux/amd64/Dockerfile b/edgelet/iotedge-diagnostics/docker/linux/amd64/Dockerfile new file mode 100644 index 00000000000..b7b376ad334 --- /dev/null +++ b/edgelet/iotedge-diagnostics/docker/linux/amd64/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine:3.9 + +COPY ./docker/linux/amd64/iotedge-diagnostics /iotedge-diagnostics diff --git a/edgelet/iotedge-diagnostics/docker/linux/arm32v7/Dockerfile b/edgelet/iotedge-diagnostics/docker/linux/arm32v7/Dockerfile new file mode 100644 index 00000000000..056f0ef8fb2 --- /dev/null +++ b/edgelet/iotedge-diagnostics/docker/linux/arm32v7/Dockerfile @@ -0,0 +1,3 @@ +FROM arm32v6/alpine:3.9 + +COPY ./docker/linux/arm32v7/iotedge-diagnostics /iotedge-diagnostics diff --git a/edgelet/iotedge-diagnostics/docker/manifest.yaml.template b/edgelet/iotedge-diagnostics/docker/manifest.yaml.template new file mode 100644 index 00000000000..07132a47bb5 --- /dev/null +++ b/edgelet/iotedge-diagnostics/docker/manifest.yaml.template @@ -0,0 +1,18 @@ +image: __REGISTRY__/__NAMESPACE__/azureiotedge-diagnostics:__VERSION__ +tags: __TAGS__ +manifests: + - + image: __REGISTRY__/__NAMESPACE__/azureiotedge-diagnostics:__VERSION__-linux-amd64 + platform: + architecture: amd64 + os: linux + - + image: __REGISTRY__/__NAMESPACE__/azureiotedge-diagnostics:__VERSION__-linux-arm32v7 + platform: + architecture: arm + os: linux + - + image: __REGISTRY__/__NAMESPACE__/azureiotedge-diagnostics:__VERSION__-windows-amd64 + platform: + architecture: amd64 + os: windows diff --git a/edgelet/iotedge-diagnostics/docker/windows/amd64/Dockerfile b/edgelet/iotedge-diagnostics/docker/windows/amd64/Dockerfile new file mode 100644 index 00000000000..75413f5fe3c --- /dev/null +++ b/edgelet/iotedge-diagnostics/docker/windows/amd64/Dockerfile @@ -0,0 +1,3 @@ +FROM mcr.microsoft.com/windows/nanoserver:1809_amd64 + +COPY ./docker/windows/amd64/iotedge-diagnostics.exe /iotedge-diagnostics diff --git a/edgelet/iotedge-diagnostics/src/main.rs b/edgelet/iotedge-diagnostics/src/main.rs new file mode 100644 index 00000000000..c978ebd9f19 --- /dev/null +++ b/edgelet/iotedge-diagnostics/src/main.rs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft. All rights reserved. + +#![deny(rust_2018_idioms, warnings)] +#![deny(clippy::all, clippy::pedantic)] +#![allow(clippy::use_self)] + +#[macro_use] +extern crate clap; + +use std::net::{TcpStream, ToSocketAddrs}; + +use futures::{Future, Stream}; + +use edgelet_core::UrlExt; + +#[cfg(unix)] +use hyperlocal::{UnixConnector, Uri as UnixUri}; +#[cfg(windows)] +use hyperlocal_windows::{UnixConnector, Uri as UnixUri}; + +fn main() -> Result<(), Error> { + let app = app_from_crate!() + .setting(clap::AppSettings::SubcommandRequiredElseHelp) + .setting(clap::AppSettings::VersionlessSubcommands) + .subcommand( + clap::SubCommand::with_name("edge-agent") + .about("edge-agent diagnostics") + .arg( + clap::Arg::with_name("management-uri") + .long("management-uri") + .required(true) + .takes_value(true) + .help("URI of management endpoint"), + ), + ) + .subcommand( + clap::SubCommand::with_name("iothub") + .about("connect to Azure IoT Hub") + .arg( + clap::Arg::with_name("hostname") + .long("hostname") + .required(true) + .takes_value(true) + .help("Hostname of Azure IoT Hub"), + ) + .arg( + clap::Arg::with_name("port") + .long("port") + .required(true) + .takes_value(true) + .help("Port to connect to"), + ), + ) + .subcommand(clap::SubCommand::with_name("local-time").about("print local time")); + + let matches = app.get_matches(); + + let mut runtime = tokio::runtime::Runtime::new() + .map_err(|err| format!("could not create tokio runtime: {}", err))?; + + match matches.subcommand() { + ("edge-agent", Some(matches)) => { + let management_uri = matches + .value_of("management-uri") + .expect("parameter is required"); + let management_uri = url::Url::parse(management_uri) + .map_err(|err| format!("could not parse management URI: {}", err))?; + + let list_modules_response = match management_uri.scheme() { + "unix" => { + let client = + hyper::Client::builder().build::<_, hyper::Body>(UnixConnector::new()); + let uri = UnixUri::new( + management_uri + .to_uds_file_path() + .map_err(|err| format!("couldn't get file path from URI: {}", err))?, + "/modules/?api-version=2018-06-28", + ); + client.get(uri.into()) + } + + "http" => { + let client = hyper::Client::new(); + let uri = management_uri + .join("/modules/?api-version=2018-06-28") + .map_err(|err| { + format!("could not construct list-modules request URI: {}", err) + })?; + client.get(uri.to_string().parse().map_err(|err| { + format!("could not convert list-modules request URI from url::Url to hyper::Uri: {}", err) + })?) + } + + scheme => { + return Err(format!( + "unrecognized scheme {:?} in management URI {:?}", + scheme, management_uri + ) + .into()); + } + }; + + let f = list_modules_response + .then(|response| { + let response = response.map_err(|err| { + format!("could not execute list-modules request: {}", err) + })?; + assert_eq!( + response.status(), + hyper::StatusCode::OK, + "list-modules request did not succeed" + ); + Ok::<_, Error>(response.into_body().concat2().map_err(|err| { + format!("could not execute list-modules request: {}", err).into() + })) + }) + .flatten() + .and_then(|body| { + let _: management::models::ModuleList = serde_json::from_slice(&*body) + .map_err(|err| format!("could not parse list-modules response: {}", err))?; + Ok::<_, Error>(()) + }); + + runtime.block_on(f)?; + } + + ("iothub", Some(matches)) => { + let iothub_hostname = matches.value_of("hostname").expect("parameter is required"); + + let port = matches.value_of("port").expect("parameter is required"); + + let port = port + .parse() + .map_err(|err| format!("could not parse port: {}", err))?; + + let iothub_host = (iothub_hostname, port) + .to_socket_addrs() + .map_err(|err| format!("could not resolve Azure IoT Hub hostname: {}", err))? + .next() + .ok_or_else(|| { + "could not resolve Azure IoT Hub hostname: no addresses found".to_owned() + })?; + + let _ = TcpStream::connect_timeout(&iothub_host, std::time::Duration::from_secs(10)) + .map_err(|err| format!("could not connect to IoT Hub: {}", err))?; + } + + ("local-time", _) => { + println!( + "{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + } + + (subcommand, _) => panic!("unexpected subcommand {}", subcommand), + } + + Ok(()) +} + +struct Error(String); + +impl std::fmt::Debug for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for Error { + fn from(s: String) -> Self { + Error(s) + } +} diff --git a/edgelet/iotedge/Cargo.toml b/edgelet/iotedge/Cargo.toml index 905875e6205..acbd9515499 100644 --- a/edgelet/iotedge/Cargo.toml +++ b/edgelet/iotedge/Cargo.toml @@ -7,15 +7,36 @@ The iotedge tool is used to manage the IoT Edge runtime. """ [dependencies] +atty = "0.2" bytes = "0.4" chrono = "0.4" chrono-humanize = "0.0.11" clap = "2.31" failure = "0.1" futures = "0.1" +hyper = "0.12" +native-tls = "0.2" +openssl = "0.10" +regex = "0.2" +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" tabwriter = "1.0" +termcolor = "0.3" tokio = "0.1" url = "1.7" +docker = { path = "../docker-rs" } +edgelet-config = { path = "../edgelet-config" } edgelet-core = { path = "../edgelet-core" } +edgelet-docker = { path = "../edgelet-docker" } +edgelet-http = { path = "../edgelet-http" } edgelet-http-mgmt = { path = "../edgelet-http-mgmt" } +management = { path = "../management" } +mini-sntp = { path = "../mini-sntp" } + +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[target.'cfg(windows)'.dependencies] +winapi = { version = "0.3", features = ["winsock2"] } diff --git a/edgelet/iotedge/src/check.rs b/edgelet/iotedge/src/check.rs new file mode 100644 index 00000000000..f5a796d3fb4 --- /dev/null +++ b/edgelet/iotedge/src/check.rs @@ -0,0 +1,1723 @@ +// Copyright (c) Microsoft. All rights reserved. + +use std; +use std::borrow::Cow; +use std::ffi::{CStr, OsStr, OsString}; +use std::fs::File; +use std::io::{Read, Write}; +use std::net::TcpStream; +use std::path::PathBuf; +use std::process::Command; + +use failure::Fail; +use failure::{self, Context, ResultExt}; +use futures::future::{self, FutureResult}; +use futures::{Future, IntoFuture, Stream}; +#[cfg(unix)] +use libc; +use regex::Regex; +use serde_json; +use termcolor::WriteColor; + +use edgelet_config::{Provisioning, Settings}; +use edgelet_core::UrlExt; +use edgelet_docker::DockerConfig; +use edgelet_http::client::ClientImpl; +use edgelet_http::MaybeProxyClient; + +use error::{Error, ErrorKind, FetchLatestVersionsReason}; +use LatestVersions; + +pub struct Check { + config_file: PathBuf, + container_engine_config_path: PathBuf, + diagnostics_image_name: String, + iotedged: PathBuf, + latest_versions: Result>, + ntp_server: String, + verbose: bool, + + // These optional fields are populated by the pre-checks + settings: Option>, + docker_host_arg: Option, + docker_server_version: Option, + iothub_hostname: Option, +} + +/// The various ways a check can resolve. +/// +/// Check functions return `Result` where `Err` represents the check failed. +#[derive(Debug)] +enum CheckResult { + /// Check succeeded. + Ok, + + /// Check failed with a warning. + Warning(failure::Error), + + /// Check is not applicable and was ignored. Should be treated as success. + Ignored, + + /// Check was skipped because of errors from some previous checks. Should be treated as an error. + Skipped, + + /// Check failed, and further checks should not be performed. + Fatal(failure::Error), +} + +impl Check { + pub fn new( + config_file: PathBuf, + container_engine_config_path: PathBuf, + diagnostics_image_name: String, + expected_iotedged_version: Option, + iotedged: PathBuf, + ntp_server: String, + verbose: bool, + ) -> impl Future + Send { + let latest_versions = if let Some(expected_iotedged_version) = expected_iotedged_version { + future::Either::A(future::ok::<_, Error>(LatestVersions { + iotedged: expected_iotedged_version, + })) + } else { + let proxy = std::env::var("HTTPS_PROXY") + .ok() + .or_else(|| std::env::var("https_proxy").ok()); + let proxy = if let Some(proxy) = proxy { + let proxy = proxy + .parse::() + .context(ErrorKind::FetchLatestVersions( + FetchLatestVersionsReason::CreateClient, + )); + match proxy { + Ok(proxy) => future::ok(Some(proxy)), + Err(err) => future::err(Error::from(err)), + } + } else { + future::ok(None) + }; + + let hyper_client = proxy.and_then(|proxy| { + Ok( + MaybeProxyClient::new(proxy).context(ErrorKind::FetchLatestVersions( + FetchLatestVersionsReason::CreateClient, + ))?, + ) + }); + + let request = hyper::Request::get("https://aka.ms/latest-iotedge-stable") + .body(hyper::Body::default()) + .expect("can't fail to create request"); + + future::Either::B( + hyper_client + .and_then(|hyper_client| { + hyper_client.call(request).then(|response| { + let response = response.context(ErrorKind::FetchLatestVersions( + FetchLatestVersionsReason::GetResponse, + ))?; + Ok((response, hyper_client)) + }) + }) + .and_then(move |(response, hyper_client)| match response.status() { + hyper::StatusCode::MOVED_PERMANENTLY => { + let uri = response + .headers() + .get(hyper::header::LOCATION) + .ok_or_else(|| { + ErrorKind::FetchLatestVersions( + FetchLatestVersionsReason::InvalidOrMissingLocationHeader, + ) + })? + .to_str() + .context(ErrorKind::FetchLatestVersions( + FetchLatestVersionsReason::InvalidOrMissingLocationHeader, + ))?; + let request = hyper::Request::get(uri) + .body(hyper::Body::default()) + .expect("can't fail to create request"); + Ok(hyper_client.call(request).map_err(|err| { + err.context(ErrorKind::FetchLatestVersions( + FetchLatestVersionsReason::GetResponse, + )) + .into() + })) + } + status_code => Err(ErrorKind::FetchLatestVersions( + FetchLatestVersionsReason::ResponseStatusCode(status_code), + ) + .into()), + }) + .flatten() + .and_then(|response| -> Result<_, Error> { + match response.status() { + hyper::StatusCode::OK => { + Ok(response.into_body().concat2().map_err(|err| { + err.context(ErrorKind::FetchLatestVersions( + FetchLatestVersionsReason::GetResponse, + )) + .into() + })) + } + status_code => Err(ErrorKind::FetchLatestVersions( + FetchLatestVersionsReason::ResponseStatusCode(status_code), + ) + .into()), + } + }) + .flatten() + .and_then(|body| { + Ok(serde_json::from_slice(&body).context( + ErrorKind::FetchLatestVersions(FetchLatestVersionsReason::GetResponse), + )?) + }), + ) + }; + + latest_versions.then(move |latest_versions| { + Ok(Check { + config_file, + container_engine_config_path, + diagnostics_image_name, + iotedged, + ntp_server, + latest_versions: latest_versions.map_err(Some), + verbose, + + settings: None, + docker_host_arg: None, + docker_server_version: None, + iothub_hostname: None, + }) + }) + } + + fn execute_inner(&mut self) -> Result<(), Error> { + const CHECKS: &[( + &str, // Section name + &[( + &str, // Check description + fn(&mut Check) -> Result, // Check function + )], + )] = &[ + ( + "Configuration checks", + &[ + ("config.yaml is well-formed", parse_settings), + ( + "config.yaml has well-formed connection string", + settings_connection_string, + ), + ( + "container engine is installed and functional", + container_engine, + ), + ("config.yaml has correct hostname", settings_hostname), + ( + "config.yaml has correct URIs for daemon mgmt endpoint", + daemon_mgmt_endpoint_uri, + ), + ("latest security daemon", iotedged_version), + ("host time is close to real time", host_local_time), + ("container time is close to host time", container_local_time), + ("DNS server", container_engine_dns), + ("production readiness: certificates", settings_certificates), + ( + "production readiness: certificates expiry", + settings_certificates_expiry, + ), + ( + "production readiness: container engine", + settings_moby_runtime_uri, + ), + ( + "production readiness: logs policy", + container_engine_logrotate, + ), + ], + ), + ( + "Connectivity checks", + &[ + ( + "host can connect to and perform TLS handshake with IoT Hub AMQP port", + |check| connection_to_iot_hub_host(check, 5671), + ), + ( + "host can connect to and perform TLS handshake with IoT Hub HTTPS port", + |check| connection_to_iot_hub_host(check, 443), + ), + ( + "host can connect to and perform TLS handshake with IoT Hub MQTT port", + |check| connection_to_iot_hub_host(check, 8883), + ), + ("container on the default network can connect to IoT Hub AMQP port", |check| { + if cfg!(windows) { + // The default network is the same as the IoT Edge module network, + // so let the module network checks handle it. + Ok(CheckResult::Ignored) + } else { + connection_to_iot_hub_container(check, 5671, false) + } + }), + ("container on the default network can connect to IoT Hub HTTPS port", |check| { + if cfg!(windows) { + // The default network is the same as the IoT Edge module network, + // so let the module network checks handle it. + Ok(CheckResult::Ignored) + } else { + connection_to_iot_hub_container(check, 443, false) + } + }), + ("container on the default network can connect to IoT Hub MQTT port", |check| { + if cfg!(windows) { + // The default network is the same as the IoT Edge module network, + // so let the module network checks handle it. + Ok(CheckResult::Ignored) + } else { + connection_to_iot_hub_container(check, 8883, false) + } + }), + ("container on the IoT Edge module network can connect to IoT Hub AMQP port", |check| { + connection_to_iot_hub_container(check, 5671, true) + }), + ("container on the IoT Edge module network can connect to IoT Hub HTTPS port", |check| { + connection_to_iot_hub_container(check, 443, true) + }), + ("container on the IoT Edge module network can connect to IoT Hub MQTT port", |check| { + connection_to_iot_hub_container(check, 8883, true) + }), + ("Edge Hub can bind to ports on host", edge_hub_ports_on_host), + ], + ), + ]; + + let mut stdout = termcolor::StandardStream::stdout(termcolor::ColorChoice::Auto); + let success_color_spec = { + let mut success_color_spec = termcolor::ColorSpec::new(); + if cfg!(windows) { + // `Color::Green` maps to `FG_GREEN` which is too hard to read on the default blue-background profile that PS uses. + // PS uses `FG_GREEN | FG_INTENSITY` == 8 == `[ConsoleColor]::Green` as the foreground color for its error text, + // so mimic that. + success_color_spec.set_fg(Some(termcolor::Color::Rgb(0, 255, 0))); + } else { + success_color_spec.set_fg(Some(termcolor::Color::Green)); + } + success_color_spec + }; + let warning_color_spec = { + let mut warning_color_spec = termcolor::ColorSpec::new(); + if cfg!(windows) { + // `Color::Yellow` maps to `FOREGROUND_GREEN | FOREGROUND_RED` == 6 == `ConsoleColor::DarkYellow`. + // In its default blue-background profile, PS uses `ConsoleColor::DarkYellow` as its default foreground text color + // and maps it to a dark gray. + // + // So use explicit RGB to define yellow for Windows. Also use a black background to mimic PS warnings. + // + // Ref: + // - https://docs.rs/termcolor/0.3.6/src/termcolor/lib.rs.html#1380 defines `termcolor::Color::Yellow` as `wincolor::Color::Yellow` + // - https://docs.rs/wincolor/0.1.6/x86_64-pc-windows-msvc/src/wincolor/win.rs.html#18 + // defines `wincolor::Color::Yellow` as `FG_YELLOW`, which in turn is `FOREGROUND_GREEN | FOREGROUND_RED` + // - https://docs.microsoft.com/en-us/windows/console/char-info-str defines `FOREGROUND_GREEN | FOREGROUND_RED` as `2 | 4 == 6` + // - https://docs.microsoft.com/en-us/dotnet/api/system.consolecolor#fields defines `6` as `[ConsoleColor]::DarkYellow` + // - `$Host.UI.RawUI.ForegroundColor` in the default PS profile is `DarkYellow`, and writing in it prints dark gray text. + warning_color_spec.set_fg(Some(termcolor::Color::Rgb(255, 255, 0))); + warning_color_spec.set_bg(Some(termcolor::Color::Black)); + } else { + warning_color_spec.set_fg(Some(termcolor::Color::Yellow)); + } + warning_color_spec + }; + let error_color_spec = { + let mut error_color_spec = termcolor::ColorSpec::new(); + if cfg!(windows) { + // `Color::Red` maps to `FG_RED` which is too hard to read on the default blue-background profile that PS uses. + // PS uses `FG_RED | FG_INTENSITY` == 12 == `[ConsoleColor]::Red` as the foreground color for its error text, + // with black background, so mimic that. + error_color_spec.set_fg(Some(termcolor::Color::Rgb(255, 0, 0))); + error_color_spec.set_bg(Some(termcolor::Color::Black)); + } else { + error_color_spec.set_fg(Some(termcolor::Color::Red)); + } + error_color_spec + }; + let is_a_tty = atty::is(atty::Stream::Stdout); + + let mut have_warnings = false; + let mut have_skipped = false; + let mut have_fatal = false; + let mut have_errors = false; + + for (section_name, section_checks) in CHECKS { + if have_fatal { + break; + } + + println!("{}", section_name); + println!("{}", "-".repeat(section_name.len())); + + for (check_name, check) in *section_checks { + if have_fatal { + break; + } + + match check(self) { + Ok(CheckResult::Ok) => { + colored(&mut stdout, &success_color_spec, is_a_tty, |stdout| { + writeln!(stdout, "\u{221a} {}", check_name)?; + Ok(()) + }); + } + + Ok(CheckResult::Warning(warning)) => { + have_warnings = true; + + colored(&mut stdout, &warning_color_spec, is_a_tty, |stdout| { + writeln!(stdout, "\u{203c} {}", check_name)?; + + let message = warning.to_string(); + + write_lines(stdout, " ", " ", message.lines())?; + + if self.verbose { + for cause in warning.iter_causes() { + write_lines( + stdout, + " caused by: ", + " ", + cause.to_string().lines(), + )?; + } + } + + Ok(()) + }); + } + + Ok(CheckResult::Ignored) => (), + + Ok(CheckResult::Skipped) => { + have_skipped = true; + + if self.verbose { + colored(&mut stdout, &warning_color_spec, is_a_tty, |stdout| { + writeln!(stdout, "\u{203c} {}", check_name)?; + writeln!(stdout, " skipping because of previous failures")?; + Ok(()) + }); + } + } + + Ok(CheckResult::Fatal(err)) => { + have_fatal = true; + + colored(&mut stdout, &error_color_spec, is_a_tty, |stdout| { + writeln!(stdout, "\u{00d7} {}", check_name)?; + + let message = err.to_string(); + + write_lines(stdout, " ", " ", message.lines())?; + + if self.verbose { + for cause in err.iter_causes() { + write_lines( + stdout, + " caused by: ", + " ", + cause.to_string().lines(), + )?; + } + } + + Ok(()) + }); + } + + Err(err) => { + have_errors = true; + + colored(&mut stdout, &error_color_spec, is_a_tty, |stdout| { + writeln!(stdout, "\u{00d7} {}", check_name)?; + + let message = err.to_string(); + + write_lines(stdout, " ", " ", message.lines())?; + + if self.verbose { + for cause in err.iter_causes() { + write_lines( + stdout, + " caused by: ", + " ", + cause.to_string().lines(), + )?; + } + } + + Ok(()) + }); + } + } + } + + println!(); + } + + match (have_warnings, have_skipped, have_fatal || have_errors) { + (false, false, false) => { + colored(&mut stdout, &success_color_spec, is_a_tty, |stdout| { + writeln!(stdout, "All checks succeeded.")?; + Ok(()) + }); + + Ok(()) + } + + (_, _, true) => { + colored(&mut stdout, &error_color_spec, is_a_tty, |stdout| { + write!(stdout, "One or more checks raised errors.")?; + if self.verbose { + writeln!(stdout)?; + } else { + writeln!(stdout, " Re-run with --verbose for more details.")?; + } + Ok(()) + }); + + Err(ErrorKind::Diagnostics.into()) + } + + (_, true, _) => { + colored(&mut stdout, &warning_color_spec, is_a_tty, |stdout| { + write!( + stdout, + "One or more checks were skipped due to errors from other checks." + )?; + if self.verbose { + writeln!(stdout)?; + } else { + writeln!(stdout, " Re-run with --verbose for more details.")?; + } + Ok(()) + }); + + Ok(()) + } + + (true, _, _) => { + colored(&mut stdout, &warning_color_spec, is_a_tty, |stdout| { + write!(stdout, "One or more checks raised warnings.")?; + if self.verbose { + writeln!(stdout)?; + } else { + writeln!(stdout, " Re-run with --verbose for more details.")?; + } + Ok(()) + }); + + Ok(()) + } + } + } +} + +impl ::Command for Check { + type Future = FutureResult<(), Error>; + + fn execute(&mut self) -> Self::Future { + self.execute_inner().into_future() + } +} + +fn parse_settings(check: &mut Check) -> Result { + let config_file = &check.config_file; + + // The config crate just returns a "file not found" error when it can't open the file for any reason, + // even if the real error was a permissions issue. + // + // So we first try to open the file for reading ourselves. + if let Err(err) = File::open(config_file) { + if err.kind() == std::io::ErrorKind::PermissionDenied { + return Ok(CheckResult::Fatal( + err.context(format!( + "Could not open file {}. You might need to run this command as {}.", + config_file.display(), + if cfg!(windows) { + "Administrator" + } else { + "root" + }, + )) + .into(), + )); + } else { + return Err(err + .context(format!("Could not open file {}", config_file.display())) + .into()); + } + } + + let settings = match Settings::new(Some(config_file)) { + Ok(settings) => settings, + Err(err) => { + let message = if check.verbose { + format!( + "The IoT Edge daemon's configuration file {} is not well-formed.\n\ + Note: In case of syntax errors, the error may not be exactly at the reported line number and position.", + config_file.display(), + ) + } else { + format!( + "The IoT Edge daemon's configuration file {} is not well-formed.", + config_file.display(), + ) + }; + return Err(err.context(message).into()); + } + }; + + check.settings = Some(settings); + + Ok(CheckResult::Ok) +} + +fn settings_connection_string(check: &mut Check) -> Result { + let settings = if let Some(settings) = &check.settings { + settings + } else { + return Ok(CheckResult::Skipped); + }; + + if let Provisioning::Manual(manual) = settings.provisioning() { + let (_, _, hub) = manual.parse_device_connection_string().context( + "Invalid connection string format detected.\n\ + Please check the value of the provisioning.device_connection_string parameter.", + )?; + check.iothub_hostname = Some(hub.to_owned()); + } + + Ok(CheckResult::Ok) +} + +fn container_engine(check: &mut Check) -> Result { + let settings = if let Some(settings) = &check.settings { + settings + } else { + return Ok(CheckResult::Skipped); + }; + + let uri = settings.moby_runtime().uri(); + + let docker_host_arg = match uri.scheme() { + "unix" => uri.to_string(), + + "npipe" => { + let mut uri = uri.to_string(); + uri.replace_range(0.."npipe://".len(), "npipe:////"); + uri + } + + scheme => { + return Err(Context::new(format!( + "Could not communicate with container engine at {}. The scheme {} is invalid.", + uri, scheme, + )) + .into()); + } + }; + + let output = docker( + &docker_host_arg, + &["version", "--format", "{{.Server.Version}}"], + ); + let output = match output { + Ok(output) => output, + Err((message, err)) => { + let mut error_message = format!( + "Could not communicate with container engine at {}.\n\ + Please check your moby-engine installation and ensure the service is running.", + uri, + ); + + if let Some(message) = message { + #[cfg(unix)] + { + if message.contains("Got permission denied") { + error_message += "\nYou might need to run this command as root."; + return Ok(CheckResult::Fatal(err.context(error_message).into())); + } + } + + #[cfg(windows)] + { + if message.contains("Access is denied") { + error_message += "\nYou might need to run this command as Administrator."; + return Ok(CheckResult::Fatal(err.context(error_message).into())); + } + } + } + + return Err(err.context(error_message).into()); + } + }; + + check.docker_host_arg = Some(docker_host_arg); + + check.docker_server_version = Some(String::from_utf8_lossy(&output).into_owned()); + + Ok(CheckResult::Ok) +} + +fn settings_hostname(check: &mut Check) -> Result { + let settings = if let Some(settings) = &check.settings { + settings + } else { + return Ok(CheckResult::Skipped); + }; + + let config_hostname = settings.hostname(); + + let machine_hostname = unsafe { + let mut result = vec![0_u8; 256]; + + #[cfg(unix)] + { + if libc::gethostname(result.as_mut_ptr() as _, result.len()) != 0 { + return Err( + std::io::Error::last_os_error() // Calls errno + .context("Could not get hostname: gethostname failed") + .into(), + ); + } + } + + #[cfg(windows)] + #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] + { + // libstd only calls WSAStartup when something under std::net gets used, like creating a TcpStream. + // Since we haven't done anything like that up to this point, it ends up not being called. + // So we call it manually. + // + // The process is going to exit anyway, so there's no reason to make the effort of + // calling the corresponding WSACleanup later. + let mut wsa_data: winapi::um::winsock2::WSADATA = std::mem::zeroed(); + match winapi::um::winsock2::WSAStartup(0x202, &mut wsa_data) { + 0 => (), + result => { + return Err(Context::new(format!( + "Could not get hostname: WSAStartup failed with {}", + result, + )) + .into()); + } + } + + if winapi::um::winsock2::gethostname(result.as_mut_ptr() as _, result.len() as _) != 0 { + // Can't use std::io::Error::last_os_error() because that calls GetLastError, not WSAGetLastError + let winsock_err = winapi::um::winsock2::WSAGetLastError(); + + return Err(Context::new(format!( + "Could not get hostname: gethostname failed with {}", + winsock_err, + )) + .into()); + } + } + + let nul_index = result.iter().position(|&b| b == b'\0').ok_or_else(|| { + Context::new("Could not get hostname: gethostname did not return NUL-terminated string") + })?; + + CStr::from_bytes_with_nul_unchecked(&result[..=nul_index]) + .to_str() + .context("Could not get hostname: gethostname returned non-ASCII string")? + .to_owned() + }; + + if config_hostname != machine_hostname { + return Err(Context::new(format!( + "config.yaml has hostname {} but device reports hostname {}", + config_hostname, machine_hostname, + )) + .into()); + } + + Ok(CheckResult::Ok) +} + +fn daemon_mgmt_endpoint_uri(check: &mut Check) -> Result { + let settings = if let Some(settings) = &check.settings { + settings + } else { + return Ok(CheckResult::Skipped); + }; + + let docker_host_arg = if let Some(docker_host_arg) = &check.docker_host_arg { + docker_host_arg + } else { + return Ok(CheckResult::Skipped); + }; + + let connect_management_uri = settings.connect().management_uri(); + let listen_management_uri = settings.listen().management_uri(); + + let mut args: Vec> = vec![ + Cow::Borrowed(OsStr::new("run")), + Cow::Borrowed(OsStr::new("--rm")), + ]; + + for (name, value) in settings.agent().env() { + args.push(Cow::Borrowed(OsStr::new("-e"))); + args.push(Cow::Owned(format!("{}={}", name, value).into())); + } + + match (connect_management_uri.scheme(), listen_management_uri.scheme()) { + ("http", "http") => (), + + ("unix", "unix") | ("unix", "fd") => { + args.push(Cow::Borrowed(OsStr::new("-v"))); + + let socket_path = + connect_management_uri.to_uds_file_path() + .context("Could not parse connect.management_uri: does not represent a valid file path")?; + + // On Windows we mount the parent folder because we can't mount the socket files directly + #[cfg(windows)] + let socket_path = + socket_path.parent() + .ok_or_else(|| Context::new("Could not parse connect.management_uri: does not have a parent directory"))?; + + let socket_path = + socket_path.to_str() + .ok_or_else(|| Context::new("Could not parse connect.management_uri: file path is not valid utf-8"))?; + + args.push(Cow::Owned(format!("{}:{}", socket_path, socket_path).into())); + }, + + (scheme1, scheme2) if scheme1 != scheme2 => return Err(Context::new( + format!( + "config.yaml has invalid combination of schemes for connect.management_uri ({:?}) and listen.management_uri ({:?})", + scheme1, scheme2, + )) + .into()), + + (scheme, _) => return Err(Context::new( + format!("Could not parse connect.management_uri: scheme {} is invalid", scheme), + ).into()), + } + + args.extend(vec![ + Cow::Borrowed(OsStr::new(&check.diagnostics_image_name)), + Cow::Borrowed(OsStr::new("/iotedge-diagnostics")), + Cow::Borrowed(OsStr::new("edge-agent")), + Cow::Borrowed(OsStr::new("--management-uri")), + Cow::Owned(OsString::from(connect_management_uri.to_string())), + ]); + + match docker(docker_host_arg, args) { + Ok(_) => Ok(CheckResult::Ok), + Err((Some(stderr), err)) => Err(err.context(stderr).into()), + Err((None, err)) => Err(err.context("Could not spawn docker process").into()), + } +} + +fn iotedged_version(check: &mut Check) -> Result { + let latest_versions = match &mut check.latest_versions { + Ok(latest_versions) => &*latest_versions, + Err(err) => match err.take() { + Some(err) => return Ok(CheckResult::Warning(err.into())), + None => return Ok(CheckResult::Skipped), + }, + }; + + let mut process = Command::new(&check.iotedged); + process.arg("--version"); + + if cfg!(windows) { + process.env("IOTEDGE_RUN_AS_CONSOLE", "true"); + } + + let output = process + .output() + .context("Could not spawn iotedged process")?; + if !output.status.success() { + return Err(Context::new(format!( + "iotedged returned {}, stderr = {}", + output.status, + String::from_utf8_lossy(&*output.stderr), + )) + .context("Could not spawn iotedged process") + .into()); + } + + let output = + String::from_utf8(output.stdout).context("Could not parse output of iotedged --version")?; + + let iotedged_version_regex = Regex::new(r"^iotedged ([^ ]+)(?: \(.*\))?$") + .expect("This hard-coded regex is expected to be valid."); + let captures = iotedged_version_regex + .captures(output.trim()) + .ok_or_else(|| { + Context::new(format!( + "output {:?} does not match expected format", + output, + )) + .context("Could not parse output of iotedged --version") + })?; + let version = captures + .get(1) + .expect("unreachable: regex defines one capturing group") + .as_str(); + + if version != latest_versions.iotedged { + return Ok(CheckResult::Warning( + Context::new(format!( + "Installed IoT Edge daemon has version {} but version {} is available.\n\ + Please see https://aka.ms/iotedge-update-runtime for update instructions.", + version, latest_versions.iotedged, + )) + .into(), + )); + } + + Ok(CheckResult::Ok) +} + +fn host_local_time(check: &mut Check) -> Result { + fn is_server_unreachable_error(err: &mini_sntp::Error) -> bool { + match err.kind() { + mini_sntp::ErrorKind::ResolveNtpPoolHostname(_) => true, + mini_sntp::ErrorKind::SendClientRequest(err) + | mini_sntp::ErrorKind::ReceiveServerResponse(err) => { + err.kind() == std::io::ErrorKind::TimedOut || // Windows + err.kind() == std::io::ErrorKind::WouldBlock // Unix + } + _ => false, + } + } + + let mini_sntp::SntpTimeQueryResult { + local_clock_offset, .. + } = match mini_sntp::query(&check.ntp_server) { + Ok(result) => result, + Err(err) => { + if is_server_unreachable_error(&err) { + return Ok(CheckResult::Warning( + err.context("Could not query NTP server").into(), + )); + } else { + return Err(err.context("Could not query NTP server").into()); + } + } + }; + + if local_clock_offset.num_seconds().abs() >= 10 { + return Ok(CheckResult::Warning(Context::new(format!( + "Time on the device is out of sync with the NTP server. This may cause problems connecting to IoT Hub.\n\ + Please ensure time on device is accurate, for example by {}.", + if cfg!(windows) { + "setting up the Windows Time service to automatically sync with a time server" + } else { + "installing an NTP daemon" + }, + )).into())); + } + + Ok(CheckResult::Ok) +} + +fn container_local_time(check: &mut Check) -> Result { + let docker_host_arg = if let Some(docker_host_arg) = &check.docker_host_arg { + docker_host_arg + } else { + return Ok(CheckResult::Skipped); + }; + + let expected_duration = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .context("Could not query local time of host")?; + + let output = docker( + docker_host_arg, + vec![ + "run", + "--rm", + &check.diagnostics_image_name, + "/iotedge-diagnostics", + "local-time", + ], + ) + .map_err(|(_, err)| err) + .context("Could not query local time inside container")?; + let output = std::str::from_utf8(&output) + .map_err(failure::Error::from) + .and_then(|output| output.trim_end().parse::().map_err(Into::into)) + .context("Could not parse container output")?; + let actual_duration = std::time::Duration::from_secs(output); + + let diff = std::cmp::max(actual_duration, expected_duration) + - std::cmp::min(actual_duration, expected_duration); + if diff.as_secs() >= 10 { + return Err(Context::new("Detected time drift between host and container").into()); + } + + Ok(CheckResult::Ok) +} + +fn container_engine_dns(check: &mut Check) -> Result { + const MESSAGE: &str = + "Container engine is not configured with DNS server setting, which may impact connectivity to IoT Hub.\n\ + Please see https://aka.ms/iotedge-prod-checklist-dns for best practices.\n\ + You can ignore this warning if you are setting DNS server per module in the Edge deployment."; + + #[derive(serde_derive::Deserialize)] + struct DaemonConfig { + dns: Option>, + } + + let daemon_config_file = File::open(&check.container_engine_config_path) + .with_context(|_| { + format!( + "Could not open container engine config file {}", + check.container_engine_config_path.display(), + ) + }) + .context(MESSAGE); + let daemon_config_file = match daemon_config_file { + Ok(daemon_config_file) => daemon_config_file, + Err(err) => { + return Ok(CheckResult::Warning(err.into())); + } + }; + let daemon_config: DaemonConfig = serde_json::from_reader(daemon_config_file) + .with_context(|_| { + format!( + "Could not parse container engine config file {}", + check.container_engine_config_path.display(), + ) + }) + .context(MESSAGE)?; + + if let Some(&[]) | None = daemon_config.dns.as_ref().map(std::ops::Deref::deref) { + return Ok(CheckResult::Warning(Context::new(MESSAGE).into())); + } + + Ok(CheckResult::Ok) +} + +fn settings_certificates(check: &mut Check) -> Result { + let settings = if let Some(settings) = &check.settings { + settings + } else { + return Ok(CheckResult::Skipped); + }; + + if settings.certificates().is_none() { + return Ok(CheckResult::Warning( + Context::new( + "Device is using self-signed, automatically generated certs.\n\ + Please see https://aka.ms/iotedge-prod-checklist-certs for best practices.", + ) + .into(), + )); + } + + Ok(CheckResult::Ok) +} + +fn settings_certificates_expiry(check: &mut Check) -> Result { + fn parse_openssl_time( + time: &openssl::asn1::Asn1TimeRef, + ) -> chrono::ParseResult> { + // openssl::asn1::Asn1TimeRef does not expose any way to convert the ASN1_TIME to a Rust-friendly type + // + // Its Display impl uses ASN1_TIME_print, so we convert it into a String and parse it back + // into a chrono::DateTime + let time = time.to_string(); + let time = chrono::NaiveDateTime::parse_from_str(&time, "%b %e %H:%M:%S %Y GMT")?; + Ok(chrono::DateTime::::from_utc(time, chrono::Utc)) + } + + let settings = if let Some(settings) = &check.settings { + settings + } else { + return Ok(CheckResult::Skipped); + }; + + let (device_ca_cert_path, device_ca_cert_path_source) = if let Some(certificates) = + settings.certificates() + { + ( + certificates.device_ca_cert().to_owned(), + Cow::Borrowed("certificates.device_ca_cert"), + ) + } else { + let certs_dir = settings.homedir().join("hsm").join("certs"); + + let mut device_ca_cert_path = None; + + let entries = std::fs::read_dir(&certs_dir) + .with_context(|_| format!("Could not enumerate files under {}", certs_dir.display()))?; + for entry in entries { + let entry = entry.with_context(|_| { + format!("Could not enumerate files under {}", certs_dir.display()) + })?; + let path = entry.path(); + let is_device_ca_cert = + if let Some(file_name) = path.file_name().and_then(OsStr::to_str) { + file_name.starts_with("device_ca_alias") && file_name.ends_with(".cert.pem") + } else { + false + }; + if is_device_ca_cert { + device_ca_cert_path = Some(path); + break; + } + } + + let device_ca_cert_path = device_ca_cert_path.ok_or_else(|| { + Context::new(format!( + "Could not find device CA certificate under {}", + certs_dir.display(), + )) + })?; + let device_ca_cert_path_source = device_ca_cert_path.to_string_lossy().into_owned(); + (device_ca_cert_path, Cow::Owned(device_ca_cert_path_source)) + }; + + let (not_after, not_before) = File::open(device_ca_cert_path) + .map_err(failure::Error::from) + .and_then(|mut device_ca_cert_file| { + let mut device_ca_cert = vec![]; + device_ca_cert_file.read_to_end(&mut device_ca_cert)?; + let device_ca_cert = openssl::x509::X509::stack_from_pem(&device_ca_cert)?; + let device_ca_cert = &device_ca_cert[0]; + + let not_after = parse_openssl_time(device_ca_cert.not_after())?; + let not_before = parse_openssl_time(device_ca_cert.not_before())?; + + Ok((not_after, not_before)) + }) + .with_context(|_| { + format!( + "Could not parse {} as a valid certificate file", + device_ca_cert_path_source, + ) + })?; + + let now = chrono::Utc::now(); + + if not_before > now { + return Err(Context::new(format!( + "Device CA certificate in {} has not-before time {} which is in the future", + device_ca_cert_path_source, not_before, + )) + .into()); + } + + if not_after < now { + return Err(Context::new(format!( + "Device CA certificate in {} expired at {}", + device_ca_cert_path_source, not_after, + )) + .into()); + } + + if not_after < now + chrono::Duration::days(7) { + return Ok(CheckResult::Warning( + Context::new(format!( + "Device CA certificate in {} will expire soon ({})", + device_ca_cert_path_source, not_after, + )) + .into(), + )); + } + + Ok(CheckResult::Ok) +} + +fn settings_moby_runtime_uri(check: &mut Check) -> Result { + const MESSAGE: &str = + "Device is not using a production-supported container engine (moby-engine).\n\ + Please see https://aka.ms/iotedge-prod-checklist-moby for details."; + + let settings = if let Some(settings) = &check.settings { + settings + } else { + return Ok(CheckResult::Skipped); + }; + + let docker_server_version = if let Some(docker_server_version) = &check.docker_server_version { + docker_server_version + } else { + return Ok(CheckResult::Skipped); + }; + + if cfg!(windows) { + let moby_runtime_uri = settings.moby_runtime().uri().to_string(); + + if moby_runtime_uri != "npipe://./pipe/iotedge_moby_engine" { + return Ok(CheckResult::Warning(Context::new(MESSAGE).into())); + } + } + + let docker_server_major_version = docker_server_version + .split('.') + .next() + .map(std::str::FromStr::from_str); + let docker_server_major_version: u32 = match docker_server_major_version { + Some(Ok(docker_server_major_version)) => docker_server_major_version, + Some(Err(_)) | None => { + return Ok(CheckResult::Warning( + Context::new(format!( + "Container engine returned malformed version string {:?}", + docker_server_version, + )) + .context(MESSAGE) + .into(), + )); + } + }; + + // Moby does not identify itself in any unique way. Moby devs recommend assuming that anything less than version 10 is Moby, + // since it's currently 3.x and regular Docker is in the high 10s. + if docker_server_major_version >= 10 { + return Ok(CheckResult::Warning(Context::new(MESSAGE).into())); + } + + Ok(CheckResult::Ok) +} + +fn container_engine_logrotate(check: &mut Check) -> Result { + const MESSAGE: &str = + "Container engine is not configured to rotate module logs which may cause it run out of disk space.\n\ + Please see https://aka.ms/iotedge-prod-checklist-logs for best practices.\n\ + You can ignore this warning if you are setting log policy per module in the Edge deployment."; + + #[derive(serde_derive::Deserialize)] + struct DaemonConfig { + #[serde(rename = "log-driver")] + log_driver: Option, + + #[serde(rename = "log-opts")] + log_opts: Option, + } + + #[derive(serde_derive::Deserialize)] + struct DaemonConfigLogOpts { + #[serde(rename = "max-file")] + max_file: Option, + + #[serde(rename = "max-size")] + max_size: Option, + } + + let daemon_config_file = File::open(&check.container_engine_config_path) + .with_context(|_| { + format!( + "Could not open container engine config file {}", + check.container_engine_config_path.display(), + ) + }) + .context(MESSAGE); + let daemon_config_file = match daemon_config_file { + Ok(daemon_config_file) => daemon_config_file, + Err(err) => { + return Ok(CheckResult::Warning(err.into())); + } + }; + let daemon_config: DaemonConfig = serde_json::from_reader(daemon_config_file) + .with_context(|_| { + format!( + "Could not parse container engine config file {}", + check.container_engine_config_path.display(), + ) + }) + .context(MESSAGE)?; + + if daemon_config.log_driver.is_none() { + return Ok(CheckResult::Warning(Context::new(MESSAGE).into())); + } + + if let Some(log_opts) = &daemon_config.log_opts { + if log_opts.max_file.is_none() { + return Ok(CheckResult::Warning(Context::new(MESSAGE).into())); + } + + if log_opts.max_size.is_none() { + return Ok(CheckResult::Warning(Context::new(MESSAGE).into())); + } + } else { + return Ok(CheckResult::Warning(Context::new(MESSAGE).into())); + } + + Ok(CheckResult::Ok) +} + +fn connection_to_iot_hub_host(check: &mut Check, port: u16) -> Result { + let iothub_hostname = if let Some(iothub_hostname) = &check.iothub_hostname { + iothub_hostname + } else { + return Ok(CheckResult::Skipped); + }; + + let iothub_host = std::net::ToSocketAddrs::to_socket_addrs(&(&**iothub_hostname, port)) + .with_context(|_| { + format!( + "Could not connect to {}:{} : could not resolve hostname", + iothub_hostname, port, + ) + })? + .next() + .ok_or_else(|| { + Context::new(format!( + "Could not connect to {}:{} : could not resolve hostname: no addresses found", + iothub_hostname, port, + )) + })?; + + let stream = TcpStream::connect_timeout(&iothub_host, std::time::Duration::from_secs(10)) + .with_context(|_| format!("Could not connect to {}:{}", iothub_hostname, port))?; + + let tls_connector = native_tls::TlsConnector::new().with_context(|_| { + format!( + "Could not connect to {}:{} : could not create TLS connector", + iothub_hostname, port, + ) + })?; + + let _ = tls_connector + .connect(iothub_hostname, stream) + .with_context(|_| { + format!( + "Could not connect to {}:{} : could not complete TLS handshake", + iothub_hostname, port, + ) + })?; + + Ok(CheckResult::Ok) +} + +fn connection_to_iot_hub_container( + check: &mut Check, + port: u16, + use_container_runtime_network: bool, +) -> Result { + let settings = if let Some(settings) = &check.settings { + settings + } else { + return Ok(CheckResult::Skipped); + }; + + let docker_host_arg = if let Some(docker_host_arg) = &check.docker_host_arg { + docker_host_arg + } else { + return Ok(CheckResult::Skipped); + }; + + let iothub_hostname = if let Some(iothub_hostname) = &check.iothub_hostname { + iothub_hostname + } else { + return Ok(CheckResult::Skipped); + }; + + let network_name = settings.moby_runtime().network(); + + let port = port.to_string(); + + let mut args = vec!["run", "--rm"]; + + if use_container_runtime_network { + args.extend(&["--network", network_name]); + } + + args.extend(&[ + &check.diagnostics_image_name, + "/iotedge-diagnostics", + "iothub", + "--hostname", + iothub_hostname, + "--port", + &port, + ]); + + if let Err((_, err)) = docker(docker_host_arg, args) { + return Err(err + .context(format!( + "Container on the {} network could not connect to {}:{}", + if use_container_runtime_network { + network_name + } else { + "default" + }, + iothub_hostname, + port, + )) + .into()); + } + + Ok(CheckResult::Ok) +} + +fn edge_hub_ports_on_host(check: &mut Check) -> Result { + let docker_host_arg = if let Some(docker_host_arg) = &check.docker_host_arg { + docker_host_arg + } else { + return Ok(CheckResult::Skipped); + }; + + let inspect_result = docker(docker_host_arg, vec!["inspect", "edgeHub"]) + .map_err(|(_, err)| err) + .and_then(|output| { + let (inspect_result,): (docker::models::InlineResponse200,) = + serde_json::from_slice(&output) + .context("could not parse result of docker inspect")?; + Ok(inspect_result) + }) + .context("Could not check current state of Edge Hub container")?; + + let is_running = inspect_result + .state() + .and_then(docker::models::InlineResponse200State::running) + .cloned() + .ok_or_else(|| { + Context::new( + "Could not check current state of Edge Hub container: \ + could not parse result of docker inspect: state.status is not set", + ) + })?; + if is_running { + // Whatever ports it wanted to bind to must've been available for it to be running + return Ok(CheckResult::Ok); + } + + let port_bindings = inspect_result + .host_config() + .and_then(docker::models::HostConfig::port_bindings) + .ok_or_else(|| { + Context::new( + "Could not check port bindings of Edge Hub container: \ + could not parse result of docker inspect: host_config.port_bindings is not set", + ) + })? + .values() + .flatten() + .filter_map(docker::models::HostConfigPortBindings::host_port); + + for port_binding in port_bindings { + // Try to bind to the port ourselves. If it fails with AddrInUse, then something else has bound to it. + match std::net::TcpListener::bind(format!("127.0.0.1:{}", port_binding)) { + Ok(_) => (), + + Err(ref err) if err.kind() == std::io::ErrorKind::AddrInUse => { + return Err(Context::new(format!( + "Edge hub cannot start on device because port {} is already in use.\n\ + Please stop the application using the port or remove the port binding from Edge hub's deployment.", + port_binding, + )).into()); + } + + #[cfg(unix)] + Err(ref err) if err.kind() == std::io::ErrorKind::PermissionDenied => { + return Ok(CheckResult::Fatal(Context::new(format!( + "Permission denied when attempting to bind to port {}. You might need to run this command as root.", + port_binding, + )).into())); + } + + Err(err) => { + return Err(err + .context(format!( + "Could not check if port {} is available for Edge Hub to bind to", + port_binding, + )) + .into()); + } + } + } + + Ok(CheckResult::Ok) +} + +fn colored( + stdout: &mut termcolor::StandardStream, + spec: &termcolor::ColorSpec, + is_a_tty: bool, + f: F, +) where + F: FnOnce(&mut termcolor::StandardStream) -> std::io::Result<()>, +{ + if is_a_tty { + let _ = stdout.set_color(spec); + } + + f(stdout).expect("could not write to stdout"); + + if is_a_tty { + let _ = stdout.reset(); + } +} + +fn docker(docker_host_arg: &str, args: I) -> Result, (Option, failure::Error)> +where + I: IntoIterator, + ::Item: AsRef, +{ + let mut process = Command::new("docker"); + process.arg("-H"); + process.arg(docker_host_arg); + + process.args(args); + + let output = process.output().map_err(|err| { + ( + None, + err.context(format!("could not run {:?}", process)).into(), + ) + })?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&*output.stderr).into_owned(); + let err = Context::new(format!( + "docker returned {}, stderr = {}", + output.status, stderr, + )) + .into(); + return Err((Some(stderr), err)); + } + + Ok(output.stdout) +} + +fn write_lines<'a>( + writer: &mut impl Write, + first_line_indent: &str, + other_lines_indent: &str, + mut lines: impl Iterator, +) -> std::io::Result<()> { + if let Some(line) = lines.next() { + writeln!(writer, "{}{}", first_line_indent, line)?; + } + + for line in lines { + writeln!(writer, "{}{}", other_lines_indent, line)?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + #[test] + fn config_file_checks_ok() { + let mut runtime = tokio::runtime::current_thread::Runtime::new().unwrap(); + + for filename in &[ + "sample_settings.yaml", + "sample_settings.dps.sym.yaml", + "sample_settings.tg.yaml", + ] { + let config_file = format!( + "{}/../edgelet-config/test/{}/{}", + env!("CARGO_MANIFEST_DIR"), + if cfg!(windows) { "windows" } else { "linux" }, + filename, + ); + + let mut check = runtime + .block_on(super::Check::new( + config_file.into(), + "daemon.json".into(), // unused for this test + "mcr.microsoft.com/azureiotedge-diagnostics:1.0.0".to_owned(), // unused for this test + Some("1.0.0".to_owned()), // unused for this test + "iotedged".into(), // unused for this test + "pool.ntp.org:123".to_owned(), // unused for this test + false, + )) + .unwrap(); + + match super::parse_settings(&mut check) { + Ok(super::CheckResult::Ok) => (), + check_result => panic!("parsing {} returned {:?}", filename, check_result), + } + + match super::settings_connection_string(&mut check) { + Ok(super::CheckResult::Ok) => (), + check_result => panic!( + "checking connection string in {} returned {:?}", + filename, check_result + ), + } + + match super::settings_hostname(&mut check) { + Err(err) => { + let message = err.to_string(); + assert!( + message + .starts_with("config.yaml has hostname localhost but device reports"), + "checking hostname in {} produced unexpected error: {}", + filename, + message, + ); + } + check_result => panic!( + "checking hostname in {} returned {:?}", + filename, check_result + ), + } + + // Pretend it's Moby + check.docker_server_version = Some("3.0.3".to_owned()); + + match super::settings_moby_runtime_uri(&mut check) { + Ok(super::CheckResult::Ok) => (), + check_result => panic!( + "checking moby_runtime.uri in {} returned {:?}", + filename, check_result + ), + } + } + } + + #[test] + fn parse_settings_err() { + let mut runtime = tokio::runtime::current_thread::Runtime::new().unwrap(); + + let filename = "bad_sample_settings.yaml"; + let config_file = format!( + "{}/../edgelet-config/test/{}/{}", + env!("CARGO_MANIFEST_DIR"), + if cfg!(windows) { "windows" } else { "linux" }, + filename, + ); + + let mut check = runtime + .block_on(super::Check::new( + config_file.into(), + "daemon.json".into(), // unused for this test + "mcr.microsoft.com/azureiotedge-diagnostics:1.0.0".to_owned(), // unused for this test + Some("1.0.0".to_owned()), // unused for this test + "iotedged".into(), // unused for this test + "pool.ntp.org:123".to_owned(), // unused for this test + false, + )) + .unwrap(); + + match super::parse_settings(&mut check) { + Err(err) => { + let err = err + .iter_causes() + .nth(1) + .expect("expected to find cause-of-cause-of-error"); + assert!( + err.to_string() + .contains("while parsing a flow mapping, did not find expected ',' or '}' at line 10 column 5"), + "parsing {} produced unexpected error: {}", + filename, + err, + ); + } + + check_result => panic!("parsing {} returned {:?}", filename, check_result), + } + } + + #[test] + #[cfg(windows)] + fn moby_runtime_uri_windows_wants_moby_based_on_runtime_uri() { + let mut runtime = tokio::runtime::current_thread::Runtime::new().unwrap(); + + let filename = "sample_settings_notmoby.yaml"; + let config_file = format!( + "{}/../edgelet-config/test/{}/{}", + env!("CARGO_MANIFEST_DIR"), + if cfg!(windows) { "windows" } else { "linux" }, + filename, + ); + + let mut check = runtime + .block_on(super::Check::new( + config_file.into(), + "daemon.json".into(), // unused for this test + "mcr.microsoft.com/azureiotedge-diagnostics:1.0.0".to_owned(), // unused for this test + Some("1.0.0".to_owned()), // unused for this test + "iotedged".into(), // unused for this test + "pool.ntp.org:123".to_owned(), // unused for this test + false, + )) + .unwrap(); + + match super::parse_settings(&mut check) { + Ok(super::CheckResult::Ok) => (), + check_result => panic!("parsing {} returned {:?}", filename, check_result), + } + + // Pretend it's Moby even though named pipe indicates otherwise + check.docker_server_version = Some("3.0.3".to_owned()); + + match super::settings_moby_runtime_uri(&mut check) { + Ok(super::CheckResult::Warning(warning)) => assert!( + warning.to_string().contains( + "Device is not using a production-supported container engine (moby-engine)." + ), + "checking moby_runtime.uri in {} failed with an unexpected warning: {}", + filename, + warning + ), + + check_result => panic!( + "checking moby_runtime.uri in {} returned {:?}", + filename, check_result + ), + } + } + + #[test] + fn moby_runtime_uri_wants_moby_based_on_server_version() { + let mut runtime = tokio::runtime::current_thread::Runtime::new().unwrap(); + + let filename = "sample_settings.yaml"; + let config_file = format!( + "{}/../edgelet-config/test/{}/{}", + env!("CARGO_MANIFEST_DIR"), + if cfg!(windows) { "windows" } else { "linux" }, + filename, + ); + + let mut check = runtime + .block_on(super::Check::new( + config_file.into(), + "daemon.json".into(), // unused for this test + "mcr.microsoft.com/azureiotedge-diagnostics:1.0.0".to_owned(), // unused for this test + Some("1.0.0".to_owned()), // unused for this test + "iotedged".into(), // unused for this test + "pool.ntp.org:123".to_owned(), // unused for this test + false, + )) + .unwrap(); + + match super::parse_settings(&mut check) { + Ok(super::CheckResult::Ok) => (), + check_result => panic!("parsing {} returned {:?}", filename, check_result), + } + + // Pretend it's Docker + check.docker_server_version = Some("18.09.1".to_owned()); + + match super::settings_moby_runtime_uri(&mut check) { + Ok(super::CheckResult::Warning(warning)) => assert!( + warning.to_string().contains( + "Device is not using a production-supported container engine (moby-engine)." + ), + "checking moby_runtime.uri in {} failed with an unexpected warning: {}", + filename, + warning + ), + + check_result => panic!( + "checking moby_runtime.uri in {} returned {:?}", + filename, check_result + ), + } + } +} diff --git a/edgelet/iotedge/src/error.rs b/edgelet/iotedge/src/error.rs index f14c376ad19..55061d3c375 100644 --- a/edgelet/iotedge/src/error.rs +++ b/edgelet/iotedge/src/error.rs @@ -10,11 +10,20 @@ pub struct Error { inner: Context, } -#[derive(Clone, Copy, Debug, Fail)] +#[derive(Clone, Debug, Fail)] pub enum ErrorKind { #[fail(display = "Invalid value for --host parameter")] BadHostParameter, + #[fail(display = "")] + Diagnostics, + + #[fail( + display = "Error while fetching latest versions of edge components: {}", + _0 + )] + FetchLatestVersions(FetchLatestVersionsReason), + #[fail(display = "Missing --host parameter")] MissingHostParameter, @@ -57,3 +66,27 @@ impl From> for Error { Error { inner } } } + +#[derive(Clone, Copy, Debug)] +pub enum FetchLatestVersionsReason { + CreateClient, + GetResponse, + InvalidOrMissingLocationHeader, + ResponseStatusCode(hyper::StatusCode), +} + +impl Display for FetchLatestVersionsReason { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FetchLatestVersionsReason::CreateClient => write!(f, "could not create HTTP client"), + FetchLatestVersionsReason::GetResponse => write!(f, "could not send HTTP request"), + FetchLatestVersionsReason::InvalidOrMissingLocationHeader => write!( + f, + "redirect response has invalid or missing location header" + ), + FetchLatestVersionsReason::ResponseStatusCode(status_code) => { + write!(f, "response failed with status code {}", status_code) + } + } + } +} diff --git a/edgelet/iotedge/src/lib.rs b/edgelet/iotedge/src/lib.rs index bc256178a59..c6bebdb0ef3 100644 --- a/edgelet/iotedge/src/lib.rs +++ b/edgelet/iotedge/src/lib.rs @@ -2,22 +2,38 @@ #![deny(unused_extern_crates, warnings)] #![deny(clippy::all, clippy::pedantic)] -#![allow(clippy::module_name_repetitions, clippy::use_self)] +#![allow( + clippy::module_name_repetitions, + clippy::type_complexity, + clippy::use_self +)] extern crate bytes; extern crate chrono; extern crate chrono_humanize; #[macro_use] extern crate clap; -extern crate edgelet_core; extern crate failure; #[macro_use] extern crate futures; +#[cfg(unix)] +extern crate libc; +extern crate regex; +#[macro_use] +extern crate serde_derive; +extern crate serde_json; extern crate tabwriter; +extern crate termcolor; extern crate tokio; +extern crate edgelet_config; +extern crate edgelet_core; +extern crate edgelet_docker; +extern crate edgelet_http; + use futures::Future; +mod check; mod error; mod list; mod logs; @@ -25,7 +41,8 @@ mod restart; mod unknown; mod version; -pub use error::{Error, ErrorKind}; +pub use check::Check; +pub use error::{Error, ErrorKind, FetchLatestVersionsReason}; pub use list::List; pub use logs::Logs; pub use restart::Restart; @@ -33,7 +50,12 @@ pub use unknown::Unknown; pub use version::Version; pub trait Command { - type Future: Future + Send; + type Future: Future + Send; fn execute(&mut self) -> Self::Future; } + +#[derive(Debug, Deserialize)] +pub struct LatestVersions { + pub iotedged: String, +} diff --git a/edgelet/iotedge/src/main.rs b/edgelet/iotedge/src/main.rs index b95a191502e..e1ab5fcfa34 100644 --- a/edgelet/iotedge/src/main.rs +++ b/edgelet/iotedge/src/main.rs @@ -2,22 +2,24 @@ #![deny(unused_extern_crates, warnings)] #![deny(clippy::all, clippy::pedantic)] +#![allow(clippy::similar_names)] #[macro_use] extern crate clap; extern crate edgelet_core; extern crate edgelet_http_mgmt; extern crate failure; +extern crate futures; extern crate iotedge; extern crate tokio; extern crate url; use std::io; -use std::io::Write; use std::process; use clap::{App, AppSettings, Arg, SubCommand}; use failure::{Fail, ResultExt}; +use futures::Future; use url::Url; use edgelet_core::{LogOptions, LogTail}; @@ -30,24 +32,39 @@ const MGMT_URI: &str = "unix:///var/run/iotedge/mgmt.sock"; #[cfg(windows)] const MGMT_URI: &str = "unix:///C:/ProgramData/iotedge/mgmt/sock"; +#[cfg(unix)] +const DEFAULT_CONFIG_PATH: &str = "/etc/iotedge/config.yaml"; +#[cfg(windows)] +const DEFAULT_CONFIG_PATH: &str = r"C:\ProgramData\iotedge\config.yaml"; + +#[cfg(unix)] +const DEFAULT_CONTAINER_ENGINE_CONFIG_PATH: &str = "/etc/docker/daemon.json"; +#[cfg(windows)] +const DEFAULT_CONTAINER_ENGINE_CONFIG_PATH: &str = + r"C:\ProgramData\iotedge-moby\config\daemon.json"; + fn main() { if let Err(ref error) = run() { - let stderr = &mut io::stderr(); - let errmsg = "Error writing to stderr"; - let mut fail: &Fail = error; - writeln!(stderr, "{}", error.to_string()).unwrap_or_else(|_| panic!(errmsg)); - while let Some(cause) = fail.cause() { - writeln!(stderr, "\tcaused by: {}", cause.to_string()) - .unwrap_or_else(|_| panic!(errmsg)); - fail = cause; + + eprintln!("{}", error.to_string()); + + for cause in fail.iter_causes() { + eprintln!("\tcaused by: {}", cause); } + + eprintln!(); + process::exit(1); } } fn run() -> Result<(), Error> { let default_uri = option_env!("IOTEDGE_HOST").unwrap_or(MGMT_URI); + let default_diagnostics_image_name = format!( + "mcr.microsoft.com/azureiotedge-diagnostics:{}", + edgelet_core::version() + ); let matches = App::new(crate_name!()) .version(edgelet_core::version()) @@ -64,6 +81,67 @@ fn run() -> Result<(), Error> { .env("IOTEDGE_HOST") .default_value(default_uri), ) + .subcommand( + SubCommand::with_name("check") + .about("Check for common config and deployment issues") + .arg( + Arg::with_name("config-file") + .short("c") + .long("config-file") + .value_name("FILE") + .help("Sets daemon configuration file") + .takes_value(true) + .default_value(DEFAULT_CONFIG_PATH), + ) + .arg( + Arg::with_name("container-engine-config-file") + .long("container-engine-config-file") + .value_name("FILE") + .help("Sets the path of the container engine configuration file") + .takes_value(true) + .default_value(DEFAULT_CONTAINER_ENGINE_CONFIG_PATH), + ) + .arg( + Arg::with_name("diagnostics-image-name") + .long("diagnostics-image-name") + .value_name("IMAGE_NAME") + .help("Sets the name of the azureiotedge-diagnostics image.") + .takes_value(true) + .default_value(&default_diagnostics_image_name), + ) + .arg( + Arg::with_name("expected-iotedged-version") + .long("expected-iotedged-version") + .value_name("VERSION") + .help("Sets the expected version of the iotedged binary. Defaults to the value contained in ") + .takes_value(true), + ) + .arg( + Arg::with_name("iotedged") + .long("iotedged") + .value_name("PATH_TO_IOTEDGED") + .help("Sets the path of the iotedged binary.") + .takes_value(true) + .default_value( + if cfg!(windows) { r"C:\Program Files\iotedge\iotedged.exe" } else { "/usr/bin/iotedged" } + ), + ) + .arg( + Arg::with_name("ntp-server") + .long("ntp-server") + .value_name("NTP_SERVER") + .help("Sets the NTP server to use when checking host local time.") + .takes_value(true) + .default_value("pool.ntp.org:123"), + ) + .arg( + Arg::with_name("verbose") + .long("verbose") + .value_name("VERBOSE") + .help("Increases verbosity of output.") + .takes_value(false), + ), + ) .subcommand(SubCommand::with_name("list").about("List modules")) .subcommand( SubCommand::with_name("restart") @@ -115,6 +193,32 @@ fn run() -> Result<(), Error> { let mut tokio_runtime = tokio::runtime::Runtime::new().context(ErrorKind::InitializeTokio)?; match matches.subcommand() { + ("check", Some(args)) => tokio_runtime.block_on( + Check::new( + args.value_of_os("config-file") + .expect("arg has a default value") + .to_os_string() + .into(), + args.value_of_os("container-engine-config-file") + .expect("arg has a default value") + .to_os_string() + .into(), + args.value_of("diagnostics-image-name") + .expect("arg has a default value") + .to_string(), + args.value_of("expected-iotedged-version") + .map(ToOwned::to_owned), + args.value_of_os("iotedged") + .expect("arg has a default value") + .to_os_string() + .into(), + args.value_of("ntp-server") + .expect("arg has a default value") + .to_string(), + args.occurrences_of("verbose") > 0, + ) + .and_then(|mut check| check.execute()), + ), ("list", Some(_args)) => tokio_runtime.block_on(List::new(runtime, io::stdout()).execute()), ("restart", Some(args)) => tokio_runtime.block_on( Restart::new( diff --git a/edgelet/iotedged/Cargo.toml b/edgelet/iotedged/Cargo.toml index d26965820e5..c5eba19c114 100644 --- a/edgelet/iotedged/Cargo.toml +++ b/edgelet/iotedged/Cargo.toml @@ -7,24 +7,21 @@ publish = false [dependencies] base64 = "0.9" clap = "2.31" -config = "0.8" env_logger = "0.5" failure = "0.1" futures = "0.1" hyper = "0.12.17" log = "0.4" -serde = "1.0" -serde_derive = "1.0" serde_json = "1.0" sha2 = "0.7.0" tokio = "0.1.8" tokio-signal = "0.2" url = "1.7" -url_serde = "0.2" hsm = { path = "../hsm-rs"} dps = { path = "../dps" } docker = { path = "../docker-rs" } +edgelet-config = { path = "../edgelet-config" } edgelet-core = { path = "../edgelet-core" } edgelet-docker = { path = "../edgelet-docker" } edgelet-hsm = { path = "../edgelet-hsm" } diff --git a/edgelet/iotedged/src/app.rs b/edgelet/iotedged/src/app.rs index 5846b636a2a..e9dccf0b5cf 100644 --- a/edgelet/iotedged/src/app.rs +++ b/edgelet/iotedged/src/app.rs @@ -1,16 +1,20 @@ // Copyright (c) Microsoft. All rights reserved. +use std::path::Path; + use clap::{App, Arg, ArgMatches}; +use failure::ResultExt; + +use edgelet_config::Settings; use edgelet_core; use edgelet_docker::DockerConfig; -use error::Error; +use error::{Error, ErrorKind, InitializeErrorReason}; use logging; -use settings::Settings; pub fn create_base_app<'a, 'b>() -> App<'a, 'b> { App::new(crate_name!()) - .version(crate_version!()) + .version(edgelet_core::version()) .author(crate_authors!("\n")) .about(crate_description!()) .arg( @@ -50,9 +54,10 @@ pub fn init_common<'a>() -> Result<(Settings, ArgMatches<'a>), Err let matches = create_app().get_matches(); let settings = { let config_file = matches - .value_of("config-file") + .value_of_os("config-file") .and_then(|name| { - info!("Using config file: {}", name); + let name = Path::new(name); + info!("Using config file: {}", name.display()); Some(name) }) .or_else(|| { @@ -60,7 +65,8 @@ pub fn init_common<'a>() -> Result<(Settings, ArgMatches<'a>), Err None }); - Settings::::new(config_file)? + Settings::::new(config_file) + .context(ErrorKind::Initialize(InitializeErrorReason::LoadSettings))? }; Ok((settings, matches)) @@ -84,13 +90,15 @@ pub fn init() -> Result, Error> { #[cfg(not(target_os = "windows"))] pub fn init() -> Result, Error> { logging::init(); + let (settings, _) = init_common()?; log_banner(); - init_common().map(|(settings, _)| settings) + Ok(settings) } #[cfg(target_os = "windows")] pub fn init_win_svc() -> Result, Error> { logging::init_win_log(); + let (settings, _) = init_common()?; log_banner(); - init_common().map(|(settings, _)| settings) + Ok(settings) } diff --git a/edgelet/iotedged/src/lib.rs b/edgelet/iotedged/src/lib.rs index 93826909402..c0bc8063861 100644 --- a/edgelet/iotedged/src/lib.rs +++ b/edgelet/iotedged/src/lib.rs @@ -12,9 +12,9 @@ extern crate base64; #[macro_use] extern crate clap; -extern crate config; extern crate docker; extern crate dps; +extern crate edgelet_config; extern crate edgelet_core; extern crate edgelet_docker; extern crate edgelet_hsm; @@ -34,17 +34,13 @@ extern crate iothubservice; #[macro_use] extern crate log; extern crate provisioning; -extern crate serde; -extern crate sha2; -#[macro_use] -extern crate serde_derive; extern crate serde_json; +extern crate sha2; #[cfg(test)] extern crate tempdir; extern crate tokio; extern crate tokio_signal; extern crate url; -extern crate url_serde; #[cfg(target_os = "windows")] #[macro_use] extern crate windows_service; @@ -54,7 +50,6 @@ extern crate win_logger; pub mod app; mod error; pub mod logging; -pub mod settings; pub mod signal; pub mod workload; @@ -82,20 +77,22 @@ use url::Url; use docker::models::HostConfig; use dps::DPS_API_VERSION; +use edgelet_config::{Dps, Manual, Provisioning, Settings, DEFAULT_CONNECTION_STRING}; use edgelet_core::crypto::{ Activate, CreateCertificate, Decrypt, DerivedKeyStore, Encrypt, GetTrustBundle, KeyIdentity, KeyStore, MasterEncryptionKey, MemoryKey, MemoryKeyStore, Sign, IOTEDGED_CA_ALIAS, }; use edgelet_core::watchdog::Watchdog; -use edgelet_core::WorkloadConfig; -use edgelet_core::{CertificateIssuer, CertificateProperties, CertificateType}; -use edgelet_core::{ModuleRuntime, ModuleSpec}; +use edgelet_core::{ + CertificateIssuer, CertificateProperties, CertificateType, ModuleRuntime, ModuleSpec, UrlExt, + WorkloadConfig, UNIX_SCHEME, +}; use edgelet_docker::{DockerConfig, DockerModuleRuntime}; use edgelet_hsm::tpm::{TpmKey, TpmKeyStore}; use edgelet_hsm::Crypto; use edgelet_http::client::{Client as HttpClient, ClientImpl}; use edgelet_http::logging::LoggingService; -use edgelet_http::{HyperExt, MaybeProxyClient, UrlExt, API_VERSION}; +use edgelet_http::{HyperExt, MaybeProxyClient, API_VERSION}; use edgelet_http_mgmt::ManagementService; use edgelet_http_workload::WorkloadService; use edgelet_iothub::{HubIdentityManager, SasTokenSource}; @@ -107,7 +104,6 @@ use provisioning::provisioning::{ Provision, ProvisioningResult, }; -use settings::{Dps, Manual, Provisioning, Settings, DEFAULT_CONNECTION_STRING}; use workload::WorkloadData; pub use self::error::{Error, ErrorKind, InitializeErrorReason}; @@ -171,7 +167,6 @@ const EDGE_NETWORKID_KEY: &str = "NetworkId"; const API_VERSION_KEY: &str = "IOTEDGE_APIVERSION"; const IOTHUB_API_VERSION: &str = "2017-11-08-preview"; -const UNIX_SCHEME: &str = "unix"; /// This is the name of the provisioning backup file const EDGE_PROVISIONING_BACKUP_FILENAME: &str = "provisioning_backup.json"; @@ -429,7 +424,7 @@ where info!("Detecting if configuration file has changed..."); let path = subdir_path.join(filename); let mut reconfig_reqd = false; - let diff = settings.diff_with_cached(path)?; + let diff = settings.diff_with_cached(&path); if diff { info!("Change to configuration file detected."); reconfig_reqd = true; @@ -613,9 +608,13 @@ fn manual_provision( provisioning: &Manual, tokio_runtime: &mut tokio::runtime::Runtime, ) -> Result<(DerivedKeyStore, ProvisioningResult, MemoryKey), Error> { - let manual = ManualProvisioning::new(provisioning.device_connection_string()).context( - ErrorKind::Initialize(InitializeErrorReason::ManualProvisioningClient), - )?; + let (key, device_id, hub) = + provisioning + .parse_device_connection_string() + .context(ErrorKind::Initialize( + InitializeErrorReason::ManualProvisioningClient, + ))?; + let manual = ManualProvisioning::new(key, device_id, hub); let memory_hsm = MemoryKeyStore::new(); let provision = manual .provision(memory_hsm.clone()) @@ -991,6 +990,7 @@ where mod tests { use std::fmt; use std::io::Read; + use std::path::Path; use tempdir::TempDir; @@ -1002,14 +1002,14 @@ mod tests { use super::*; #[cfg(unix)] - static SETTINGS: &str = "test/linux/sample_settings.yaml"; + static SETTINGS: &str = "../edgelet-config/test/linux/sample_settings.yaml"; #[cfg(unix)] - static SETTINGS1: &str = "test/linux/sample_settings1.yaml"; + static SETTINGS1: &str = "../edgelet-config/test/linux/sample_settings1.yaml"; #[cfg(windows)] - static SETTINGS: &str = "test/windows/sample_settings.yaml"; + static SETTINGS: &str = "../edgelet-config/test/windows/sample_settings.yaml"; #[cfg(windows)] - static SETTINGS1: &str = "test/windows/sample_settings1.yaml"; + static SETTINGS1: &str = "../edgelet-config/test/windows/sample_settings1.yaml"; #[derive(Clone, Copy, Debug, Fail)] pub struct Error; @@ -1020,12 +1020,6 @@ mod tests { } } - // impl From for super::Error { - // fn from(_error: Error) -> Self { - // super::Error::from(ErrorKind::Var) - // } - // } - struct TestCrypto {} impl MasterEncryptionKey for TestCrypto { @@ -1071,7 +1065,7 @@ mod tests { #[test] fn settings_first_time_creates_backup() { let tmp_dir = TempDir::new("blah").unwrap(); - let settings = Settings::::new(Some(SETTINGS)).unwrap(); + let settings = Settings::::new(Some(Path::new(SETTINGS))).unwrap(); let config = TestConfig::new("microsoft/test-image".to_string()); let state = ModuleRuntimeState::default(); let module: TestModule = @@ -1103,7 +1097,7 @@ mod tests { #[test] fn settings_change_creates_new_backup() { let tmp_dir = TempDir::new("blah").unwrap(); - let settings = Settings::::new(Some(SETTINGS)).unwrap(); + let settings = Settings::::new(Some(Path::new(SETTINGS))).unwrap(); let config = TestConfig::new("microsoft/test-image".to_string()); let state = ModuleRuntimeState::default(); let module: TestModule = @@ -1126,7 +1120,7 @@ mod tests { .read_to_string(&mut written) .unwrap(); - let settings1 = Settings::::new(Some(SETTINGS1)).unwrap(); + let settings1 = Settings::::new(Some(Path::new(SETTINGS1))).unwrap(); let mut tokio_runtime = tokio::runtime::Runtime::new().unwrap(); check_settings_state( tmp_dir.path().to_path_buf(), diff --git a/edgelet/mini-sntp/Cargo.toml b/edgelet/mini-sntp/Cargo.toml new file mode 100644 index 00000000000..d29c2e07723 --- /dev/null +++ b/edgelet/mini-sntp/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "mini-sntp" +version = "0.1.0" +authors = ["Azure IoT Edge Devs"] +edition = "2018" +publish = false + +[dependencies] +chrono = "0.4" +failure = "0.1" diff --git a/edgelet/mini-sntp/src/error.rs b/edgelet/mini-sntp/src/error.rs new file mode 100644 index 00000000000..063c453023b --- /dev/null +++ b/edgelet/mini-sntp/src/error.rs @@ -0,0 +1,126 @@ +#[derive(Debug)] +pub struct Error(failure::Context); + +impl Error { + pub fn kind(&self) -> &ErrorKind { + self.0.get_context() + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl failure::Fail for Error { + fn cause(&self) -> Option<&dyn failure::Fail> { + self.0.cause() + } + + fn backtrace(&self) -> Option<&failure::Backtrace> { + self.0.backtrace() + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Self { + Error(failure::Context::new(kind)) + } +} + +impl From> for Error { + fn from(context: failure::Context) -> Self { + Error(context) + } +} + +#[derive(Debug)] +pub enum ErrorKind { + BadServerResponse(BadServerResponseReason), + BindLocalSocket, + ReceiveServerResponse(std::io::Error), + ResolveNtpPoolHostname(Option), + SendClientRequest(std::io::Error), + SetReadTimeoutOnSocket, + SetWriteTimeoutOnSocket, +} + +impl std::fmt::Display for ErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ErrorKind::BadServerResponse(reason) => { + write!(f, "could not parse NTP server response: {}", reason) + } + ErrorKind::BindLocalSocket => write!(f, "could not bind local UDP socket"), + ErrorKind::ReceiveServerResponse(err) => { + write!(f, "could not receive NTP server response: {}", err) + } + ErrorKind::ResolveNtpPoolHostname(Some(err)) => { + write!(f, "could not resolve NTP pool hostname: {}", err) + } + ErrorKind::ResolveNtpPoolHostname(None) => { + write!(f, "could not resolve NTP pool hostname: no addresses found") + } + ErrorKind::SendClientRequest(err) => { + write!(f, "could not send SNTP client request: {}", err) + } + ErrorKind::SetReadTimeoutOnSocket => { + write!(f, "could not set read timeout on local UDP socket") + } + ErrorKind::SetWriteTimeoutOnSocket => { + write!(f, "could not set write timeout on local UDP socket") + } + } + } +} + +impl failure::Fail for ErrorKind { + fn cause(&self) -> Option<&dyn failure::Fail> { + #[allow(clippy::match_same_arms)] + match self { + ErrorKind::BadServerResponse(_) => None, + ErrorKind::BindLocalSocket => None, + ErrorKind::ReceiveServerResponse(err) => Some(err), + ErrorKind::ResolveNtpPoolHostname(Some(err)) => Some(err), + ErrorKind::ResolveNtpPoolHostname(None) => None, + ErrorKind::SendClientRequest(err) => Some(err), + ErrorKind::SetReadTimeoutOnSocket => None, + ErrorKind::SetWriteTimeoutOnSocket => None, + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum BadServerResponseReason { + LeapIndicator(u8), + OriginateTimestamp { + expected: chrono::DateTime, + actual: chrono::DateTime, + }, + Mode(u8), + VersionNumber(u8), +} + +impl std::fmt::Display for BadServerResponseReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BadServerResponseReason::LeapIndicator(leap_indicator) => { + write!(f, "invalid value of leap indicator {}", leap_indicator) + } + BadServerResponseReason::OriginateTimestamp { expected, actual } => write!( + f, + "expected originate timestamp to be {} but it was {}", + expected, actual + ), + BadServerResponseReason::Mode(mode) => { + write!(f, "expected mode to be 4 but it was {}", mode) + } + BadServerResponseReason::VersionNumber(version_number) => write!( + f, + "expected version number to be 3 but it was {}", + version_number + ), + } + } +} diff --git a/edgelet/mini-sntp/src/lib.rs b/edgelet/mini-sntp/src/lib.rs new file mode 100644 index 00000000000..168f054c35b --- /dev/null +++ b/edgelet/mini-sntp/src/lib.rs @@ -0,0 +1,340 @@ +#![deny(rust_2018_idioms, warnings)] +#![deny(clippy::all, clippy::pedantic)] +#![allow( + clippy::doc_markdown, + clippy::module_name_repetitions, + clippy::use_self +)] + +use std::net::{SocketAddr, ToSocketAddrs, UdpSocket}; + +use failure::ResultExt; + +mod error; +pub use error::{BadServerResponseReason, Error, ErrorKind}; + +/// The result of [`query`] +#[derive(Debug)] +pub struct SntpTimeQueryResult { + pub local_clock_offset: chrono::Duration, + pub round_trip_delay: chrono::Duration, +} + +/// Executes an SNTP query against the NTPv3 server at the given address. +/// +/// Ref: +pub fn query(addr: &A) -> Result +where + A: ToSocketAddrs, +{ + let addr = addr + .to_socket_addrs() + .map_err(|err| ErrorKind::ResolveNtpPoolHostname(Some(err)))? + .next() + .ok_or(ErrorKind::ResolveNtpPoolHostname(None))?; + + let socket = UdpSocket::bind("0.0.0.0:0").context(ErrorKind::BindLocalSocket)?; + socket + .set_read_timeout(Some(std::time::Duration::from_secs(10))) + .context(ErrorKind::SetReadTimeoutOnSocket)?; + socket + .set_write_timeout(Some(std::time::Duration::from_secs(10))) + .context(ErrorKind::SetWriteTimeoutOnSocket)?; + + let mut num_retries_remaining = 3; + while num_retries_remaining > 0 { + match query_inner(&socket, addr) { + Ok(result) => return Ok(result), + Err(err) => { + let is_retriable = match err.kind() { + ErrorKind::SendClientRequest(err) | ErrorKind::ReceiveServerResponse(err) => { + err.kind() == std::io::ErrorKind::TimedOut || // Windows + err.kind() == std::io::ErrorKind::WouldBlock // Unix + } + + _ => false, + }; + if is_retriable { + num_retries_remaining -= 1; + if num_retries_remaining == 0 { + return Err(err); + } + } else { + return Err(err); + } + } + } + } + + unreachable!(); +} + +fn query_inner(socket: &UdpSocket, addr: SocketAddr) -> Result { + let request_transmit_timestamp = { + let (buf, request_transmit_timestamp) = create_client_request(); + + #[cfg(test)] + std::thread::sleep(std::time::Duration::from_secs(5)); // simulate network delay + + let mut buf = &buf[..]; + while !buf.is_empty() { + let sent = socket + .send_to(buf, addr) + .map_err(ErrorKind::SendClientRequest)?; + buf = &buf[sent..]; + } + + request_transmit_timestamp + }; + + let result = { + let mut buf = [0_u8; 48]; + + { + let mut buf = &mut buf[..]; + while !buf.is_empty() { + let (received, received_from) = socket + .recv_from(buf) + .map_err(ErrorKind::ReceiveServerResponse)?; + if received_from == addr { + buf = &mut buf[received..]; + } + } + } + + #[cfg(test)] + std::thread::sleep(std::time::Duration::from_secs(5)); // simulate network delay + + parse_server_response(buf, request_transmit_timestamp)? + }; + + Ok(result) +} + +fn create_client_request() -> ([u8; 48], chrono::DateTime) { + let sntp_epoch = sntp_epoch(); + + let mut buf = [0_u8; 48]; + buf[0] = 0b00_011_011; // version_number: 3, mode: 3 (client) + + let transmit_timestamp = chrono::Utc::now(); + + #[cfg(test)] + let transmit_timestamp = transmit_timestamp - chrono::Duration::seconds(30); // simulate unsynced local clock + + let mut duration_since_sntp_epoch = transmit_timestamp - sntp_epoch; + + let integral_part = duration_since_sntp_epoch.num_seconds(); + duration_since_sntp_epoch = + duration_since_sntp_epoch - chrono::Duration::seconds(integral_part); + + assert!(integral_part >= 0 && integral_part < i64::from(u32::max_value())); + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let integral_part = (integral_part as u32).to_be_bytes(); + buf[40..44].copy_from_slice(&integral_part[..]); + + let fractional_part = duration_since_sntp_epoch + .num_nanoseconds() + .expect("can't overflow nanoseconds"); + let fractional_part = (fractional_part << 32) / 1_000_000_000; + assert!(fractional_part >= 0 && fractional_part < i64::from(u32::max_value())); + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let fractional_part = (fractional_part as u32).to_be_bytes(); + buf[44..48].copy_from_slice(&fractional_part[..]); + + let packet = Packet::parse(buf, sntp_epoch); + #[cfg(test)] + let packet = dbg!(packet); + + // Re-extract transmit timestamp from the packet. This may not be the same as the original `transmit_timestamp` + // that was serialized into the packet due to rounding. Specifically, it's usually off by 1ns. + let transmit_timestamp = packet.transmit_timestamp; + + (buf, transmit_timestamp) +} + +fn parse_server_response( + buf: [u8; 48], + request_transmit_timestamp: chrono::DateTime, +) -> Result { + let sntp_epoch = sntp_epoch(); + + let destination_timestamp = chrono::Utc::now(); + + #[cfg(test)] + let destination_timestamp = destination_timestamp - chrono::Duration::seconds(30); // simulate unsynced local clock + + let packet = Packet::parse(buf, sntp_epoch); + #[cfg(test)] + let packet = dbg!(packet); + + match packet.leap_indicator { + 0..=2 => (), + leap_indicator => { + return Err( + ErrorKind::BadServerResponse(BadServerResponseReason::LeapIndicator( + leap_indicator, + )) + .into(), + ); + } + }; + + if packet.version_number != 3 { + return Err( + ErrorKind::BadServerResponse(BadServerResponseReason::VersionNumber( + packet.version_number, + )) + .into(), + ); + } + + if packet.mode != 4 { + return Err(ErrorKind::BadServerResponse(BadServerResponseReason::Mode(packet.mode)).into()); + } + + if packet.mode != 4 { + return Err(ErrorKind::BadServerResponse(BadServerResponseReason::Mode(packet.mode)).into()); + } + + if packet.originate_timestamp != request_transmit_timestamp { + return Err( + ErrorKind::BadServerResponse(BadServerResponseReason::OriginateTimestamp { + expected: request_transmit_timestamp, + actual: packet.originate_timestamp, + }) + .into(), + ); + } + + Ok(SntpTimeQueryResult { + local_clock_offset: ((packet.receive_timestamp - request_transmit_timestamp) + + (packet.transmit_timestamp - destination_timestamp)) + / 2, + + round_trip_delay: (destination_timestamp - request_transmit_timestamp) + - (packet.receive_timestamp - packet.transmit_timestamp), + }) +} + +fn sntp_epoch() -> chrono::DateTime { + chrono::DateTime::::from_utc( + chrono::NaiveDate::from_ymd(1900, 1, 1).and_time(chrono::NaiveTime::from_hms(0, 0, 0)), + chrono::Utc, + ) +} + +#[derive(Debug)] +struct Packet { + leap_indicator: u8, + version_number: u8, + mode: u8, + stratum: u8, + poll_interval: u8, + precision: u8, + root_delay: u32, + root_dispersion: u32, + reference_identifier: u32, + reference_timestamp: chrono::DateTime, + originate_timestamp: chrono::DateTime, + receive_timestamp: chrono::DateTime, + transmit_timestamp: chrono::DateTime, +} + +impl Packet { + fn parse(buf: [u8; 48], sntp_epoch: chrono::DateTime) -> Self { + let leap_indicator = (buf[0] & 0b11_000_000) >> 6; + let version_number = (buf[0] & 0b00_111_000) >> 3; + let mode = buf[0] & 0b00_000_111; + let stratum = buf[1]; + let poll_interval = buf[2]; + let precision = buf[3]; + let root_delay = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]); + let root_dispersion = u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]]); + let reference_identifier = u32::from_be_bytes([buf[12], buf[13], buf[14], buf[15]]); + let reference_timestamp = deserialize_timestamp( + [ + buf[16], buf[17], buf[18], buf[19], buf[20], buf[21], buf[22], buf[23], + ], + sntp_epoch, + ); + let originate_timestamp = deserialize_timestamp( + [ + buf[24], buf[25], buf[26], buf[27], buf[28], buf[29], buf[30], buf[31], + ], + sntp_epoch, + ); + let receive_timestamp = deserialize_timestamp( + [ + buf[32], buf[33], buf[34], buf[35], buf[36], buf[37], buf[38], buf[39], + ], + sntp_epoch, + ); + let transmit_timestamp = deserialize_timestamp( + [ + buf[40], buf[41], buf[42], buf[43], buf[44], buf[45], buf[46], buf[47], + ], + sntp_epoch, + ); + + Packet { + leap_indicator, + version_number, + mode, + stratum, + poll_interval, + precision, + root_delay, + root_dispersion, + reference_identifier, + reference_timestamp, + originate_timestamp, + receive_timestamp, + transmit_timestamp, + } + } +} + +fn deserialize_timestamp( + raw: [u8; 8], + sntp_epoch: chrono::DateTime, +) -> chrono::DateTime { + let integral_part = i64::from(u32::from_be_bytes([raw[0], raw[1], raw[2], raw[3]])); + let fractional_part = i64::from(u32::from_be_bytes([raw[4], raw[5], raw[6], raw[7]])); + let duration_since_sntp_epoch = chrono::Duration::nanoseconds( + integral_part * 1_000_000_000 + ((fractional_part * 1_000_000_000) >> 32), + ); + + sntp_epoch + duration_since_sntp_epoch +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() -> Result<(), Error> { + let SntpTimeQueryResult { + local_clock_offset, + round_trip_delay, + } = query(&("pool.ntp.org", 123))?; + + println!("local clock offset: {}", local_clock_offset); + println!("round-trip delay: {}", round_trip_delay); + + assert!( + (local_clock_offset - chrono::Duration::seconds(30)) + .num_seconds() + .abs() + < 1 + ); + assert!( + (round_trip_delay - chrono::Duration::seconds(10)) + .num_seconds() + .abs() + < 1 + ); + + Ok(()) + } +} diff --git a/edgelet/provisioning/Cargo.toml b/edgelet/provisioning/Cargo.toml index 4ee31481fa9..00b07923fa0 100644 --- a/edgelet/provisioning/Cargo.toml +++ b/edgelet/provisioning/Cargo.toml @@ -4,12 +4,10 @@ version = "0.1.0" authors = ["Azure IoT Edge Devs"] [dependencies] -base64 = "0.9" bytes = "0.4" failure = "0.1" futures = "0.1" log = "0.4" -regex = "0.2" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" @@ -25,3 +23,5 @@ edgelet-utils = { path = "../edgelet-utils" } [dev_dependencies] tempdir = "0.3.7" tokio = "0.1.8" + +edgelet-config = { path = "../edgelet-config" } diff --git a/edgelet/provisioning/src/error.rs b/edgelet/provisioning/src/error.rs index 475cfd29385..96c08037cab 100644 --- a/edgelet/provisioning/src/error.rs +++ b/edgelet/provisioning/src/error.rs @@ -12,15 +12,6 @@ pub struct Error { #[derive(Clone, Copy, Debug, Fail)] pub enum ErrorKind { - #[fail(display = "The Connection String is missing required parameter {}", _0)] - ConnStringMissingRequiredParameter(&'static str), - - #[fail( - display = "The Connection String has a malformed value for parameter {}.", - _0 - )] - ConnStringMalformedParameter(&'static str), - #[fail(display = "Could not backup provisioning result")] CouldNotBackup, @@ -30,11 +21,6 @@ pub enum ErrorKind { #[fail(display = "Could not initialize DPS provisioning client")] DpsInitialization, - #[fail( - display = "The Connection String is empty or invalid. Please update the config.yaml and provide the IoTHub connection information." - )] - InvalidConnString, - #[fail(display = "Could not provision device")] Provision, } diff --git a/edgelet/provisioning/src/lib.rs b/edgelet/provisioning/src/lib.rs index cb498820dea..5c0cd5acad3 100644 --- a/edgelet/provisioning/src/lib.rs +++ b/edgelet/provisioning/src/lib.rs @@ -4,14 +4,12 @@ #![deny(clippy::all, clippy::pedantic)] #![allow(clippy::module_name_repetitions, clippy::use_self)] -extern crate base64; extern crate bytes; extern crate failure; extern crate futures; extern crate hsm; #[macro_use] extern crate log; -extern crate regex; #[macro_use] extern crate serde_derive; extern crate serde_json; @@ -22,6 +20,8 @@ extern crate tokio; extern crate url; extern crate dps; +#[cfg(test)] +extern crate edgelet_config; extern crate edgelet_core; extern crate edgelet_hsm; extern crate edgelet_http; diff --git a/edgelet/provisioning/src/provisioning.rs b/edgelet/provisioning/src/provisioning.rs index 7421e0f32b1..6ce358e7262 100644 --- a/edgelet/provisioning/src/provisioning.rs +++ b/edgelet/provisioning/src/provisioning.rs @@ -1,17 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. -use std::collections::HashMap; use std::fs::File; use std::io::{Read, Write}; use std::path::PathBuf; -use base64; use bytes::Bytes; use failure::{Fail, ResultExt}; use futures::future::Either; use futures::{future, Future, IntoFuture}; -use regex::Regex; use serde_json; use url::Url; @@ -19,19 +16,11 @@ use dps::registration::{DpsAuthKind, DpsClient, DpsTokenSource}; use edgelet_core::crypto::{Activate, KeyIdentity, KeyStore, MemoryKey, MemoryKeyStore}; use edgelet_hsm::tpm::{TpmKey, TpmKeyStore}; use edgelet_http::client::{Client as HttpClient, ClientImpl}; -use edgelet_utils::{ensure_not_empty_with_context, log_failure}; +use edgelet_utils::log_failure; use error::{Error, ErrorKind}; use hsm::TpmKey as HsmTpmKey; use log::Level; -const DEVICEID_KEY: &str = "DeviceId"; -const HOSTNAME_KEY: &str = "HostName"; -const SHAREDACCESSKEY_KEY: &str = "SharedAccessKey"; - -const DEVICEID_REGEX: &str = r"^[A-Za-z0-9\-:.+%_#*?!(),=@;$']{1,128}$"; -const HOSTNAME_REGEX: &str = r"^[a-zA-Z0-9_\-\.]+$"; -const SHAREDACCESSKEY_REGEX: &str = r"^.+$"; - #[derive(Clone, Serialize, Deserialize)] pub struct ProvisioningResult { device_id: String, @@ -71,68 +60,12 @@ pub struct ManualProvisioning { } impl ManualProvisioning { - pub fn new(conn_string: &str) -> Result { - ensure_not_empty_with_context(&conn_string, || ErrorKind::InvalidConnString)?; - - let hash_map = ManualProvisioning::parse_conn_string(&conn_string)?; - - let key = hash_map.get(SHAREDACCESSKEY_KEY).ok_or( - ErrorKind::ConnStringMissingRequiredParameter(SHAREDACCESSKEY_KEY), - )?; - let key_regex = Regex::new(SHAREDACCESSKEY_REGEX) - .expect("This hard-coded regex is expected to be valid."); - if !key_regex.is_match(&key) { - return Err(Error::from(ErrorKind::ConnStringMalformedParameter( - SHAREDACCESSKEY_REGEX, - ))); - } - let key = MemoryKey::new(base64::decode(&key).context( - ErrorKind::ConnStringMalformedParameter(SHAREDACCESSKEY_REGEX), - )?); - - let device_id = hash_map - .get(DEVICEID_KEY) - .ok_or(ErrorKind::ConnStringMissingRequiredParameter(DEVICEID_KEY))?; - let device_id_regex = - Regex::new(DEVICEID_REGEX).expect("This hard-coded regex is expected to be valid."); - if !device_id_regex.is_match(&device_id) { - return Err(Error::from(ErrorKind::ConnStringMalformedParameter( - DEVICEID_KEY, - ))); - } - - let hub = hash_map - .get(HOSTNAME_KEY) - .ok_or(ErrorKind::ConnStringMissingRequiredParameter(HOSTNAME_KEY))?; - let hub_regex = - Regex::new(HOSTNAME_REGEX).expect("This hard-coded regex is expected to be valid."); - if !hub_regex.is_match(&hub) { - return Err(Error::from(ErrorKind::ConnStringMalformedParameter( - HOSTNAME_KEY, - ))); - } - - let result = ManualProvisioning { + pub fn new(key: MemoryKey, device_id: String, hub: String) -> Self { + ManualProvisioning { key, - device_id: device_id.to_owned(), - hub: hub.to_owned(), - }; - Ok(result) - } - - fn parse_conn_string(conn_string: &str) -> Result, Error> { - let mut hash_map = HashMap::new(); - let parts: Vec<&str> = conn_string.split(';').collect(); - for p in parts { - let s: Vec<&str> = p.split('=').collect(); - match s[0] { - SHAREDACCESSKEY_KEY | DEVICEID_KEY | HOSTNAME_KEY => { - hash_map.insert(s[0].to_string(), s[1].to_string()); - } - _ => (), // Ignore extraneous component in the connection string - } + device_id, + hub, } - Ok(hash_map) } } @@ -407,6 +340,8 @@ mod tests { use tempdir::TempDir; use tokio; + use edgelet_config::{Manual, ParseManualDeviceConnectionStringError}; + use error::ErrorKind; struct TestProvisioning {} @@ -439,24 +374,29 @@ mod tests { } } + fn parse_connection_string( + s: &str, + ) -> Result { + let (key, device_id, hub) = Manual::new(s.to_string()).parse_device_connection_string()?; + Ok(ManualProvisioning::new(key, device_id, hub)) + } + #[test] fn manual_get_credentials_success() { let provisioning = - ManualProvisioning::new("HostName=test.com;DeviceId=test;SharedAccessKey=test"); - assert_eq!(provisioning.is_ok(), true); + parse_connection_string("HostName=test.com;DeviceId=test;SharedAccessKey=test") + .unwrap(); let memory_hsm = MemoryKeyStore::new(); - let task = - provisioning - .unwrap() - .provision(memory_hsm.clone()) - .then(|result| match result { - Ok(result) => { - assert_eq!(result.hub_name, "test.com".to_string()); - assert_eq!(result.device_id, "test".to_string()); - Ok::<_, Error>(()) - } - Err(err) => panic!("Unexpected {:?}", err), - }); + let task = provisioning + .provision(memory_hsm.clone()) + .then(|result| match result { + Ok(result) => { + assert_eq!(result.hub_name, "test.com".to_string()); + assert_eq!(result.device_id, "test".to_string()); + Ok::<_, Error>(()) + } + Err(err) => panic!("Unexpected {:?}", err), + }); tokio::runtime::current_thread::Runtime::new() .unwrap() .block_on(task) @@ -465,42 +405,36 @@ mod tests { #[test] fn manual_malformed_conn_string_gets_error() { - let test = ManualProvisioning::new("HostName=test.com;DeviceId=test;"); - assert_eq!(test.is_err(), true); + let test = parse_connection_string("HostName=test.com;DeviceId=test;"); + assert!(test.is_err()); } #[test] fn connection_string_split_success() { let provisioning = - ManualProvisioning::new("HostName=test.com;DeviceId=test;SharedAccessKey=test"); - assert_eq!(provisioning.is_ok(), true); + parse_connection_string("HostName=test.com;DeviceId=test;SharedAccessKey=test") + .unwrap(); let memory_hsm = MemoryKeyStore::new(); - let task = provisioning - .unwrap() - .provision(memory_hsm.clone()) - .then(|result| { - let result = result.expect("Unexpected"); - assert_eq!(result.hub_name, "test.com".to_string()); - assert_eq!(result.device_id, "test".to_string()); - Ok::<_, Error>(()) - }); + let task = provisioning.provision(memory_hsm.clone()).then(|result| { + let result = result.expect("Unexpected"); + assert_eq!(result.hub_name, "test.com".to_string()); + assert_eq!(result.device_id, "test".to_string()); + Ok::<_, Error>(()) + }); tokio::runtime::current_thread::Runtime::new() .unwrap() .block_on(task) .unwrap(); let provisioning1 = - ManualProvisioning::new("DeviceId=test;SharedAccessKey=test;HostName=test.com"); - assert_eq!(provisioning1.is_ok(), true); - let task1 = provisioning1 - .unwrap() - .provision(memory_hsm.clone()) - .then(|result| { - let result = result.expect("Unexpected"); - assert_eq!(result.hub_name, "test.com".to_string()); - assert_eq!(result.device_id, "test".to_string()); - Ok::<_, Error>(()) - }); + parse_connection_string("DeviceId=test;SharedAccessKey=test;HostName=test.com") + .unwrap(); + let task1 = provisioning1.provision(memory_hsm.clone()).then(|result| { + let result = result.expect("Unexpected"); + assert_eq!(result.hub_name, "test.com".to_string()); + assert_eq!(result.device_id, "test".to_string()); + Ok::<_, Error>(()) + }); tokio::runtime::current_thread::Runtime::new() .unwrap() .block_on(task1) @@ -509,14 +443,15 @@ mod tests { #[test] fn connection_string_split_error() { - let test1 = ManualProvisioning::new("DeviceId=test;SharedAccessKey=test"); - assert_eq!(test1.is_err(), true); - let test2 = ManualProvisioning::new( + let test1 = parse_connection_string("DeviceId=test;SharedAccessKey=test"); + assert!(test1.is_err()); + + let test2 = parse_connection_string( "HostName=test.com;Extra=something;DeviceId=test;SharedAccessKey=test", - ); - assert_eq!(test2.is_ok(), true); + ) + .unwrap(); let memory_hsm = MemoryKeyStore::new(); - let task1 = test2.unwrap().provision(memory_hsm.clone()).then(|result| { + let task1 = test2.provision(memory_hsm.clone()).then(|result| { let result = result.expect("Unexpected"); assert_eq!(result.hub_name, "test.com".to_string()); assert_eq!(result.device_id, "test".to_string()); diff --git a/scripts/linux/buildDiagnostics.sh b/scripts/linux/buildDiagnostics.sh new file mode 100755 index 00000000000..1d3692b1b43 --- /dev/null +++ b/scripts/linux/buildDiagnostics.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# This script builds the iotedge-diagnostics binary that goes into the azureiotedge-diagnostics image, +# for each supported arch (x86_64, arm32v7). +# It then publishes the binaries along with their corresponding dockerfiles to the publish directory, +# so that buildImage.sh can build the container image. + +set -euo pipefail + +VERSIONINFO_FILE_PATH=$BUILD_REPOSITORY_LOCALPATH/versionInfo.json +PUBLISH_FOLDER=$BUILD_BINARIESDIRECTORY/publish +export VERSION="$(cat "$VERSIONINFO_FILE_PATH" | jq '.version' -r)" + +mkdir -p $PUBLISH_FOLDER/azureiotedge-diagnostics/ +cp -R $BUILD_REPOSITORY_LOCALPATH/edgelet/iotedge-diagnostics/docker $PUBLISH_FOLDER/azureiotedge-diagnostics/docker + +cd "$BUILD_REPOSITORY_LOCALPATH/edgelet" + +cross build -p iotedge-diagnostics --release --target x86_64-unknown-linux-musl +strip $BUILD_REPOSITORY_LOCALPATH/edgelet/target/x86_64-unknown-linux-musl/release/iotedge-diagnostics +cp $BUILD_REPOSITORY_LOCALPATH/edgelet/target/x86_64-unknown-linux-musl/release/iotedge-diagnostics $PUBLISH_FOLDER/azureiotedge-diagnostics/docker/linux/amd64/ + +cross build -p iotedge-diagnostics --release --target armv7-unknown-linux-musleabihf +arm-linux-gnueabihf-strip $BUILD_REPOSITORY_LOCALPATH/edgelet/target/armv7-unknown-linux-musleabihf/release/iotedge-diagnostics +cp $BUILD_REPOSITORY_LOCALPATH/edgelet/target/armv7-unknown-linux-musleabihf/release/iotedge-diagnostics $PUBLISH_FOLDER/azureiotedge-diagnostics/docker/linux/arm32v7/ + +aarch64-linux-gnu-strip $BUILD_REPOSITORY_LOCALPATH/edgelet/target/aarch64-unknown-linux-musl/release/iotedge-diagnostics \ No newline at end of file