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