├── .devcontainer ├── Dockerfile ├── devcontainer.json └── library-scripts │ └── docker-in-docker-debian.sh ├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── ThirdPartyNotice.html ├── iotedgehubdev.spec ├── iotedgehubdev ├── __init__.py ├── certutils.py ├── cli.py ├── compose_parser.py ├── composeproject.py ├── configs.py ├── constants.py ├── decorators.py ├── edgecert.py ├── edgedockerclient.py ├── edgemanager.py ├── errors.py ├── hostplatform.py ├── output.py ├── telemetry.py ├── telemetry_upload.py └── utils.py ├── main.py ├── pyinstaller └── hook-iotedgehubdev.cli.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── assets │ ├── certs │ │ ├── iotedgehubdev-test-only.root.ca.cert.pem │ │ └── iotedgehubdev-test-only.root.ca.key.pem │ └── config │ │ └── deployment.json ├── test_certutils.py ├── test_cli.py ├── test_compose.py ├── test_compose_resources │ ├── deployment.json │ ├── deployment_with_chunked_create_options.json │ ├── deployment_with_create_options.json │ ├── deployment_with_create_options_for_bind.json │ ├── deployment_with_custom_volume.json │ ├── deployment_without_custom_module.json │ ├── docker-compose.yml │ └── docker-compose_with_chunked_create_options.yml ├── test_config.py ├── test_connectionstr.py ├── test_edgecert.py ├── test_edgedockerclient.py ├── test_edgedockerclient_int.py ├── test_edgemanager.py └── test_utils.py ├── tox.ini └── vsts_ci ├── azure-pipelines.yml ├── darwin └── continuous-build-darwin.yml ├── linux └── continuous-build-linux.yml ├── policy └── continuous-legal-status-policy-check.yml ├── release.ps1 ├── standalone-binaries └── continuous-build-standalone-binaries-win32.yml └── win32 └── continuous-build-win32.yml /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Python version 2 | ARG PYTHON_VERSION="3.9" 3 | 4 | FROM mcr.microsoft.com/vscode/devcontainers/python:0-${PYTHON_VERSION} 5 | 6 | # Other Dependencies versions 7 | ARG AZURE_CLI_VERSION="2.34.1-1~bullseye" 8 | 9 | # Bring in utility scripts 10 | COPY .devcontainer/library-scripts/*.sh /tmp/library-scripts/ 11 | 12 | RUN \ 13 | # Install some basics 14 | apt-get update && \ 15 | apt-get install -y --no-install-recommends build-essential apt-utils && \ 16 | apt-get install -y libffi-dev libssl-dev vim sudo && \ 17 | apt-get upgrade -y 18 | 19 | RUN \ 20 | # Upgrade to latest pip 21 | python -m pip install --upgrade pip && \ 22 | # Install the Azure CLI and IoT extension 23 | apt-get install -y ca-certificates curl apt-transport-https lsb-release gnupg && \ 24 | curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null && \ 25 | echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/azure-cli.list && \ 26 | apt-get update && apt-get install -y azure-cli=${AZURE_CLI_VERSION} && \ 27 | az extension add --name azure-iot --system && \ 28 | az extension update --name azure-iot && \ 29 | # Use Docker script from script library to set things up - enable non-root docker, user vscode, using moby 30 | /bin/bash /tmp/library-scripts/docker-in-docker-debian.sh "true" "vscode" "true" \ 31 | # Install bash completion 32 | apt-get update && \ 33 | apt-get -y install bash-completion && \ 34 | # Clean up 35 | apt-get autoremove -y && \ 36 | apt-get clean -y && \ 37 | rm -rf /tmp/* && \ 38 | rm -rf /var/lib/apt/lists/* 39 | 40 | # customize vscode environmnet 41 | USER vscode 42 | RUN \ 43 | git clone https://github.com/magicmonty/bash-git-prompt.git $HOME/.bash-git-prompt --depth=1 && \ 44 | echo "\n# setup GIT prompt and completion\nif [ -f \"$HOME/.bash-git-prompt/gitprompt.sh\" ]; then\n GIT_PROMPT_ONLY_IN_REPO=1 && source $HOME/.bash-git-prompt/gitprompt.sh;\nfi" >> $HOME/.bashrc && \ 45 | echo "source /usr/share/bash-completion/bash_completion" >> $HOME/.bashrc && \ 46 | echo "\n# add local python programs to PATH\nexport PATH=$PATH:$HOME/.local/bin\n" >> $HOME/.bashrc && \ 47 | # enable some useful aliases 48 | sed -i -e "s/#alias/alias/" $HOME/.bashrc && \ 49 | pip install cookiecutter 50 | 51 | # launch docker-ce 52 | ENTRYPOINT [ "/usr/local/share/docker-init.sh" ] 53 | CMD [ "sleep", "infinity" ] 54 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.140.1/containers/dotnetcore 3 | { 4 | "name": "iotedgehubdev", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "context": ".." 8 | }, 9 | 10 | "runArgs": ["--init", "--privileged"], 11 | "mounts": [ 12 | // Keep command history 13 | "source=ostf-bashhistory,target=/commandhistory,type=volume", 14 | // Use docker-in-docker socket 15 | "source=dind-var-lib-docker,target=/var/lib/docker,type=volume" 16 | ], 17 | 18 | "overrideCommand": false, 19 | "postCreateCommand": "docker image prune -a -f && sudo chown vscode:users -R /home/vscode/HubDev && pip install -r requirements.txt && pip install -e .", 20 | 21 | // Set *default* container specific settings.json values on container create. 22 | "settings": { 23 | "#terminal.integrated.defaultProfile.linux#": "/bin/bash" 24 | }, 25 | 26 | // Add the IDs of extensions you want installed when the container is created. 27 | "extensions": [ 28 | "ms-python.python", 29 | "ms-azuretools.vscode-docker", 30 | "redhat.vscode-yaml", 31 | "mikestead.dotenv", 32 | "streetsidesoftware.code-spell-checker", 33 | "yzhang.markdown-all-in-one", 34 | "davidanson.vscode-markdownlint" 35 | ], 36 | 37 | "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/HubDev,type=bind,consistency=cached", 38 | "workspaceFolder": "/home/vscode/HubDev", 39 | "remoteUser": "vscode" 40 | } 41 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/docker-in-docker-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker-in-docker.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./docker-in-docker-debian.sh [enable non-root docker access flag] [non-root user] [use moby] 11 | 12 | ENABLE_NONROOT_DOCKER=${1:-"true"} 13 | USERNAME=${2:-"automatic"} 14 | USE_MOBY=${3:-"true"} 15 | MICROSOFT_GPG_KEYS_URI="https://packages.microsoft.com/keys/microsoft.asc" 16 | 17 | set -e 18 | 19 | if [ "$(id -u)" -ne 0 ]; then 20 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 21 | exit 1 22 | fi 23 | 24 | # Determine the appropriate non-root user 25 | if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then 26 | USERNAME="" 27 | POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") 28 | for CURRENT_USER in ${POSSIBLE_USERS[@]}; do 29 | if id -u ${CURRENT_USER} > /dev/null 2>&1; then 30 | USERNAME=${CURRENT_USER} 31 | break 32 | fi 33 | done 34 | if [ "${USERNAME}" = "" ]; then 35 | USERNAME=root 36 | fi 37 | elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then 38 | USERNAME=root 39 | fi 40 | 41 | # Get central common setting 42 | get_common_setting() { 43 | if [ "${common_settings_file_loaded}" != "true" ]; then 44 | curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." 45 | common_settings_file_loaded=true 46 | fi 47 | if [ -f "/tmp/vsdc-settings.env" ]; then 48 | local multi_line="" 49 | if [ "$2" = "true" ]; then multi_line="-z"; fi 50 | local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" 51 | if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi 52 | fi 53 | echo "$1=${!1}" 54 | } 55 | 56 | # Function to run apt-get if needed 57 | apt_get_update_if_needed() 58 | { 59 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 60 | echo "Running apt-get update..." 61 | apt-get update 62 | else 63 | echo "Skipping apt-get update." 64 | fi 65 | } 66 | 67 | # Checks if packages are installed and installs them if not 68 | check_packages() { 69 | if ! dpkg -s "$@" > /dev/null 2>&1; then 70 | apt_get_update_if_needed 71 | apt-get -y install --no-install-recommends "$@" 72 | fi 73 | } 74 | 75 | # Ensure apt is in non-interactive to avoid prompts 76 | export DEBIAN_FRONTEND=noninteractive 77 | 78 | # Install dependencies 79 | check_packages apt-transport-https curl ca-certificates lxc pigz iptables gnupg2 dirmngr 80 | 81 | # Swap to legacy iptables for compatibility 82 | if type iptables-legacy > /dev/null 2>&1; then 83 | update-alternatives --set iptables /usr/sbin/iptables-legacy 84 | update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy 85 | fi 86 | 87 | # Install Docker / Moby CLI if not already installed 88 | architecture="$(dpkg --print-architecture)" 89 | if type docker > /dev/null 2>&1 && type dockerd > /dev/null 2>&1; then 90 | echo "Docker / Moby CLI and Engine already installed." 91 | else 92 | # Source /etc/os-release to get OS info 93 | . /etc/os-release 94 | if [ "${USE_MOBY}" = "true" ]; then 95 | # Import key safely (new 'signed-by' method rather than deprecated apt-key approach) and install 96 | get_common_setting MICROSOFT_GPG_KEYS_URI 97 | curl -sSL ${MICROSOFT_GPG_KEYS_URI} | gpg --dearmor > /usr/share/keyrings/microsoft-archive-keyring.gpg 98 | echo "deb [arch=${architecture} signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/repos/microsoft-${ID}-${VERSION_CODENAME}-prod ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/microsoft.list 99 | apt-get update 100 | apt-get -y install --no-install-recommends moby-cli moby-buildx moby-engine 101 | apt-get -y install --no-install-recommends moby-compose || echo "(*) Package moby-compose (Docker Compose v2) not available for ${VERSION_CODENAME} ${architecture}. Skipping." 102 | else 103 | # Import key safely (new 'signed-by' method rather than deprecated apt-key approach) and install 104 | curl -fsSL https://download.docker.com/linux/${ID}/gpg | gpg --dearmor > /usr/share/keyrings/docker-archive-keyring.gpg 105 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list 106 | apt-get update 107 | apt-get -y install --no-install-recommends docker-ce-cli docker-ce 108 | fi 109 | fi 110 | 111 | echo "Finished installing docker / moby" 112 | 113 | # Install Docker Compose if not already installed and is on a supported architecture 114 | if type docker-compose > /dev/null 2>&1; then 115 | echo "Docker Compose already installed." 116 | else 117 | target_compose_arch="${architecture}" 118 | if [ "${target_compose_arch}" != "x86_64" ]; then 119 | # Use pip to get a version that runns on this architecture 120 | if ! dpkg -s python3-minimal python3-pip libffi-dev python3-venv > /dev/null 2>&1; then 121 | apt_get_update_if_needed 122 | apt-get -y install python3-minimal python3-pip libffi-dev python3-venv 123 | fi 124 | export PIPX_HOME=/usr/local/pipx 125 | mkdir -p ${PIPX_HOME} 126 | export PIPX_BIN_DIR=/usr/local/bin 127 | export PYTHONUSERBASE=/tmp/pip-tmp 128 | export PIP_CACHE_DIR=/tmp/pip-tmp/cache 129 | pipx_bin=pipx 130 | if ! type pipx > /dev/null 2>&1; then 131 | pip3 install --disable-pip-version-check --no-warn-script-location --no-cache-dir --user pipx 132 | pipx_bin=/tmp/pip-tmp/bin/pipx 133 | fi 134 | ${pipx_bin} install --system-site-packages --pip-args '--no-cache-dir --force-reinstall' docker-compose 135 | rm -rf /tmp/pip-tmp 136 | else 137 | latest_compose_version=$(basename "$(curl -fsSL -o /dev/null -w "%{url_effective}" https://github.com/docker/compose/releases/latest)") 138 | curl -fsSL "https://github.com/docker/compose/releases/download/${latest_compose_version}/docker-compose-$(uname -s)-${target_compose_arch}" -o /usr/local/bin/docker-compose 139 | chmod +x /usr/local/bin/docker-compose 140 | fi 141 | fi 142 | 143 | # If init file already exists, exit 144 | if [ -f "/usr/local/share/docker-init.sh" ]; then 145 | echo "/usr/local/share/docker-init.sh already exists, so exiting." 146 | exit 0 147 | fi 148 | echo "docker-init doesnt exist..." 149 | 150 | # Add user to the docker group 151 | if [ "${ENABLE_NONROOT_DOCKER}" = "true" ]; then 152 | if ! getent group docker > /dev/null 2>&1; then 153 | groupadd docker 154 | fi 155 | 156 | usermod -aG docker ${USERNAME} 157 | fi 158 | 159 | tee /usr/local/share/docker-init.sh > /dev/null \ 160 | << 'EOF' 161 | #!/usr/bin/env bash 162 | #------------------------------------------------------------------------------------------------------------- 163 | # Copyright (c) Microsoft Corporation. All rights reserved. 164 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 165 | #------------------------------------------------------------------------------------------------------------- 166 | 167 | sudoIf() 168 | { 169 | if [ "$(id -u)" -ne 0 ]; then 170 | sudo "$@" 171 | else 172 | "$@" 173 | fi 174 | } 175 | 176 | # explicitly remove dockerd and containerd PID file to ensure that it can start properly if it was stopped uncleanly 177 | # ie: docker kill 178 | sudoIf find /run /var/run -iname 'docker*.pid' -delete || : 179 | sudoIf find /run /var/run -iname 'container*.pid' -delete || : 180 | 181 | set -e 182 | 183 | ## Dind wrapper script from docker team 184 | # Maintained: https://github.com/moby/moby/blob/master/hack/dind 185 | 186 | export container=docker 187 | 188 | if [ -d /sys/kernel/security ] && ! sudoIf mountpoint -q /sys/kernel/security; then 189 | sudoIf mount -t securityfs none /sys/kernel/security || { 190 | echo >&2 'Could not mount /sys/kernel/security.' 191 | echo >&2 'AppArmor detection and --privileged mode might break.' 192 | } 193 | fi 194 | 195 | # Mount /tmp (conditionally) 196 | if ! sudoIf mountpoint -q /tmp; then 197 | sudoIf mount -t tmpfs none /tmp 198 | fi 199 | 200 | # cgroup v2: enable nesting 201 | if [ -f /sys/fs/cgroup/cgroup.controllers ]; then 202 | # move the init process (PID 1) from the root group to the /init group, 203 | # otherwise writing subtree_control fails with EBUSY. 204 | sudoIf mkdir -p /sys/fs/cgroup/init 205 | sudoIf echo 1 > /sys/fs/cgroup/init/cgroup.procs 206 | # enable controllers 207 | sudoIf sed -e 's/ / +/g' -e 's/^/+/' < /sys/fs/cgroup/cgroup.controllers \ 208 | > /sys/fs/cgroup/cgroup.subtree_control 209 | fi 210 | ## Dind wrapper over. 211 | 212 | # Handle DNS 213 | set +e 214 | cat /etc/resolv.conf | grep -i 'internal.cloudapp.net' 215 | if [ $? -eq 0 ] 216 | then 217 | echo "Setting dockerd Azure DNS." 218 | CUSTOMDNS="--dns 168.63.129.16" 219 | else 220 | echo "Not setting dockerd DNS manually." 221 | CUSTOMDNS="" 222 | fi 223 | set -e 224 | 225 | # Start docker/moby engine 226 | ( sudoIf dockerd $CUSTOMDNS > /tmp/dockerd.log 2>&1 ) & 227 | 228 | set +e 229 | 230 | # Execute whatever commands were passed in (if any). This allows us 231 | # to set this script to ENTRYPOINT while still executing the default CMD. 232 | exec "$@" 233 | EOF 234 | 235 | chmod +x /usr/local/share/docker-init.sh 236 | chown ${USERNAME}:root /usr/local/share/docker-init.sh 237 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # setuptools has trouble hanlding crlf if we load README.md content as long description 2 | README.md text eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | cov.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # testoutput 107 | %%PROGRAMDATA%%/ 108 | tests/output/ 109 | 110 | .vscode/tags 111 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: debug test", 9 | "type": "python", 10 | "request": "launch", 11 | "module": "pytest", 12 | "args": [ 13 | "-v", 14 | "${workspaceFolder}/tests/test_cli.py::test_cli_start_with_custom_edgehub_image_version" 15 | ], 16 | "cwd": "${workspaceFolder}", 17 | "env": { 18 | "IOTHUB_CONNECTION_STRING": "", 19 | "WINDOWS_DEVICE_CONNECTION_STRING": "", 20 | "CONTAINER_REGISTRY_SERVER": "", 21 | "CONTAINER_REGISTRY_USERNAME": "", 22 | "CONTAINER_REGISTRY_PASSWORD": "", 23 | "TEST_CA_KEY_PASSPHASE": "" 24 | } 25 | }, 26 | { 27 | "name": "Python Module", 28 | "type": "python", 29 | "request": "launch", 30 | "stopOnEntry": true, 31 | "module": "iotedgehubdev.cli", 32 | "args": [ 33 | // "singlemoduletest" 34 | "start", 35 | "-er", 36 | "1.1" 37 | ], 38 | "debugOptions": [ 39 | "RedirectOutput" 40 | ], 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.unittestEnabled": true, 3 | "python.testing.pytestEnabled": false, 4 | "python.testing.unittestArgs": [ 5 | "-v", 6 | "-s", 7 | "./tests", 8 | "-p", 9 | "test_*.py" 10 | ], 11 | "python.testing.nosetestsEnabled": false, 12 | "python.linting.pylintEnabled": false, 13 | "python.linting.flake8Enabled": true, 14 | "python.linting.enabled": true, 15 | } 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | [![PyPI version](https://badge.fury.io/py/iotedgehubdev.svg)](https://badge.fury.io/py/iotedgehubdev) 4 | ## 0.14.15 -2022-10-06 5 | * Updated pyOpenssl 6 | 7 | ## 0.14.14 - 2022-03-18 8 | * Drop sudo requirement 9 | 10 | ## 0.14.13 - 2022-03-16 11 | * Update regex match module 12 | 13 | ## 0.14.12 - 2022-03-01 14 | * Update Python support to 3.9 15 | 16 | ## 0.14.11 - 2022-02-10 17 | * Update Device Twin API version to include support for Arrays in desired properties 18 | 19 | ## 0.14.10 - 2021-10-20 20 | * Update Python Docker package version to latest 21 | 22 | ## 0.14.9 - 2021-08-05 23 | * Use 1.2 as default for edgeRuntime in single module mode. 24 | 25 | ## 0.14.8 - 2021-04-22 26 | * Drop PY 2 suport. 27 | * Update status from Beta to Production. 28 | 29 | ## 0.14.7 - 2021-04-02 30 | ### Changed 31 | * Use 1.1 as default for edgeRuntime in single module mode. 32 | 33 | ## 0.14.6 - 2021-03-26 34 | ### Changed 35 | * Rename edgeHub version command line parameter to '--edge-runtime-version/-er' 36 | * Limit support for edge runtime 1.0x and 1.1x 37 | 38 | ## 0.14.5 - 2021-03-19 39 | ### Changed 40 | * Dependency upgrades to address vulnerability issues 41 | * Add vulnerability scanner Bandit 42 | * Revert default edgeHub to v1.0 43 | 44 | ## 0.14.4 - 2021-03-16 45 | ### Changed 46 | * Default to v1.1 of EdgeHub image 47 | 48 | ## 0.14.2 - 2020-09-29 49 | ### Added 50 | * Support for running on specific version of EdgeHub image 51 | 52 | ## 0.14.1 - 2020-05-29 53 | ### Fixed 54 | * Fix false alarm issue of Windows Defender 55 | 56 | ## 0.14.0 - 2020-05-25 57 | ### Added 58 | * Support new route schema 59 | 60 | ## 0.13.0 - 2019-12-13 61 | ### Added 62 | * Add command to generate cert 63 | 64 | ## 0.12.0 - 2019-12-05 65 | ### Changed 66 | * Support Python 3.8 67 | * Make python version consistent between pip and standalone 68 | * Update cert generation logic 69 | 70 | ## 0.11.1 - 2019-10-25 71 | ### Fixed 72 | * Fix telemetry issue 73 | 74 | ## 0.11.0 - 2019-10-09 75 | ### Added 76 | * Support host network for modules 77 | * Standalone binaries of iotedgehubdev for Windows is available 78 | 79 | ## 0.10.0 - 2019-08-02 80 | ### Added 81 | * Support environment variables for single module ([#193](https://github.com/Azure/iotedgehubdev/issues/193)) 82 | * Add validateconfig command to check whether configuration is valid 83 | 84 | ### Changed 85 | * Use error code 2 for invalid configuration 86 | 87 | ## 0.9.0 - 2019-06-14 88 | ### Changed 89 | * Use range version for dependences to avoid incompatible issue 90 | 91 | ## 0.8.0 - 2019-04-01 92 | ### Added 93 | * Add module twin support 94 | 95 | ### Changed 96 | * Upgrade docker-py dependency to support connect remote Docker engine with ssh:// 97 | * Output errors to stderr 98 | 99 | ## 0.7.0 - 2019-01-29 100 | ### Added 101 | * Allow specifying Docker daemon socket to connect to with the `--host/-H` option 102 | * docker-compose as a pip dependency 103 | * Partially support `on-unhealthy` restart policy by falling back to `always` 104 | * Provide more friendly information when starting without setting up 105 | 106 | ### Changed 107 | * Update testing utility image version to 1.0.0 108 | 109 | ## 0.6.0 - 2018-12-05 110 | ### Added 111 | * Support parsing `Binds` in `createOptions` 112 | * Log in registries with credentials in deployment manifest 113 | 114 | ### Changed 115 | * Fix authentication error when hostname is longer than 64 116 | 117 | ## 0.5.0 - 2018-10-31 118 | ### Added 119 | * Support extended `createOptions` 120 | 121 | ### Changed 122 | * Update REST API version 123 | * Fix "Error getting device scope result from IoTHub, HttpStatusCode: Unauthorized" issue after starting ([#95](https://github.com/Azure/iotedgehubdev/issues/95)) 124 | 125 | ## 0.4.0 - 2018-10-12 126 | ### Changed 127 | * Fix "Error: 'environment'" when starting ([#87](https://github.com/Azure/iotedgehubdev/issues/87)) 128 | 129 | ## 0.3.0 - 2018-09-21 130 | ### Added 131 | * Support environment variables set in the `env` section of a module 132 | * Support getting credentials of multiple modules 133 | 134 | ### Changed 135 | * Always pull the EdgeHub image before starting 136 | * Fix "the JSON object must be str, not 'bytes'" when starting on Python 3.5 137 | 138 | ## 0.2.0 - 2018-08-03 139 | ### Added 140 | * Support networks and volumes in `createOptions` 141 | * Support Windows container 142 | 143 | ### Changed 144 | * Remove requirement of `sudo` for `iotedgehubdev start` and `iotedgehubdev modulecred` command 145 | * Rename EdgeHub runtime to IoT Edge simulator 146 | * Fix a issue which causes duplicate D2C messages 147 | 148 | ### Known Issues 149 | * [#67](https://github.com/Azure/iotedgehubdev/issues/67) Running Python and C modules which relies on SDK's support is not ready yet 150 | * [#62](https://github.com/Azure/iotedgehubdev/issues/62) Debugging C# modules may fail with Windows container due to incorrect timestamp 151 | * [#30](https://github.com/Azure/iotedgehubdev/issues/30) Debugging C# modules locally on macOS requires manually adding edge-device-ca.cert.pem to Keychain, and removing the EdgeModuleCACertificateFile environment variable 152 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | IoT Edge Hub Dev Tool 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | MIT License 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Azure IoT EdgeHub Dev Tool [![Build Status](https://dev.azure.com/mseng/VSIoT/_apis/build/status/Azure%20IoT%20Edge/iotedgehubdev?branchName=main)](https://dev.azure.com/mseng/VSIoT/_build/latest?definitionId=7735&branchName=main) 3 | 4 | ## Announcement 5 | The Azure IoT EdgeHub Dev Tool is in a maintenance mode. Please see [this announcement](https://github.com/Azure/iotedgehubdev/issues/396) for more details. We recommend using VM, Physical devices or [EFLOW](https://github.com/Azure/iotedge-eflow). 6 | 7 | ## Introduction 8 | The Azure IoT EdgeHub Dev Tool provides a local development experience with a simulator for creating, developing, testing, running, and debugging Azure IoT Edge modules and solutions. 9 | 10 | The simulator allows you to run, test and debug your own custom IoT Edge modules locally, without the IoT Edge Runtime, and with the following benefits: 11 | - Your custom Edge module code is the **same** whether running on the simulator or the full IoT Edge Runtime. 12 | - Your Edge solution can be run locally **without the need** to push new images or create IoT Edge deployment manifests. 13 | - The only credential required to run your Edge solution on the simulator is the **IoT Edge Device Connection String**. The IoT Hub Connection String is not needed. 14 | - It helps you debug your custom Edge modules on the host and not just in the container. 15 | 16 | The following table compares the requirements to run your solution on the IoT Edge Runtime and iotedgehubdev tool: 17 | 18 | | | IoT Edge Runtime | iotedgehubdev | 19 | | ----------------------------- |:----------------:|:-------------:| 20 | | Device Credential Needed | YES | YES | 21 | | IoT Hub Credential Needed | YES | **NO** | 22 | | Build Image | YES | YES | 23 | | Push Image | YES | **NO** | 24 | | Create Deployment | YES | **NO** | 25 | | Support native debug scenario | NO | **YES** | 26 | 27 | ## Installing 28 | 1. Install [Docker CE (18.02.0+)](https://www.docker.com/community-edition) on 29 | [Windows](https://docs.docker.com/docker-for-windows/install/), [macOS](https://docs.docker.com/docker-for-mac/install/) or [Linux](https://docs.docker.com/install/linux/docker-ce/ubuntu/#install-docker-ce) 30 | 31 | 2. Install [Docker Compose (1.20.0+)](https://docs.docker.com/compose/install/#install-compose) (***Linux only***. *Compose has already been included in Windows/macOS Docker CE installation*) 32 | 3. Install [Python (3.5/3.6/3.7/3.8/3.9) and Pip](https://www.python.org/) 33 | 4. Install **iotedgehubdev** by running the following command in your terminal: 34 | ``` 35 | pip install --upgrade iotedgehubdev 36 | ``` 37 | 5. Ensure the user is a member of **docker** user group (**Linux / MacOS only**): 38 | ``` 39 | sudo usermod -aG docker $USER 40 | ``` 41 | 42 | **Please make sure there is no Azure IoT Edge runtime running on the same machine as iotedgehubdev since they require the same ports.** 43 | 44 | ## Quickstart 45 | ### 1. Setup 46 | 47 | ``` 48 | iotedgehubdev setup -c "" 49 | ``` 50 | 51 | ### 2. Start/Stop an IoT Edge solution in simulator 52 | 53 | ``` 54 | iotedgehubdev start -d 55 | iotedgehubdev stop 56 | ``` 57 | 58 | ### 3. Start and debug a single module natively 59 | 60 | 1. Start the module with specific input(s) and/or environment variable(s) 61 | 62 | ``` 63 | iotedgehubdev start -i "" 64 | 65 | // OR 66 | 67 | iotedgehubdev start -i "" -e "" 68 | ``` 69 | 70 | **For example**: 71 | `iotedgehubdev start -i "input1,input2" -e "TestEnv1=Value1" -e "TestEnv2=Value2"` 72 | 73 | 2. Output the module credential environment variables 74 | 75 | ``` 76 | iotedgehubdev modulecred 77 | ``` 78 | 79 | 3. Start your module natively with the environment variables from the previous step 80 | 81 | 4. Send a message to your module through the RESTful API 82 | 83 | **For example**: 84 | `curl --header "Content-Type: application/json" --request POST --data '{"inputName": "input1","data": "hello world"}' http://localhost:53000/api/v1/messages` 85 | 86 | 5. Stop the simulator 87 | 88 | ``` 89 | iotedgehubdev stop 90 | ``` 91 | 92 | ## Other resources 93 | - [Azure IoT Edge for Visual Studio Code](https://github.com/microsoft/vscode-azure-iot-edge) 94 | - [Azure IoT Edge Dev CLI Tool](https://github.com/azure/iotedgedev) 95 | 96 | # Data/Telemetry 97 | This project collects usage data and sends it to Microsoft to help improve our products and services. Read our [privacy statement](http://go.microsoft.com/fwlink/?LinkId=521839) to learn more. 98 | If you don’t wish to send usage data to Microsoft, you can change your telemetry settings by updating `collect_telemetry` to `no` in the ini file. 99 | 100 | # Contributing 101 | 102 | This project welcomes contributions and suggestions. Most contributions require you to 103 | agree to a Contributor License Agreement (CLA) declaring that you have the right to, 104 | and actually do, grant us the rights to use your contribution. For details, visit 105 | https://cla.microsoft.com. 106 | 107 | When you submit a pull request, a CLA-bot will automatically determine whether you need 108 | to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the 109 | instructions provided by the bot. You will only need to do this once across all repositories using our CLA. 110 | 111 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 112 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 113 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 114 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /iotedgehubdev.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | import PyInstaller.config 4 | PyInstaller.config.CONF['distpath'] = './standalone-binaries' 5 | 6 | block_cipher = None 7 | 8 | 9 | a = Analysis(['main.py'], 10 | pathex=['.'], 11 | binaries=[], 12 | datas=[('.\\ThirdPartyNotice.html', '.')], 13 | hiddenimports=[], 14 | hookspath=['.\\pyinstaller'], 15 | runtime_hooks=[], 16 | excludes=[], 17 | win_no_prefer_redirects=False, 18 | win_private_assemblies=False, 19 | cipher=block_cipher, 20 | noarchive=False) 21 | pyz = PYZ(a.pure, a.zipped_data, 22 | cipher=block_cipher) 23 | exe = EXE(pyz, 24 | a.scripts, 25 | [], 26 | exclude_binaries=True, 27 | name='iotedgehubdev', 28 | debug=False, 29 | bootloader_ignore_signals=False, 30 | strip=False, 31 | upx=False, 32 | console=True ) 33 | coll = COLLECT(exe, 34 | a.binaries, 35 | a.zipfiles, 36 | a.datas, 37 | strip=False, 38 | upx=True, 39 | upx_exclude=[], 40 | name='iotedgehubdev') 41 | -------------------------------------------------------------------------------- /iotedgehubdev/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | import pkg_resources 6 | 7 | pkg_resources.declare_namespace(__name__) 8 | 9 | __author__ = 'Microsoft Corporation' 10 | __version__ = '0.14.18' 11 | __AIkey__ = '95b20d64-f54f-4de3-8ad5-165a75a6c6fe' 12 | __production__ = 'iotedgehubdev' 13 | -------------------------------------------------------------------------------- /iotedgehubdev/cli.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | import json 6 | import os 7 | import sys 8 | import re 9 | from functools import wraps 10 | 11 | import click 12 | 13 | from . import configs, decorators, telemetry 14 | from .constants import EdgeConstants 15 | from .edgecert import EdgeCert 16 | from .edgemanager import EdgeManager 17 | from .hostplatform import HostPlatform 18 | from .output import Output 19 | from .utils import Utils 20 | from .errors import EdgeError, InvalidConfigError 21 | 22 | CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'], max_content_width=120) 23 | output = Output() 24 | 25 | CONN_STR = 'connectionString' 26 | CERT_PATH = 'certPath' 27 | GATEWAY_HOST = 'gatewayhost' 28 | DOCKER_HOST = 'DOCKER_HOST' 29 | HUB_CONN_STR = 'iothubConnectionString' 30 | 31 | # a set of parameters whose value should be logged as given 32 | PARAMS_WITH_VALUES = {'edge_runtime_version'} 33 | 34 | @decorators.suppress_all_exceptions() 35 | def _parse_params(*args, **kwargs): 36 | params = [] 37 | for key, value in kwargs.items(): 38 | if (value is None) or (key in PARAMS_WITH_VALUES): 39 | params.append('{0}={1}'.format(key, value)) 40 | else: 41 | params.append('{0}!=None'.format(key)) 42 | return params 43 | 44 | 45 | def _send_failed_telemetry(e): 46 | output.error(str(e)) 47 | telemetry.fail('Command failed') 48 | telemetry.flush() 49 | 50 | 51 | def _with_telemetry(func): 52 | @wraps(func) 53 | def _wrapper(*args, **kwargs): 54 | configs.check_firsttime() 55 | params = _parse_params(*args, **kwargs) 56 | telemetry.start(func.__name__, params) 57 | try: 58 | value = func(*args, **kwargs) 59 | telemetry.success() 60 | telemetry.flush() 61 | return value 62 | except InvalidConfigError as e: 63 | _send_failed_telemetry(e) 64 | sys.exit(2) 65 | except Exception as e: 66 | _send_failed_telemetry(e) 67 | sys.exit(1) 68 | 69 | return _wrapper 70 | 71 | 72 | def _parse_config_json(): 73 | try: 74 | config_file = HostPlatform.get_config_file_path() 75 | 76 | if not Utils.check_if_file_exists(config_file): 77 | raise ValueError('Cannot find config file. Please run `{0}` first.'.format(_get_setup_command())) 78 | 79 | with open(config_file) as f: 80 | try: 81 | config_json = json.load(f) 82 | 83 | connection_str = config_json[CONN_STR] 84 | cert_path = config_json[CERT_PATH] 85 | gatewayhost = config_json[GATEWAY_HOST] 86 | hub_conn_str = config_json.get(HUB_CONN_STR) 87 | return EdgeManager(connection_str, gatewayhost, cert_path, hub_conn_str) 88 | 89 | except (ValueError, KeyError): 90 | raise ValueError('Invalid config file. Please run `{0}` again.'.format(_get_setup_command())) 91 | except Exception as e: 92 | raise InvalidConfigError(str(e)) 93 | 94 | 95 | def _get_setup_command(): 96 | return 'iotedgehubdev setup -c ""' 97 | 98 | 99 | @click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True) 100 | @click.version_option() 101 | def main(): 102 | ctx = click.get_current_context() 103 | if ctx.invoked_subcommand is None: 104 | click.echo(ctx.get_help()) 105 | sys.exit(0) 106 | 107 | 108 | @click.command(context_settings=CONTEXT_SETTINGS, 109 | help='Setup the IoT Edge Simulator. This must be done before starting.') 110 | @click.option('--connection-string', 111 | '-c', 112 | required=True, 113 | help='Set Azure IoT Edge device connection string. Note: Use double quotes when supplying this input.') 114 | @click.option('--gateway-host', 115 | '-g', 116 | required=False, 117 | default=Utils.get_hostname(), 118 | show_default=True, 119 | help='GatewayHostName value for the module to connect.') 120 | @click.option('--iothub-connection-string', 121 | '-i', 122 | required=False, 123 | help='Set Azure IoT Hub connection string. Note: Use double quotes when supplying this input.') 124 | @_with_telemetry 125 | def setup(connection_string, gateway_host, iothub_connection_string): 126 | try: 127 | gateway_host = gateway_host.lower() 128 | certDir = HostPlatform.get_default_cert_path() 129 | Utils.parse_connection_strs(connection_string, iothub_connection_string) 130 | if iothub_connection_string is None: 131 | configDict = { 132 | CONN_STR: connection_string, 133 | CERT_PATH: certDir, 134 | GATEWAY_HOST: gateway_host 135 | } 136 | else: 137 | configDict = { 138 | CONN_STR: connection_string, 139 | CERT_PATH: certDir, 140 | GATEWAY_HOST: gateway_host, 141 | HUB_CONN_STR: iothub_connection_string 142 | } 143 | 144 | fileType = 'edgehub.config' 145 | Utils.mkdir_if_needed(certDir) 146 | edgeCert = EdgeCert(certDir, gateway_host) 147 | edgeCert.generate_self_signed_certs() 148 | configFile = HostPlatform.get_config_file_path() 149 | Utils.delete_file(configFile, fileType) 150 | Utils.mkdir_if_needed(HostPlatform.get_config_path()) 151 | configJson = json.dumps(configDict, indent=2, sort_keys=True) 152 | Utils.create_file(configFile, configJson, fileType) 153 | 154 | dataDir = HostPlatform.get_share_data_path() 155 | Utils.mkdir_if_needed(dataDir) 156 | os.chmod(dataDir, 0o755) 157 | 158 | with open(EdgeManager.COMPOSE_FILE, 'w') as f: 159 | f.write('version: \'3.6\'') 160 | os.chmod(EdgeManager.COMPOSE_FILE, 0o777) 161 | output.info('Setup IoT Edge Simulator successfully.') 162 | except Exception as e: 163 | raise e 164 | 165 | 166 | @click.command(context_settings=CONTEXT_SETTINGS, 167 | # short_help hack to prevent Click truncating help text (https://github.com/pallets/click/issues/486) 168 | short_help='Get the module credentials such as connection string and certificate file path.', 169 | help='Get the module credentials such as connection string and certificate file path.') 170 | @click.option('--modules', 171 | '-m', 172 | required=False, 173 | default='target', 174 | show_default=True, 175 | help='Specify the vertical-bar-separated ("|") module names to get credentials for, e.g., "module1|module2". ' 176 | 'Note: Use double quotes when supplying this input.') 177 | @click.option('--local', 178 | '-l', 179 | required=False, 180 | is_flag=True, 181 | default=False, 182 | show_default=True, 183 | help='Set `localhost` to `GatewayHostName` for module to run on host natively.') 184 | @click.option('--output-file', 185 | '-o', 186 | required=False, 187 | show_default=True, 188 | help='Specify the output file to save the connection string. If the file exists, the content will be overwritten.') 189 | @_with_telemetry 190 | def modulecred(modules, local, output_file): 191 | edge_manager = _parse_config_json() 192 | 193 | if edge_manager: 194 | modules = [module.strip() for module in modules.strip().split('|')] 195 | credential = edge_manager.outputModuleCred(modules, local, output_file) 196 | output.info(credential[0]) 197 | output.info(credential[1]) 198 | 199 | 200 | @click.command(context_settings=CONTEXT_SETTINGS, 201 | help="Start the IoT Edge Simulator.") 202 | @click.option('--inputs', 203 | '-i', 204 | required=False, 205 | help='Start IoT Edge Simulator in single module mode ' 206 | 'using the specified comma-separated inputs of the target module, e.g., `input1,input2`.') 207 | @click.option('--port', 208 | '-p', 209 | required=False, 210 | default=53000, 211 | show_default=True, 212 | help='Port of the service for sending message.') 213 | @click.option('--deployment', 214 | '-d', 215 | required=False, 216 | help='Start IoT Edge Simulator in solution mode using the specified deployment manifest.') 217 | @click.option('--verbose', 218 | '-v', 219 | required=False, 220 | is_flag=True, 221 | default=False, 222 | show_default=True, 223 | help='Show the solution container logs.') 224 | @click.option('--host', 225 | '-H', 226 | required=False, 227 | help='Docker daemon socket to connect to.') 228 | @click.option('--environment', 229 | '-e', 230 | required=False, 231 | multiple=True, 232 | help='Environment variables for single module mode, e.g., `-e "Env1=Value1" -e "Env2=Value2"`.') 233 | @click.option('--edge-runtime-version', 234 | '-er', 235 | required=False, 236 | multiple=False, 237 | default='1.2', 238 | show_default=True, 239 | help='EdgeHub image version. Currently supported tags 1.0x, 1.1x, or 1.2x') 240 | @_with_telemetry 241 | def start(inputs, port, deployment, verbose, host, environment, edge_runtime_version): 242 | edge_manager = _parse_config_json() 243 | 244 | if edge_manager: 245 | if host is not None: 246 | os.environ[DOCKER_HOST] = str(host) 247 | 248 | hostname_hash, suffix = Utils.hash_connection_str_hostname(edge_manager.hostname) 249 | telemetry.add_extra_props({'iothubhostname': hostname_hash, 'iothubhostnamesuffix': suffix}) 250 | 251 | if inputs is None and deployment is not None: 252 | if len(environment) > 0: 253 | output.info('Environment variables are ignored in solution mode.') 254 | 255 | if len(edge_runtime_version) > 0: 256 | output.info('edgeHub image version is ignored in solution mode.') 257 | 258 | with open(deployment) as json_file: 259 | json_data = json.load(json_file) 260 | if 'modulesContent' in json_data: 261 | module_content = json_data['modulesContent'] 262 | elif 'moduleContent' in json_data: 263 | module_content = json_data['moduleContent'] 264 | edge_manager.start_solution(module_content, verbose, output) 265 | if not verbose: 266 | output.info('IoT Edge Simulator has been started in solution mode.') 267 | else: 268 | if edge_runtime_version is not None: 269 | # The only validated versions are 1.0, 1.1, and 1.2 variants, hence the current limitation 270 | if re.match(r'^(1\.0)|(1\.1)|(1\.2)', edge_runtime_version) is None: 271 | raise ValueError('-edge-runtime-version `{0}` is not valid.'.format(edge_runtime_version)) 272 | 273 | if deployment is not None: 274 | output.info('Deployment manifest is ignored when inputs are present.') 275 | if inputs is None: 276 | input_list = ['input1'] 277 | else: 278 | input_list = [input_.strip() for input_ in inputs.strip().split(',')] 279 | 280 | for env in environment: 281 | if re.match(r'^[a-zA-Z][a-zA-Z0-9_]*?=.*$', env) is None: 282 | raise ValueError('Environment variable: `{0}` is not valid.'.format(env)) 283 | 284 | edge_manager.start_singlemodule(input_list, port, environment, edge_runtime_version) 285 | 286 | data = '--data \'{{"inputName": "{0}","data":"hello world"}}\''.format(input_list[0]) 287 | url = 'http://localhost:{0}/api/v1/messages'.format(port) 288 | curl_msg = ' curl --header "Content-Type: application/json" --request POST {0} {1}'.format(data, url) 289 | output.info('IoT Edge Simulator has been started in single module mode.') 290 | output.info('Please run `iotedgehubdev modulecred` to get credential to connect your module.') 291 | output.info('And send message through:') 292 | output.line() 293 | output.echo(curl_msg, 'green') 294 | output.line() 295 | output.info( 296 | 'Please refer to https://github.com/Azure/iot-edge-testing-utility/blob/master/swagger.json' 297 | ' for detail schema') 298 | 299 | 300 | @click.command(context_settings=CONTEXT_SETTINGS, 301 | help="Stop the IoT Edge Simulator.") 302 | @click.option('--host', 303 | '-H', 304 | required=False, 305 | help='Docker daemon socket to connect to') 306 | @_with_telemetry 307 | def stop(host): 308 | if host is not None: 309 | os.environ[DOCKER_HOST] = str(host) 310 | EdgeManager.stop() 311 | output.info('IoT Edge Simulator has been stopped successfully.') 312 | 313 | 314 | @click.command(context_settings=CONTEXT_SETTINGS, 315 | help="Determine whether config file is valid.") 316 | @_with_telemetry 317 | def validateconfig(): 318 | _parse_config_json() 319 | output.info('Config file is valid.') 320 | 321 | @click.command(context_settings=CONTEXT_SETTINGS, 322 | help="Create IoT Edge device CA") 323 | @click.option('--output-dir', 324 | '-o', 325 | required=False, 326 | default=".", 327 | help='The output folder of generated certs. ' 328 | 'The tool will create a certs folder under given path to store the certs.') 329 | @click.option('--valid-days', 330 | '-d', 331 | required=False, 332 | default=90, 333 | show_default=True, 334 | help='Days before cert expires.') 335 | @click.option('--force', 336 | '-f', 337 | required=False, 338 | is_flag=True, 339 | default=False, 340 | show_default=True, 341 | help='Whether overwrite existing cert files.') 342 | @click.option('--trusted-ca', 343 | '-c', 344 | required=False, 345 | help='Path of your own trusted ca used to sign IoT Edge device ca. ' 346 | 'Please also provide trsuted ca private key and related passphase (if have).' 347 | ) 348 | @click.option('--trusted-ca-key', 349 | '-k', 350 | required=False, 351 | help='Path of your own trusted ca private key used to sign IoT Edge device ca. ' 352 | 'Please also provide trusted ca and related passphase (if have).') 353 | @click.option('--trusted-ca-key-passphase', 354 | '-p', 355 | required=False, 356 | help='Passphase of your own trusted ca private key.') 357 | @_with_telemetry 358 | def generatedeviceca(output_dir, valid_days, force, trusted_ca, trusted_ca_key, trusted_ca_key_passphase): 359 | try: 360 | output_dir = os.path.abspath(os.path.join(output_dir, EdgeConstants.CERT_FOLDER)) 361 | if trusted_ca_key_passphase: 362 | trusted_ca_key_passphase = trusted_ca_key_passphase.encode() # crypto requires byte string 363 | # Check whether create new trusted CA and generate files to be created 364 | output_files = list(Utils.get_device_ca_file_paths(output_dir, EdgeConstants.DEVICE_CA_ID).values()) 365 | if trusted_ca and trusted_ca_key: 366 | output.info('Trusted CA (certification authority) and trusted CA key were provided.' 367 | ' Load trusted CA from given files.') 368 | else: 369 | output.info('Trusted CA (certification authority) and Trusted CA key were not provided.' 370 | ' Will create new trusted CA.') 371 | root_ca_files = Utils.get_device_ca_file_paths(output_dir, EdgeConstants.ROOT_CA_ID) 372 | output_files.append(root_ca_files[EdgeConstants.CERT_SUFFIX]) 373 | output_files.append(root_ca_files[EdgeConstants.KEY_SUFFIX]) 374 | # Check whether the output files exist 375 | existing_files = [] 376 | for file in output_files: 377 | if os.path.exists(file): 378 | existing_files.append(file) 379 | if len(existing_files) > 0: 380 | if force: 381 | output.info('Following cert files already exist and will be overwritten: %s' % existing_files) 382 | else: 383 | raise EdgeError('Following cert files already exist. ' 384 | 'You can use --force option to overwrite existing files: %s' % existing_files) 385 | # Generate certs 386 | edgeCert = EdgeCert(output_dir, '') 387 | edgeCert.generate_device_ca(valid_days, force, trusted_ca, trusted_ca_key, trusted_ca_key_passphase) 388 | output.info('Successfully generated device CA. Please find the generated certs at %s' % output_dir) 389 | except Exception as e: 390 | raise e 391 | 392 | 393 | main.add_command(setup) 394 | main.add_command(modulecred) 395 | main.add_command(start) 396 | main.add_command(stop) 397 | main.add_command(validateconfig) 398 | main.add_command(generatedeviceca) 399 | 400 | if __name__ == "__main__": 401 | main() 402 | -------------------------------------------------------------------------------- /iotedgehubdev/compose_parser.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | import os 5 | import regex 6 | 7 | from jsonpath_rw import parse 8 | 9 | from .constants import EdgeConstants 10 | 11 | 12 | class CreateOptionParser(object): 13 | def __init__(self, create_option): 14 | self.create_option = create_option 15 | 16 | def parse_create_option(self): 17 | ret = {} 18 | for compose_key in COMPOSE_KEY_CREATE_OPTION_MAPPING: 19 | create_option_value = self.get_create_option_value(compose_key) 20 | if create_option_value: 21 | parser_func = COMPOSE_KEY_CREATE_OPTION_MAPPING[compose_key]['parser_func'] 22 | ret[compose_key] = parser_func(create_option_value) 23 | return ret 24 | 25 | def get_create_option_value(self, compose_key): 26 | create_option_value_dict = {} 27 | for API_key, API_jsonpath in COMPOSE_KEY_CREATE_OPTION_MAPPING[compose_key]['API_Info'].items(): 28 | jsonpath_expr = parse(API_jsonpath) 29 | value_list = jsonpath_expr.find(self.create_option) 30 | if value_list: 31 | create_option_value_dict[API_key] = value_list[0].value 32 | return create_option_value_dict 33 | 34 | 35 | def service_parser_naive(create_options_details): 36 | return list(create_options_details.values())[0] 37 | 38 | 39 | def service_parser_expose(create_options_details): 40 | return list(create_options_details['ExposedPorts'].keys()) 41 | 42 | 43 | def service_parser_command(create_options_details): 44 | cmd = create_options_details['Cmd'] 45 | if not isinstance(cmd, list): 46 | return cmd 47 | return ' '.join(cmd).strip() 48 | 49 | 50 | def service_parser_healthcheck(create_options_details): 51 | healthcheck_config = create_options_details['Healthcheck'] 52 | try: 53 | return { 54 | 'test': healthcheck_config['Test'], 55 | 'interval': time_ns_ms(healthcheck_config['Interval']), 56 | 'timeout': time_ns_ms(healthcheck_config['Timeout']), 57 | 'retries': healthcheck_config['Retries'], 58 | 'start_period': time_ns_ms(healthcheck_config['StartPeriod']) 59 | } 60 | except KeyError as err: 61 | raise KeyError('Missing key : {0} in Healthcheck'.format(err)) 62 | 63 | 64 | def service_parser_stop_timeout(create_options_details): 65 | try: 66 | return str(int(create_options_details['StopTimeout'])) + 's' 67 | except TypeError: 68 | raise TypeError('StopTimeout should be an integer.') 69 | 70 | 71 | def service_parser_hostconfig_devices(create_options_details): 72 | devices_list = [] 73 | for device in create_options_details['Devices']: 74 | try: 75 | devices_list.append("{0}:{1}:{2}".format(device['PathOnHost'], 76 | device['PathInContainer'], device['CgroupPermissions'])) 77 | except KeyError as err: 78 | raise KeyError('Missing key : {0} in HostConfig.Devices.'.format(err)) 79 | return devices_list 80 | 81 | 82 | def service_parser_hostconfig_restart(create_options_details): 83 | restart_config = create_options_details['RestartPolicy'] 84 | ret = "" 85 | if restart_config['Name'] == "": 86 | ret = "no" 87 | elif restart_config['Name'] == "on-failure": 88 | try: 89 | ret = "on-failure:{0}".format(restart_config['MaximumRetryCount']) 90 | except KeyError as err: 91 | raise KeyError('Missing key : {0} in HostConfig.RestartPolicy.'.format(err)) 92 | elif restart_config['Name'] == "always" or restart_config['Name'] == "unless-stopped": 93 | ret = restart_config['Name'] 94 | else: 95 | raise ValueError("RestartPolicy Name should be one of '', 'always', 'unless-stopped', 'on-failure'") 96 | return ret 97 | 98 | 99 | def service_parser_hostconfig_ulimits(create_options_details): 100 | ulimits_dict = {} 101 | for ulimit in create_options_details['Ulimits']: 102 | try: 103 | ulimits_dict[ulimit['Name']] = { 104 | 'soft': ulimit['Soft'], 105 | 'hard': ulimit['Hard'] 106 | } 107 | except KeyError as err: 108 | raise KeyError('Missing key : {0} in HostConfig.Ulimits'.format(err)) 109 | return ulimits_dict 110 | 111 | 112 | def service_parser_hostconfig_logging(create_options_details): 113 | try: 114 | logging_dict = { 115 | 'driver': create_options_details['LogConfig']['Type'], 116 | 'options': create_options_details['LogConfig']['Config'] 117 | } 118 | except KeyError as err: 119 | raise KeyError('Missing key : {0} in HostConfig.LogConfig'.format(err)) 120 | return logging_dict 121 | 122 | 123 | def service_parser_hostconfig_ports(create_options_details): 124 | ports_list = [] 125 | for container_port, host_ports in create_options_details['PortBindings'].items(): 126 | for host_port_info in host_ports: 127 | host_port = "" 128 | if 'HostIp' in host_port_info and 'HostPort' in host_port_info: 129 | host_port = "{0}:{1}".format(host_port_info['HostIp'], host_port_info['HostPort']) 130 | elif 'HostIp' in host_port_info: 131 | host_port = host_port_info['HostIp'] 132 | elif 'HostPort' in host_port_info: 133 | host_port = host_port_info['HostPort'] 134 | ports_list.append("{0}:{1}".format(host_port, container_port)) 135 | return ports_list 136 | 137 | 138 | def service_parser_networks(create_options_details): 139 | networks_dict = {} 140 | for nw, nw_config in create_options_details['NetworkingConfig'].items(): 141 | networks_dict[nw] = {} 142 | if 'Aliases' in nw_config: 143 | networks_dict[nw]['aliases'] = nw_config['Aliases'] 144 | if 'IPAMConfig' in nw_config: 145 | if 'IPv4Address' in nw_config['IPAMConfig']: 146 | networks_dict[nw]['ipv4_address'] = nw_config['IPAMConfig']['IPv4Address'] 147 | if 'IPv6Address' in nw_config['IPAMConfig']: 148 | networks_dict[nw]['ipv6_address'] = nw_config['IPAMConfig']['IPv6Address'] 149 | return networks_dict 150 | 151 | 152 | def service_parser_volumes(create_options_details): 153 | volumes_list = [] 154 | for mount in create_options_details.get('Mounts', []): 155 | try: 156 | volume_info = { 157 | 'target': mount['Target'], 158 | 'type': mount['Type'] 159 | } 160 | if mount['Type'] == 'volume' or mount['Type'] == 'bind': 161 | volume_info['source'] = mount['Source'] 162 | if 'ReadOnly' in mount: 163 | volume_info['read_only'] = mount['ReadOnly'] 164 | 165 | if mount['Type'] == 'volume' and 'VolumeOptions' in mount: 166 | if 'NoCopy' in mount['VolumeOptions']: 167 | volume_info['volume'] = { 168 | 'nocopy': mount['VolumeOptions']['NoCopy'] 169 | } 170 | if mount['Type'] == 'bind' and 'BindOptions' in mount: 171 | if 'Propagation' in mount['BindOptions']: 172 | volume_info['bind'] = { 173 | 'propagation': mount['BindOptions']['Propagation'] 174 | } 175 | if mount['Type'] == 'tmpfs' and 'TmpfsOptions' in mount: 176 | if 'SizeBytes' in mount['TmpfsOptions']: 177 | volume_info['tmpfs'] = { 178 | 'size': mount['TmpfsOptions']['SizeBytes'] 179 | } 180 | except KeyError as e: 181 | raise KeyError('Missing key {0} in create option HostConfig Mounts.'.format(e)) 182 | volumes_list.append(volume_info) 183 | 184 | for bind in create_options_details.get('Binds', []): 185 | target = None 186 | 187 | # Binds should be in the format [source:]destination[:mode] 188 | # Windows format and LCOW format are more strict than Linux format due to colons in Windows paths, 189 | # so match with them first 190 | match = regex.match(EdgeConstants.MOUNT_WIN_REGEX, bind) or regex.match(EdgeConstants.MOUNT_LCOW_REGEX, bind) 191 | if match is not None: 192 | source = match.group('source') or '' 193 | target = match.group('destination') 194 | read_only = match.group('mode') == 'ro' 195 | else: 196 | # Port of Docker daemon 197 | # https://github.com/docker/docker-ce/blob/1c27a55b6259743f35549e96d06334a53d0c0549/components/engine/volume/mounts/linux_parser.go#L18-L28 198 | parts = bind.split(':') 199 | if len(parts) == 2 or (len(parts) == 3 and parts[2] in ('ro', 'rw', '')): 200 | if parts[0] != '': 201 | source = parts[0] 202 | target = parts[1] 203 | read_only = len(parts) == 3 and parts[2] == 'ro' 204 | 205 | if target is not None: 206 | volume_info = { 207 | 'type': 'bind' if source and os.path.isabs(source) else 'volume', 208 | 'source': source, 209 | 'target': target 210 | } 211 | if read_only: 212 | volume_info['read_only'] = True 213 | volumes_list.append(volume_info) 214 | else: 215 | raise ValueError('Invalid create option Binds: {0}'.format(bind)) 216 | 217 | return volumes_list 218 | 219 | 220 | def time_ns_ms(ns): 221 | if ns != 0 and ns < 1000000: 222 | raise ValueError('The time should be 0 or at least 1000000 (1 ms)') 223 | return str(int(ns / 1000000)) + 'ms' 224 | 225 | 226 | ''' 227 | The mapping relationship between docker compose key and create option API key 228 | 'docker compose key': {'API_Info': {'API key':'API jsonpath'}, 'parser_func': parser_func}, 229 | ''' 230 | COMPOSE_KEY_CREATE_OPTION_MAPPING = { 231 | 'hostname': {'API_Info': {'Hostname': "$['Hostname']"}, 'parser_func': service_parser_naive}, 232 | 'domainname': {'API_Info': {'Domainname': "$['Domainname']"}, 'parser_func': service_parser_naive}, 233 | 'user': {'API_Info': {'User': "$['User']"}, 'parser_func': service_parser_naive}, 234 | 'expose': {'API_Info': {'ExposedPorts': "$['ExposedPorts']"}, 'parser_func': service_parser_expose}, 235 | 'tty': {'API_Info': {'Tty': "$['Tty']"}, 'parser_func': service_parser_naive}, 236 | 'environment': {'API_Info': {'Env': "$['Env']"}, 'parser_func': service_parser_naive}, 237 | 'command': {'API_Info': {'Cmd': "$['Cmd']"}, 'parser_func': service_parser_command}, 238 | 'healthcheck': {'API_Info': {'Healthcheck': "$['Healthcheck']"}, 'parser_func': service_parser_healthcheck}, 239 | 'image': {'API_Info': {'Image': "$['Image']"}, 'parser_func': service_parser_naive}, 240 | 'working_dir': {'API_Info': {'WorkingDir': "$['WorkingDir']"}, 'parser_func': service_parser_naive}, 241 | 'entrypoint': {'API_Info': {'Entrypoint': "$['Entrypoint']"}, 'parser_func': service_parser_naive}, 242 | 'mac_address': {'API_Info': {'MacAddress': "$['MacAddress']"}, 'parser_func': service_parser_naive}, 243 | 'labels': {'API_Info': {'Labels': "$['Labels']"}, 'parser_func': service_parser_naive}, 244 | 'stop_signal': {'API_Info': {'StopSignal': "$['StopSignal']"}, 'parser_func': service_parser_naive}, 245 | 'stop_grace_period': {'API_Info': {'StopTimeout': "$['StopTimeout']"}, 'parser_func': service_parser_stop_timeout}, 246 | 247 | # HostConfig 248 | 'ports': {'API_Info': {'PortBindings': "$['HostConfig']['PortBindings']"}, 'parser_func': service_parser_hostconfig_ports}, 249 | 'privileged': {'API_Info': {'Privileged': "$['HostConfig']['Privileged']"}, 'parser_func': service_parser_naive}, 250 | 'network_mode': {'API_Info': {'NetworkMode': "$['HostConfig']['NetworkMode']"}, 'parser_func': service_parser_naive}, 251 | 'devices': {'API_Info': {'Devices': "$['HostConfig']['Devices']"}, 'parser_func': service_parser_hostconfig_devices}, 252 | 'dns': {'API_Info': {'Dns': "$['HostConfig']['Dns']"}, 'parser_func': service_parser_naive}, 253 | 'dns_search': {'API_Info': {'DnsSearch': "$['HostConfig']['DnsSearch']"}, 'parser_func': service_parser_naive}, 254 | 'restart': { 255 | 'API_Info': {'RestartPolicy': "$['HostConfig']['RestartPolicy']"}, 256 | 'parser_func': service_parser_hostconfig_restart 257 | }, 258 | 'cap_add': {'API_Info': {'CapAdd': "$['HostConfig']['CapAdd']"}, 'parser_func': service_parser_naive}, 259 | 'cap_drop': {'API_Info': {'CapDrop': "$['HostConfig']['CapDrop']"}, 'parser_func': service_parser_naive}, 260 | 'ulimits': {'API_Info': {'Ulimits': "$['HostConfig']['Ulimits']"}, 'parser_func': service_parser_hostconfig_ulimits}, 261 | 'logging': {'API_Info': {'LogConfig': "$['HostConfig']['LogConfig']"}, 'parser_func': service_parser_hostconfig_logging}, 262 | 'extra_hosts': {'API_Info': {'ExtraHosts': "$['HostConfig']['ExtraHosts']"}, 'parser_func': service_parser_naive}, 263 | 'read_only': {'API_Info': {'ReadonlyRootfs': "$['HostConfig']['ReadonlyRootfs']"}, 'parser_func': service_parser_naive}, 264 | 'pid': {'API_Info': {'PidMode': "$['HostConfig']['PidMode']"}, 'parser_func': service_parser_naive}, 265 | 'security_opt': {'API_Info': {'SecurityOpt': "$['HostConfig']['SecurityOpt']"}, 'parser_func': service_parser_naive}, 266 | 'ipc': {'API_Info': {'IpcMode': "$['HostConfig']['IpcMode']"}, 'parser_func': service_parser_naive}, 267 | 'cgroup_parent': {'API_Info': {'CgroupParent': "$['HostConfig']['CgroupParent']"}, 'parser_func': service_parser_naive}, 268 | # 'shm_size:':{'API_Info':'ShmSize','parser_func':service_parser_naive}, 269 | 'sysctls': {'API_Info': {'Sysctls': "$['HostConfig']['Sysctls']"}, 'parser_func': service_parser_naive}, 270 | # 'tmpfs:':{'API_Info':'Tmpfs','parser_func':service_parser_naive}, 271 | 'userns_mode': {'API_Info': {'UsernsMode': "$['HostConfig']['UsernsMode']"}, 'parser_func': service_parser_naive}, 272 | 'isolation': {'API_Info': {'Isolation': "$['HostConfig']['Isolation']"}, 'parser_func': service_parser_naive}, 273 | 274 | # Volumes 275 | 'volumes': { 276 | 'API_Info': { 277 | 'Mounts': "$['HostConfig']['Mounts']", 278 | 'Binds': "$['HostConfig']['Binds']" 279 | }, 280 | 'parser_func': service_parser_volumes 281 | }, 282 | 283 | # NetworkingConfig 284 | 'networks': { 285 | 'API_Info': {'NetworkingConfig': "$['NetworkingConfig']['EndpointsConfig']"}, 286 | 'parser_func': service_parser_networks 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /iotedgehubdev/composeproject.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | import json 6 | import os 7 | import sys 8 | import yaml 9 | 10 | from collections import OrderedDict 11 | from io import StringIO 12 | from .compose_parser import CreateOptionParser 13 | from .output import Output 14 | 15 | COMPOSE_VERSION = 3.6 16 | 17 | CREATE_OPTIONS_MAX_CHUNKS = 100 18 | 19 | 20 | class ComposeProject(object): 21 | 22 | def __init__(self, module_content): 23 | self.module_content = module_content 24 | self.yaml_dict = OrderedDict() 25 | self.Services = OrderedDict() 26 | self.Networks = {} 27 | self.Volumes = {} 28 | self.edge_info = {} 29 | 30 | def compose(self): 31 | modules = { 32 | self.edge_info['hub_name']: 33 | self.module_content['$edgeAgent']['properties.desired']['systemModules']['edgeHub'] 34 | } 35 | modules.update(self.module_content['$edgeAgent']['properties.desired']['modules']) 36 | for service_name, config in modules.items(): 37 | self.Services[service_name] = {} 38 | create_option_str = ComposeProject._join_create_options(config['settings']) 39 | if create_option_str: 40 | create_option = json.loads(create_option_str) 41 | create_option_parser = CreateOptionParser(create_option) 42 | self.Services[service_name].update(create_option_parser.parse_create_option()) 43 | self.Services[service_name]['image'] = config['settings']['image'] 44 | self.Services[service_name]['container_name'] = service_name 45 | 46 | if 'networks' not in self.Services[service_name]: 47 | self.Services[service_name]['networks'] = {} 48 | self.Services[service_name]['networks'][self.edge_info['network_info']['NW_NAME']] = None 49 | 50 | if 'network_mode' in self.Services[service_name]: 51 | del self.Services[service_name]['network_mode'] 52 | 53 | if 'host' in self.Services[service_name]['networks']: 54 | self.Services[service_name]['network_mode'] = 'host' 55 | del self.Services[service_name]['networks'] 56 | 57 | if 'labels' not in self.Services[service_name]: 58 | self.Services[service_name]['labels'] = {self.edge_info['labels']: ""} 59 | else: 60 | self.Services[service_name]['labels'][self.edge_info['labels']] = "" 61 | 62 | try: 63 | # Default restart policy is 'on-unhealthy' 64 | # https://github.com/Azure/iotedge/blob/8bd573590cdc149c014cf994dba58fc63f1a5c74/edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.Core/Constants.cs#L18 65 | restart_policy = config.get('restartPolicy', 'on-unhealthy') 66 | self.Services[service_name]['restart'] = { 67 | 'never': 'no', 68 | 'on-failure': 'on-failure', 69 | 'always': 'always', 70 | 'on-unhealthy': 'always', 71 | 'unknown': 'no' 72 | }[restart_policy] 73 | 74 | if restart_policy == 'on-unhealthy': 75 | Output().warning('Unsupported restart policy \'{0}\' in solution mode. Falling back to \'always\'.' 76 | .format(restart_policy)) 77 | except KeyError as e: 78 | raise KeyError('Unsupported restart policy {0} in solution mode.'.format(e)) 79 | 80 | if 'env' in config: 81 | self.Services[service_name]['environment'] = self.config_env( 82 | self.Services[service_name].get('environment', []), config['env']) 83 | 84 | if service_name == self.edge_info['hub_name']: 85 | self.config_edge_hub(service_name) 86 | else: 87 | self.config_modules(service_name) 88 | 89 | if 'networks' in self.Services[service_name]: 90 | for nw in self.Services[service_name]['networks']: 91 | self.Networks[nw] = { 92 | 'external': True 93 | } 94 | 95 | for vol in self.Services[service_name]['volumes']: 96 | if vol['type'] == 'volume': 97 | self.Volumes[vol['source']] = { 98 | 'name': vol['source'] 99 | } 100 | 101 | def set_edge_info(self, info): 102 | self.edge_info = info 103 | 104 | def config_modules(self, service_name): 105 | config = self.Services[service_name] 106 | if 'volumes' not in config: 107 | config['volumes'] = [] 108 | config['volumes'].append({ 109 | 'type': 'volume', 110 | 'source': self.edge_info['volume_info']['MODULE_VOLUME'], 111 | 'target': self.edge_info['volume_info']['MODULE_MOUNT'] 112 | }) 113 | 114 | if 'environment' not in config: 115 | config['environment'] = [] 116 | for module_env in self.edge_info['env_info']['module_env']: 117 | config['environment'].append(module_env) 118 | config['environment'].append( 119 | 'EdgeHubConnectionString=' + self.edge_info['ConnStr_info'][service_name] 120 | ) 121 | 122 | if 'depends_on' not in config: 123 | config['depends_on'] = [] 124 | config['depends_on'].append(self.edge_info['hub_name']) 125 | 126 | def config_edge_hub(self, service_name): 127 | config = self.Services[service_name] 128 | if 'volumes' not in config: 129 | config['volumes'] = [] 130 | config['volumes'].append({ 131 | 'type': 'volume', 132 | 'source': self.edge_info['volume_info']['HUB_VOLUME'], 133 | 'target': self.edge_info['volume_info']['HUB_MOUNT'] 134 | }) 135 | 136 | config['networks'][self.edge_info['network_info']['NW_NAME']] = { 137 | 'aliases': [self.edge_info['network_info']['ALIASES']] 138 | } 139 | 140 | if 'environment' not in config: 141 | config['environment'] = [] 142 | routes_env = self.parse_routes() 143 | for e in routes_env: 144 | config['environment'].append(e) 145 | config['environment'].append( 146 | 'IotHubConnectionString=' + self.edge_info['ConnStr_info']['$edgeHub']) 147 | config['environment'].extend(self.edge_info['env_info']['hub_env']) 148 | 149 | def config_env(self, env_list, env_section): 150 | env_dict = {} 151 | for env in env_list: 152 | if '=' in env: 153 | k, v = env.split('=', 1) 154 | else: 155 | k, v = env, '' 156 | env_dict[k] = v 157 | for k, v in env_section.items(): 158 | if 'value' not in v: 159 | env_dict[k] = '' 160 | else: 161 | env_dict[k] = v['value'] 162 | ret = [] 163 | for k, v in env_dict.items(): 164 | ret.append("{0}={1}".format(k, v)) 165 | return ret 166 | 167 | def parse_routes(self): 168 | routes = self.module_content['$edgeHub']['properties.desired']['routes'] 169 | schema_version = self.module_content['$edgeHub']['properties.desired']['schemaVersion'] 170 | routes_env = [] 171 | route_id = 1 172 | 173 | for route in routes.values(): 174 | if isinstance(route, str): 175 | routes_env.append('routes__r{0}={1}'.format(route_id, route)) 176 | else: 177 | if schema_version >= "1.1": 178 | routes_env.append('routes__r{0}={1}'.format(route_id, route["route"])) 179 | else: 180 | raise Exception("Route priority/TTL is not supported in schema {0}.".format(schema_version)) 181 | route_id = route_id + 1 182 | return routes_env 183 | 184 | def dump(self, target): 185 | def setup_yaml(): 186 | def represent_dict_order(self, data): 187 | return self.represent_mapping('tag:yaml.org,2002:map', data.items()) 188 | yaml.add_representer(OrderedDict, represent_dict_order) 189 | setup_yaml() 190 | 191 | def my_unicode_repr(self, data): 192 | return self.represent_str(data.encode('utf-8')) 193 | 194 | self.yaml_dict['version'] = str(COMPOSE_VERSION) 195 | self.yaml_dict['services'] = self.Services 196 | self.yaml_dict['networks'] = self.Networks 197 | self.yaml_dict['volumes'] = self.Volumes 198 | 199 | if sys.version_info[0] < 3: 200 | # Add # noqa: F821 to ignore undefined name 'unicode' error 201 | yaml.add_representer(unicode, my_unicode_repr) # noqa: F821 202 | yml_stream = StringIO() 203 | 204 | yaml.dump(self.yaml_dict, yml_stream, default_flow_style=False) 205 | yml_str = yml_stream.getvalue().replace('$', '$$') 206 | 207 | if not os.path.exists(os.path.dirname(target)): 208 | os.makedirs(os.path.dirname(target)) 209 | 210 | with open(target, 'w') as f: 211 | f.write(yml_str) 212 | 213 | @staticmethod 214 | def _join_create_options(settings): 215 | if 'createOptions' not in settings: 216 | return '' 217 | 218 | res = settings['createOptions'] 219 | 220 | i = 0 221 | while True: 222 | i += 1 223 | key = 'createOptions{0:0=2d}'.format(i) 224 | if i < CREATE_OPTIONS_MAX_CHUNKS and key in settings: 225 | res += settings[key] 226 | else: 227 | break 228 | 229 | return res 230 | -------------------------------------------------------------------------------- /iotedgehubdev/configs.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | import os 6 | import configparser 7 | 8 | from . import decorators 9 | from .hostplatform import HostPlatform 10 | 11 | PRIVACY_STATEMENT = """ 12 | Welcome to iotedgehubdev! 13 | ------------------------- 14 | Telemetry 15 | --------- 16 | The iotedgehubdev collects usage data in order to improve your experience. 17 | The data is anonymous and does not include commandline argument values. 18 | The data is collected by Microsoft. 19 | 20 | You can change your telemetry settings by updating 'collect_telemetry' to 'no' in {0} 21 | """ 22 | 23 | 24 | class ProductConfig(object): 25 | def __init__(self): 26 | self.config = configparser.ConfigParser({ 27 | 'firsttime': 'yes' 28 | }) 29 | self.setup_config() 30 | 31 | @decorators.suppress_all_exceptions() 32 | def setup_config(self): 33 | try: 34 | configPath = HostPlatform.get_config_path() 35 | iniFilePath = HostPlatform.get_setting_ini_path() 36 | if not os.path.exists(configPath): 37 | os.makedirs(configPath) 38 | if not os.path.exists(iniFilePath): 39 | with open(iniFilePath, 'w') as iniFile: 40 | self.config.write(iniFile) 41 | else: 42 | with open(iniFilePath, 'r') as iniFile: 43 | self.config.read_file(iniFile) 44 | with open(iniFilePath, 'w') as iniFile: 45 | self.config.write(iniFile) 46 | except Exception: 47 | pass 48 | 49 | @decorators.suppress_all_exceptions() 50 | def update_config(self): 51 | with open(HostPlatform.get_setting_ini_path(), 'w') as iniFile: 52 | self.config.write(iniFile) 53 | 54 | @decorators.suppress_all_exceptions() 55 | def set_val(self, direct, section, val): 56 | if val is not None: 57 | self.config.set(direct, section, val) 58 | self.update_config() 59 | 60 | 61 | _prod_config = ProductConfig() 62 | 63 | 64 | @decorators.suppress_all_exceptions() 65 | def get_ini_config(): 66 | return _prod_config.config 67 | 68 | 69 | @decorators.suppress_all_exceptions() 70 | def update_ini(): 71 | _prod_config.update_config() 72 | 73 | 74 | @decorators.suppress_all_exceptions() 75 | def check_firsttime(): 76 | if 'no' != _prod_config.config.get('DEFAULT', 'firsttime'): 77 | config = _prod_config.config 78 | config.set('DEFAULT', 'firsttime', 'no') 79 | print(PRIVACY_STATEMENT.format(HostPlatform.get_setting_ini_path())) 80 | config.set('DEFAULT', 'collect_telemetry', 'yes') 81 | _prod_config.update_config() 82 | -------------------------------------------------------------------------------- /iotedgehubdev/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | class EdgeConstants(): 6 | HOSTNAME_KEY = 'HostName' 7 | DEVICE_ID_KEY = 'DeviceId' 8 | ACCESS_KEY_KEY = 'SharedAccessKey' 9 | ACCESS_KEY_NAME = 'SharedAccessKeyName' 10 | DEVICE_ACCESS_KEY_KEY = 'Device_SharedAccessKey' 11 | HUB_ACCESS_KEY_KEY = 'Hub_SharedAccessKey' 12 | 13 | SUBJECT_COUNTRY_KEY = 'countryCode' 14 | SUBJECT_STATE_KEY = 'state' 15 | SUBJECT_LOCALITY_KEY = 'locality' 16 | SUBJECT_ORGANIZATION_KEY = 'organization' 17 | SUBJECT_ORGANIZATION_UNIT_KEY = 'organizationUnit' 18 | SUBJECT_COMMON_NAME_KEY = 'commonName' 19 | 20 | EDGE_CHAIN_CA = 'edge-chain-ca' 21 | EDGE_HUB_SERVER = 'edge-hub-server' 22 | EDGE_DEVICE_CA = 'edge-device-ca' 23 | EDGE_AGENT_CA = 'edge-agent-ca' 24 | CERT_SUFFIX = '.cert.pem' 25 | CHAIN_CERT_SUFFIX = '-chain.cert.pem' 26 | PFX_SUFFIX = '.cert.pfx' 27 | KEY_SUFFIX = '.key.pem' 28 | ROOT_CA_ID = 'azure-iot-test-only.root.ca' 29 | DEVICE_CA_ID = 'iot-edge-device-ca' 30 | CERT_FOLDER = 'certs' 31 | 32 | CERT_DEFAULT_DICT = { 33 | SUBJECT_COUNTRY_KEY: 'US', 34 | SUBJECT_STATE_KEY: 'Washington', 35 | SUBJECT_LOCALITY_KEY: 'Redmond', 36 | SUBJECT_ORGANIZATION_KEY: 'Default Edge Organization', 37 | SUBJECT_ORGANIZATION_UNIT_KEY: 'Edge Unit', 38 | SUBJECT_COMMON_NAME_KEY: 'Edge Test Device CA' 39 | } 40 | 41 | # Port of Docker daemon 42 | # https://github.com/docker/docker-ce/blob/f9756bfb29877236a83979170ef2c0aa35eb57c6/components/engine/volume/mounts/windows_parser.go#L19-L76 43 | MOUNT_HOST_DIR_REGEX = r'(?:\\\\\?\\)?[a-z]:[\\/](?:[^\\/:*?"<>|\r\n]+[\\/]?)*' 44 | MOUNT_NAME_REGEX = r'[^\\/:*?"<>|\r\n]+' 45 | MOUNT_PIPE_REGEX = r'[/\\]{2}.[/\\]pipe[/\\][^:*?"<>|\r\n]+' 46 | MOUNT_SOURCE_REGEX = r'((?P((' + MOUNT_HOST_DIR_REGEX + r')|(' + \ 47 | MOUNT_NAME_REGEX + r')|(' + MOUNT_PIPE_REGEX + r'))):)?' 48 | MOUNT_MODE_REGEX = r'(:(?P(?i)ro|rw))?' 49 | MOUNT_WIN_DEST_REGEX = r'(?P((?:\\\\\?\\)?([a-z]):((?:[\\/][^\\/:*?"<>\r\n]+)*[\\/]?))|(' + \ 50 | MOUNT_PIPE_REGEX + r'))' 51 | MOUNT_LCOW_DEST_REGEX = r'(?P/(?:[^\\/:*?"<>\r\n]+[/]?)*)' 52 | MOUNT_WIN_REGEX = r'^' + MOUNT_SOURCE_REGEX + MOUNT_WIN_DEST_REGEX + MOUNT_MODE_REGEX + r'$' 53 | MOUNT_LCOW_REGEX = r'^' + MOUNT_SOURCE_REGEX + MOUNT_LCOW_DEST_REGEX + MOUNT_MODE_REGEX + r'$' 54 | -------------------------------------------------------------------------------- /iotedgehubdev/decorators.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | from functools import wraps 6 | 7 | 8 | def suppress_all_exceptions(fallback_return=None): 9 | def _decorator(func): 10 | @wraps(func) 11 | def _wrapped_func(*args, **kwargs): 12 | try: 13 | return func(*args, **kwargs) 14 | except Exception: 15 | if fallback_return: 16 | return fallback_return 17 | else: 18 | pass 19 | 20 | return _wrapped_func 21 | 22 | return _decorator 23 | 24 | 25 | def hash256_result(func): 26 | """Secure the return string of the annotated function with SHA256 algorithm. If the annotated 27 | function doesn't return string or return None, raise ValueError.""" 28 | @wraps(func) 29 | def _decorator(*args, **kwargs): 30 | val = func(*args, **kwargs) 31 | if not val: 32 | raise ValueError('Return value is None') 33 | elif not isinstance(val, str): 34 | raise ValueError('Return value is not string') 35 | 36 | from .utils import Utils 37 | return Utils.get_sha256_hash(val) 38 | 39 | return _decorator 40 | -------------------------------------------------------------------------------- /iotedgehubdev/edgecert.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | from .certutils import EdgeCertUtil 5 | from .constants import EdgeConstants 6 | 7 | 8 | class EdgeCert(object): 9 | def __init__(self, certs_dir, hostname): 10 | self.certs_dir = certs_dir 11 | self.hostname = hostname 12 | 13 | def generate_self_signed_certs(self): 14 | cert_util = EdgeCertUtil() 15 | cert_util.create_root_ca_cert(EdgeConstants.EDGE_DEVICE_CA, 16 | validity_days_from_now=365, 17 | subject_dict=EdgeConstants.CERT_DEFAULT_DICT, 18 | passphrase=None) 19 | cert_util.export_simulator_cert_artifacts_to_dir(EdgeConstants.EDGE_DEVICE_CA, self.certs_dir) 20 | 21 | cert_util.create_intermediate_ca_cert(EdgeConstants.EDGE_AGENT_CA, 22 | EdgeConstants.EDGE_DEVICE_CA, 23 | validity_days_from_now=365, 24 | common_name='Edge Agent CA', 25 | set_terminal_ca=False, 26 | passphrase=None) 27 | cert_util.export_simulator_cert_artifacts_to_dir(EdgeConstants.EDGE_AGENT_CA, self.certs_dir) 28 | 29 | cert_util.create_server_cert(EdgeConstants.EDGE_HUB_SERVER, 30 | EdgeConstants.EDGE_AGENT_CA, 31 | validity_days_from_now=365, 32 | hostname=self.hostname) 33 | cert_util.export_simulator_cert_artifacts_to_dir(EdgeConstants.EDGE_HUB_SERVER, self.certs_dir) 34 | cert_util.export_pfx_cert(EdgeConstants.EDGE_HUB_SERVER, self.certs_dir) 35 | 36 | prefixes = [EdgeConstants.EDGE_AGENT_CA, EdgeConstants.EDGE_DEVICE_CA] 37 | cert_util.chain_simulator_ca_certs(EdgeConstants.EDGE_CHAIN_CA, prefixes, self.certs_dir) 38 | 39 | # Generate IoT Edge device CA to be configured in IoT Edge runtime 40 | def generate_device_ca(self, valid_days, overwrite_existing, trusted_ca, trusted_ca_key, trusted_ca_key_passphase): 41 | # Function level variables 42 | create_root_ca = not (trusted_ca and trusted_ca_key) 43 | # Generate certs 44 | cert_util = EdgeCertUtil() 45 | if create_root_ca: 46 | cert_util.create_root_ca_cert(EdgeConstants.ROOT_CA_ID, 47 | validity_days_from_now=valid_days, 48 | subject_dict=EdgeConstants.CERT_DEFAULT_DICT, 49 | passphrase=None) 50 | cert_util.export_device_ca_cert_artifacts_to_dir(EdgeConstants.ROOT_CA_ID, self.certs_dir) 51 | else: 52 | cert_util.load_cert_from_file(EdgeConstants.ROOT_CA_ID, trusted_ca, trusted_ca_key, trusted_ca_key_passphase) 53 | 54 | cert_util.create_intermediate_ca_cert(EdgeConstants.DEVICE_CA_ID, EdgeConstants.ROOT_CA_ID, 55 | validity_days_from_now=valid_days, 56 | common_name='Edge Device CA', 57 | set_terminal_ca=False, 58 | passphrase=None) 59 | cert_util.export_device_ca_cert_artifacts_to_dir(EdgeConstants.DEVICE_CA_ID, self.certs_dir) 60 | cert_util.chain_device_ca_certs(EdgeConstants.DEVICE_CA_ID, 61 | [EdgeConstants.DEVICE_CA_ID, EdgeConstants.ROOT_CA_ID], 62 | self.certs_dir) 63 | 64 | def get_cert_file_path(self, id_str): 65 | return EdgeCertUtil.get_cert_file_path(id_str, self.certs_dir) 66 | 67 | def get_pfx_file_path(self, id_str): 68 | return EdgeCertUtil.get_pfx_file_path(id_str, self.certs_dir) 69 | -------------------------------------------------------------------------------- /iotedgehubdev/edgedockerclient.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | import docker 6 | import os 7 | import time 8 | import tarfile 9 | from io import BytesIO 10 | from .errors import EdgeDeploymentError 11 | from .utils import Utils 12 | 13 | 14 | class EdgeDockerClient(object): 15 | _DOCKER_INFO_OS_TYPE_KEY = 'OSType' 16 | 17 | def __init__(self, docker_client=None): 18 | if docker_client is not None: 19 | self._client = docker_client 20 | else: 21 | try: 22 | self._client = docker.DockerClient.from_env(version='auto') 23 | except Exception as ex: 24 | msg = 'Could not connect to Docker daemon. Please make sure Docker is running' 25 | raise EdgeDeploymentError(msg, ex) 26 | 27 | def __enter__(self): 28 | return self 29 | 30 | def __exit__(self, exc_type, exc_value, traceback): 31 | if self._client is not None: 32 | self._client.api.close() 33 | 34 | def stop_remove_by_label(self, label): 35 | try: 36 | filter_dict = {'label': label} 37 | containers = self._client.containers.list(all=True, filters=filter_dict) 38 | for container in containers: 39 | container.stop() 40 | self.remove(container.name) 41 | except docker.errors.APIError as ex: 42 | msg = 'Could not stop and remove containers by label: {0}'.format(label) 43 | raise EdgeDeploymentError(msg, ex) 44 | 45 | def get_local_image_sha_id(self, image): 46 | local_id = None 47 | try: 48 | inspect_dict = self._client.api.inspect_image(image) 49 | local_id = inspect_dict['Id'] 50 | except docker.errors.APIError: 51 | local_id = None 52 | return local_id 53 | 54 | def pull(self, image, username, password): 55 | old_id = self.get_local_image_sha_id(image) 56 | try: 57 | is_updated = True 58 | auth_dict = None 59 | if username is not None: 60 | auth_dict = {'username': username, 'password': password} 61 | self._client.images.pull(image, auth_config=auth_dict) 62 | if old_id is not None: 63 | inspect_dict = self._client.api.inspect_image(image) 64 | new_id = inspect_dict['Id'] 65 | if new_id == old_id: 66 | is_updated = False 67 | 68 | return is_updated 69 | except docker.errors.APIError as ex: 70 | msg = 'Error during pull for image {0}'.format(image) 71 | raise EdgeDeploymentError(msg, ex) 72 | 73 | def pullIfNotExist(self, image, username, password): 74 | imageId = self.get_local_image_sha_id(image) 75 | if imageId is None: 76 | return self.pull(image, username, password) 77 | 78 | def status(self, container_name): 79 | try: 80 | containers = self._client.containers.list(all=True) 81 | for container in containers: 82 | if container_name == container.name: 83 | return container.status 84 | return None 85 | except docker.errors.APIError as ex: 86 | msg = 'Error while checking status for: {0}'.format(container_name) 87 | raise EdgeDeploymentError(msg, ex) 88 | 89 | def stop(self, container_name): 90 | self._exec_container_method(container_name, 'stop') 91 | 92 | def start(self, container_name): 93 | self._exec_container_method(container_name, 'start') 94 | 95 | def remove(self, container_name): 96 | self._exec_container_method(container_name, 'remove') 97 | 98 | def create_network(self, network_name): 99 | create_network = False 100 | try: 101 | networks = self._client.networks.list(names=[network_name]) 102 | if networks: 103 | num_networks = len(networks) 104 | if num_networks == 0: 105 | create_network = True 106 | else: 107 | create_network = True 108 | if create_network is True: 109 | os_name = self.get_os_type() 110 | if os_name == 'windows': 111 | return self._client.networks.create(network_name, driver='nat') 112 | else: 113 | return self._client.networks.create(network_name, driver='bridge') 114 | except docker.errors.APIError as ex: 115 | msg = 'Could not create docker network: {0}'.format(network_name) 116 | raise EdgeDeploymentError(msg, ex) 117 | 118 | def create_volume(self, volume_name): 119 | try: 120 | volume = self._get_volume_if_exists(volume_name) 121 | if volume is None: 122 | return self._client.volumes.create(volume_name) 123 | except docker .errors.APIError as ex: 124 | msg = 'Docker volume create failed for: {0}'.format(volume_name) 125 | raise EdgeDeploymentError(msg, ex) 126 | 127 | def create_config_for_network(self, nw_name, *args, **kwargs): 128 | return self._client.api.create_networking_config({ 129 | nw_name: self._client.api.create_endpoint_config(*args, **kwargs) 130 | }) 131 | 132 | def create_container(self, image, **kwargs): 133 | try: 134 | return self._client.api.create_container(image, **kwargs) 135 | except docker.errors.ContainerError as ex_ctr: 136 | msg = 'Container exited with errors: {0}'.format(kwargs.get('name', None)) 137 | raise EdgeDeploymentError(msg, ex_ctr) 138 | except docker.errors.ImageNotFound as ex_img: 139 | msg = 'Docker create failed. Image not found: {0}'.format(image) 140 | raise EdgeDeploymentError(msg, ex_img) 141 | except docker.errors.APIError as ex: 142 | msg = 'Docker create failed for image: {0}'.format(image) 143 | raise EdgeDeploymentError(msg, ex) 144 | 145 | def create_host_config(self, *args, **kwargs): 146 | try: 147 | return self._client.api.create_host_config(*args, **kwargs) 148 | except Exception as ex: 149 | msg = 'docker create host config failed' 150 | raise EdgeDeploymentError(msg, ex) 151 | 152 | def copy_file_to_volume(self, 153 | container_name, 154 | volume_name, 155 | volume_dest_file_name, 156 | volume_dest_dir_path, 157 | host_src_file): 158 | if self.get_os_type() == 'windows': 159 | self._insert_file_in_volume_mount(volume_name, host_src_file, volume_dest_file_name) 160 | else: 161 | self._insert_file_in_container(container_name, 162 | volume_dest_file_name, 163 | volume_dest_dir_path, 164 | host_src_file) 165 | 166 | def get_os_type(self): 167 | try: 168 | info = self._client.info() 169 | return info[EdgeDockerClient._DOCKER_INFO_OS_TYPE_KEY].lower() 170 | except docker.errors.APIError as ex: 171 | msg = 'Docker daemon returned error' 172 | raise EdgeDeploymentError(msg, ex) 173 | 174 | def destroy_network(self, network_name): 175 | try: 176 | networks = self._client.networks.list(names=[network_name]) 177 | if networks is not None: 178 | for network in networks: 179 | if network.name == network_name: 180 | network.remove() 181 | except docker.errors.APIError as ex: 182 | msg = 'Could not remove docker network: {0}'.format(network_name) 183 | raise EdgeDeploymentError(msg, ex) 184 | 185 | def remove_volume(self, volume_name, force=False): 186 | try: 187 | volume = self._get_volume_if_exists(volume_name) 188 | if volume is not None: 189 | volume.remove(force) 190 | except docker.errors.APIError as ex: 191 | msg = 'Docker volume remove failed for: {0}, force flag: {1}'.format(volume_name, force) 192 | raise EdgeDeploymentError(msg, ex) 193 | 194 | def _get_volume_if_exists(self, name): 195 | try: 196 | return self._client.volumes.get(name) 197 | except docker.errors.NotFound: 198 | return None 199 | except docker.errors.APIError as ex: 200 | msg = 'Docker volume get failed for: {0}'.format(name) 201 | raise EdgeDeploymentError(msg, ex) 202 | 203 | def _exec_container_method(self, container_name, method, **kwargs): 204 | container = self._get_container_by_name(container_name) 205 | try: 206 | getattr(container, method)(**kwargs) 207 | except docker.errors.APIError as ex: 208 | msg = 'Could not {0} container: {1}'.format(method, container_name) 209 | raise EdgeDeploymentError(msg, ex) 210 | 211 | def _get_container_by_name(self, container_name): 212 | try: 213 | return self._client.containers.get(container_name) 214 | except docker.errors.NotFound as nf_ex: 215 | msg = 'Could not find container by name {0}'.format(container_name) 216 | raise EdgeDeploymentError(msg, nf_ex) 217 | except docker.errors.APIError as ex: 218 | msg = 'Error getting container by name: {0}'.format(container_name) 219 | raise EdgeDeploymentError(msg, ex) 220 | 221 | def _insert_file_in_volume_mount(self, volume_name, host_src_file, volume_dest_file_name): 222 | try: 223 | volume_info = self._client.api.inspect_volume(volume_name) 224 | Utils.copy_files(host_src_file.replace('\\\\', '\\'), 225 | os.path.join(volume_info['Mountpoint'].replace('\\\\', '\\'), volume_dest_file_name)) 226 | except docker.errors.APIError as docker_ex: 227 | msg = 'Docker volume inspect failed for: {0}'.format(volume_name) 228 | raise EdgeDeploymentError(msg, docker_ex) 229 | except (OSError, IOError) as ex_os: 230 | msg = 'File IO error seen copying files to volume: {0}. ' \ 231 | 'Errno: {1}, Error {2}'.format(volume_name, str(ex_os.errno), ex_os.strerror) 232 | raise EdgeDeploymentError(msg, ex_os) 233 | 234 | def _insert_file_in_container(self, 235 | container_name, 236 | volume_dest_file_name, 237 | volume_dest_dir_path, 238 | host_src_file): 239 | try: 240 | (tar_stream, dest_archive_info, container_tar_file) = \ 241 | EdgeDockerClient.create_tar_objects(volume_dest_file_name) 242 | file_data = open(host_src_file, 'rb').read() 243 | dest_archive_info.size = len(file_data) 244 | dest_archive_info.mtime = time.time() 245 | dest_archive_info.mode = 0o444 246 | container_tar_file.addfile(dest_archive_info, BytesIO(file_data)) 247 | container_tar_file.close() 248 | tar_stream.seek(0) 249 | container = self._get_container_by_name(container_name) 250 | container.put_archive(volume_dest_dir_path, tar_stream) 251 | except docker.errors.APIError as docker_ex: 252 | msg = 'Container put_archive failed for container: {0}'.format(container_name) 253 | raise EdgeDeploymentError(msg, docker_ex) 254 | except (OSError, IOError) as ex_os: 255 | msg = 'File IO error seen during put archive for container: {0}. ' \ 256 | 'Errno: {1}, Error {2}'.format(container_name, str(ex_os.errno), ex_os.strerror) 257 | raise EdgeDeploymentError(msg, ex_os) 258 | 259 | @staticmethod 260 | def create_tar_objects(container_dest_file_name): 261 | tar_stream = BytesIO() 262 | dest_archive_info = tarfile.TarInfo(name=container_dest_file_name) 263 | container_tar_file = tarfile.TarFile(fileobj=tar_stream, mode='w') 264 | return (tar_stream, dest_archive_info, container_tar_file) 265 | 266 | @classmethod 267 | def create_instance(cls, docker_client): 268 | """ 269 | Factory method useful in testing. 270 | """ 271 | return cls(docker_client) 272 | -------------------------------------------------------------------------------- /iotedgehubdev/errors.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | class EdgeError(Exception): 6 | def __init__(self, msg, ex=None): 7 | if ex: 8 | msg += ' : {0}'.format(str(ex)) 9 | super(EdgeError, self).__init__(msg) 10 | self._ex = ex 11 | 12 | 13 | class EdgeInvalidArgument(EdgeError): 14 | def __init__(self, msg, ex=None): 15 | super(EdgeInvalidArgument, self).__init__(msg, ex) 16 | 17 | 18 | class EdgeValueError(EdgeError): 19 | def __init__(self, msg, ex=None): 20 | super(EdgeValueError, self).__init__(msg, ex) 21 | 22 | 23 | class EdgeFileAccessError(EdgeError): 24 | def __init__(self, msg, file_name, ex=None): 25 | msg += ': {0}'.format(file_name) 26 | super(EdgeFileAccessError, self).__init__(msg, ex) 27 | self.file_name = file_name 28 | 29 | 30 | class EdgeFileParseError(EdgeError): 31 | def __init__(self, msg, file_name, ex=None): 32 | msg += ': {0}'.format(file_name) 33 | super(EdgeFileParseError, self).__init__(msg, ex) 34 | self.file_name = file_name 35 | 36 | 37 | class EdgeDeploymentError(EdgeError): 38 | def __init__(self, msg, ex=None): 39 | super(EdgeDeploymentError, self).__init__(msg, ex) 40 | 41 | 42 | class ResponseError(EdgeError): 43 | def __init__(self, status_code, value): 44 | super(ResponseError, self).__init__('Code:{0}. Detail:{1}'.format(status_code, value)) 45 | self.value = value 46 | self.status_code = status_code 47 | 48 | 49 | class RegistriesLoginError(EdgeError): 50 | def __init__(self, registries, errmsg): 51 | super(RegistriesLoginError, self).__init__(errmsg) 52 | self._registries = registries 53 | self._errmsg = errmsg 54 | 55 | def getmsg(self): 56 | return ('Fail to login {0}. Detail: {1}').format(self._registries, self._errmsg) 57 | 58 | def registries(self): 59 | return self._registries 60 | 61 | 62 | class InvalidConfigError(EdgeError): 63 | def __init__(self, msg): 64 | super(InvalidConfigError, self).__init__(msg) 65 | -------------------------------------------------------------------------------- /iotedgehubdev/hostplatform.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | import os 6 | import platform 7 | from .errors import EdgeInvalidArgument 8 | 9 | 10 | class HostPlatform(object): 11 | _edge_dir = '.iotedgehubdev' 12 | _edgehub_config = 'edgehub.json' 13 | _setting_ini = 'setting.ini' 14 | _certs = 'certs' 15 | _data = 'data' 16 | _platforms = { 17 | 'linux': { 18 | 'supported_deployments': ['docker'], 19 | 'default_deployment': 'docker', 20 | 'default_edge_meta_dir_env': 'HOME', 21 | 'deployment': { 22 | 'docker': { 23 | 'linux': { 24 | 'default_uri': 'unix:///var/run/docker.sock' 25 | }, 26 | } 27 | } 28 | }, 29 | 'windows': { 30 | 'supported_deployments': ['docker'], 31 | 'default_deployment': 'docker', 32 | 'default_edge_meta_dir_env': 'LOCALAPPDATA', 33 | 'deployment': { 34 | 'docker': { 35 | 'linux': { 36 | 'default_uri': 'unix:///var/run/docker.sock' 37 | }, 38 | 'windows': { 39 | 'default_uri': 'npipe://./pipe/docker_engine' 40 | } 41 | } 42 | } 43 | }, 44 | 'darwin': { 45 | 'supported_deployments': ['docker'], 46 | 'default_deployment': 'docker', 47 | 'default_edge_meta_dir_env': 'HOME', 48 | 'deployment': { 49 | 'docker': { 50 | 'linux': { 51 | 'default_uri': 'unix:///var/run/docker.sock' 52 | }, 53 | } 54 | } 55 | } 56 | } 57 | 58 | # @staticmethod 59 | # def is_host_supported(host): 60 | # if host is None: 61 | # raise EdgeInvalidArgument('host cannot be None') 62 | 63 | # host = host.lower() 64 | # if host in _platforms: 65 | # return True 66 | # return False 67 | 68 | @staticmethod 69 | def get_edge_dir(host): 70 | return os.path.join(os.getenv(HostPlatform._platforms[host]['default_edge_meta_dir_env']), HostPlatform._edge_dir) 71 | 72 | @staticmethod 73 | def get_config_path(): 74 | host = platform.system() 75 | if host is None: 76 | raise EdgeInvalidArgument('host cannot be None') 77 | host = host.lower() 78 | if host in HostPlatform._platforms: 79 | return os.path.join(HostPlatform.get_edge_dir(host), 'config') 80 | return None 81 | 82 | @staticmethod 83 | def get_data_path(): 84 | host = platform.system() 85 | if host is None: 86 | raise EdgeInvalidArgument('host cannot be None') 87 | host = host.lower() 88 | if host in HostPlatform._platforms: 89 | return os.path.join(HostPlatform.get_edge_dir(host), 'data') 90 | return None 91 | 92 | @staticmethod 93 | def get_config_file_path(): 94 | configPath = HostPlatform.get_config_path() 95 | if configPath is not None: 96 | return os.path.join(configPath, HostPlatform._edgehub_config) 97 | return None 98 | 99 | @staticmethod 100 | def get_setting_ini_path(): 101 | configPath = HostPlatform.get_config_path() 102 | if configPath is not None: 103 | return os.path.join(configPath, HostPlatform._setting_ini) 104 | return None 105 | 106 | @staticmethod 107 | def get_default_cert_path(): 108 | host = platform.system() 109 | if host is None: 110 | raise EdgeInvalidArgument('host cannot be None') 111 | host = host.lower() 112 | if host in HostPlatform._platforms: 113 | return os.path.join(HostPlatform.get_data_path(), HostPlatform._certs) 114 | return None 115 | 116 | @staticmethod 117 | def get_share_data_path(): 118 | host = platform.system() 119 | if host is None: 120 | raise EdgeInvalidArgument('host cannot be None') 121 | host = host.lower() 122 | if host in HostPlatform._platforms: 123 | return os.path.join(HostPlatform.get_data_path(), HostPlatform._data) 124 | return None 125 | -------------------------------------------------------------------------------- /iotedgehubdev/output.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | import click 6 | 7 | 8 | class Output: 9 | 10 | def info(self, text, suppress=False): 11 | if not suppress: 12 | self.echo(text, color='yellow') 13 | 14 | def status(self, text): 15 | self.info(text) 16 | self.line() 17 | 18 | def prompt(self, text): 19 | self.echo(text, color='white') 20 | 21 | def warning(self, text): 22 | self.echo("WARNING: " + text, color='yellow') 23 | 24 | def error(self, text): 25 | self.echo("ERROR: " + text, color='red', err=True) 26 | 27 | def header(self, text, suppress=False): 28 | 29 | if not suppress: 30 | self.line() 31 | s = "======== {0} ========".format(text).upper() 32 | m = "=" * len(s) 33 | self.echo(m, color='white') 34 | self.echo(s, color='white') 35 | self.echo(m, color='white') 36 | self.line() 37 | 38 | def param(self, text, value, status, suppress): 39 | if value and not suppress: 40 | self.header("SETTING " + text) 41 | self.status(status) 42 | 43 | def footer(self, text, suppress=False): 44 | if not suppress: 45 | self.info(text.upper()) 46 | self.line() 47 | 48 | def procout(self, text): 49 | self.echo(text, dim=True) 50 | 51 | def line(self): 52 | self.echo(text="") 53 | 54 | def echo(self, text, color="", dim=False, err=False): 55 | try: 56 | click.secho(text, fg=color, dim=dim, err=err) 57 | except Exception: 58 | print(text) 59 | -------------------------------------------------------------------------------- /iotedgehubdev/telemetry.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | import datetime 6 | import json 7 | import platform 8 | import uuid 9 | import multiprocessing 10 | 11 | from collections import defaultdict 12 | from functools import wraps 13 | 14 | 15 | from . import configs, decorators 16 | from . import telemetry_upload as telemetry_core 17 | from . import __production__ as production_name 18 | 19 | PRODUCT_NAME = production_name 20 | 21 | 22 | class TelemetrySession(object): 23 | def __init__(self, correlation_id=None): 24 | self.start_time = None 25 | self.end_time = None 26 | self.correlation_id = correlation_id or str(uuid.uuid4()) 27 | self.command = 'command_name' 28 | self.parameters = [] 29 | self.result = 'None' 30 | self.result_summary = None 31 | self.extra_props = {} 32 | self.machineId = self._get_hash_mac_address() 33 | self.events = defaultdict(list) 34 | 35 | def generate_payload(self): 36 | props = { 37 | 'EventId': str(uuid.uuid4()), 38 | 'CorrelationId': self.correlation_id, 39 | 'MachineId': self.machineId, 40 | 'ProductName': PRODUCT_NAME, 41 | 'ProductVersion': _get_core_version(), 42 | 'CommandName': self.command, 43 | 'OS.Type': platform.system().lower(), 44 | 'OS.Version': platform.version().lower(), 45 | 'Result': self.result, 46 | 'StartTime': str(self.start_time), 47 | 'EndTime': str(self.end_time), 48 | 'Parameters': ','.join(self.parameters) 49 | } 50 | 51 | if self.result_summary: 52 | props['ResultSummary'] = self.result_summary 53 | 54 | props.update(self.extra_props) 55 | 56 | self.events[_get_AI_key()].append({ 57 | 'name': '{}/commandV2'.format(PRODUCT_NAME), 58 | 'properties': props 59 | }) 60 | 61 | payload = json.dumps(self.events) 62 | return _remove_symbols(payload) 63 | 64 | @decorators.suppress_all_exceptions() 65 | @decorators.hash256_result 66 | def _get_hash_mac_address(self): 67 | s = '' 68 | for index, c in enumerate(hex(uuid.getnode())[2:].upper()): 69 | s += c 70 | if index % 2: 71 | s += '-' 72 | 73 | s = s.strip('-') 74 | return s 75 | 76 | 77 | _session = TelemetrySession() 78 | 79 | 80 | def _user_agrees_to_telemetry(func): 81 | @wraps(func) 82 | def _wrapper(*args, **kwargs): 83 | if not configs.get_ini_config().getboolean('DEFAULT', 'collect_telemetry'): 84 | return None 85 | return func(*args, **kwargs) 86 | 87 | return _wrapper 88 | 89 | 90 | @decorators.suppress_all_exceptions() 91 | def start(cmdname, params=[]): 92 | _session.command = cmdname 93 | _session.start_time = datetime.datetime.utcnow() 94 | if params is not None: 95 | _session.parameters.extend(params) 96 | 97 | 98 | @decorators.suppress_all_exceptions() 99 | def success(): 100 | _session.result = 'Success' 101 | 102 | 103 | @decorators.suppress_all_exceptions() 104 | def fail(summary): 105 | _session.result = 'Fail' 106 | _session.result_summary = summary 107 | 108 | 109 | @decorators.suppress_all_exceptions() 110 | def add_extra_props(props): 111 | if props is not None: 112 | _session.extra_props.update(props) 113 | 114 | 115 | @_user_agrees_to_telemetry 116 | @decorators.suppress_all_exceptions() 117 | def flush(): 118 | # flush out current information 119 | _session.end_time = datetime.datetime.utcnow() 120 | 121 | payload = _session.generate_payload() 122 | if payload: 123 | _upload_telemetry_with_user_agreement(payload) 124 | 125 | # reset session fields, retaining correlation id and application 126 | _session.__init__(correlation_id=_session.correlation_id) 127 | 128 | 129 | @decorators.suppress_all_exceptions(fallback_return=None) 130 | def _get_core_version(): 131 | from iotedgehubdev import __version__ as core_version 132 | return core_version 133 | 134 | 135 | @decorators.suppress_all_exceptions() 136 | def _get_AI_key(): 137 | from iotedgehubdev import __AIkey__ as key 138 | return key 139 | 140 | 141 | # This includes a final user-agreement-check; ALL methods sending telemetry MUST call this. 142 | @_user_agrees_to_telemetry 143 | @decorators.suppress_all_exceptions() 144 | def _upload_telemetry_with_user_agreement(payload): 145 | p = multiprocessing.Process(target=telemetry_core.upload, args=(payload,)) 146 | p.start() 147 | 148 | 149 | def _remove_symbols(s): 150 | if isinstance(s, str): 151 | for c in '$%^&|': 152 | s = s.replace(c, '_') 153 | return s 154 | -------------------------------------------------------------------------------- /iotedgehubdev/telemetry_upload.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | import urllib.request as HTTPClient 6 | import sys 7 | import json 8 | 9 | from applicationinsights import TelemetryClient 10 | from applicationinsights.exceptions import enable 11 | from applicationinsights.channel import SynchronousSender, SynchronousQueue, TelemetryChannel 12 | from iotedgehubdev import decorators 13 | 14 | 15 | class LimitedRetrySender(SynchronousSender): 16 | def __init__(self): 17 | super(LimitedRetrySender, self).__init__() 18 | 19 | def send(self, data_to_send): 20 | """ Override the default resend mechanism in SenderBase. Stop resend when it fails.""" 21 | request_payload = json.dumps([a.write() for a in data_to_send]) 22 | 23 | request = HTTPClient.Request(self._service_endpoint_uri, bytearray(request_payload, 'utf-8'), 24 | {'Accept': 'application/json', 'Content-Type': 'application/json; charset=utf-8'}) 25 | try: 26 | HTTPClient.urlopen(request, timeout=10) 27 | except Exception: # pylint: disable=broad-except 28 | pass 29 | 30 | 31 | @decorators.suppress_all_exceptions() 32 | def upload(data_to_save): 33 | try: 34 | data_to_save = json.loads(data_to_save) 35 | except Exception: 36 | pass 37 | 38 | for instrumentation_key in data_to_save: 39 | client = TelemetryClient(instrumentation_key=instrumentation_key, 40 | telemetry_channel=TelemetryChannel(queue=SynchronousQueue(LimitedRetrySender()))) 41 | enable(instrumentation_key) 42 | for record in data_to_save[instrumentation_key]: 43 | name = record['name'] 44 | raw_properties = record['properties'] 45 | properties = {} 46 | measurements = {} 47 | for k, v in raw_properties.items(): 48 | if isinstance(v, str): 49 | properties[k] = v 50 | else: 51 | measurements[k] = v 52 | client.track_event(name, properties, measurements) 53 | client.flush() 54 | 55 | 56 | if __name__ == '__main__': 57 | # If user doesn't agree to upload telemetry, this scripts won't be executed. The caller should control. 58 | upload(sys.argv[1]) 59 | -------------------------------------------------------------------------------- /iotedgehubdev/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | import errno 6 | import os 7 | import shutil 8 | import socket 9 | import stat 10 | import subprocess 11 | 12 | 13 | from base64 import b64decode, b64encode 14 | from hashlib import sha256 15 | from hmac import HMAC 16 | from time import time 17 | from urllib.parse import urlencode, quote_plus 18 | from .constants import EdgeConstants as EC 19 | from .decorators import suppress_all_exceptions 20 | from .errors import EdgeFileAccessError 21 | 22 | 23 | class Utils(object): 24 | @staticmethod 25 | def parse_connection_strs(device_conn_str, hub_conn_str=None): 26 | data = Utils._parse_device_connection_str(device_conn_str) 27 | data[EC.DEVICE_ACCESS_KEY_KEY] = data.pop(EC.ACCESS_KEY_KEY) 28 | if hub_conn_str is not None: 29 | hub_data = Utils._parse_hub_connection_str(hub_conn_str, data[EC.HOSTNAME_KEY]) 30 | data[EC.HUB_ACCESS_KEY_KEY] = hub_data[EC.ACCESS_KEY_KEY] 31 | data[EC.ACCESS_KEY_NAME] = hub_data[EC.ACCESS_KEY_NAME] 32 | return data 33 | 34 | @staticmethod 35 | def _parse_device_connection_str(connection_string): 36 | data = Utils._split_connection_string(connection_string) 37 | if len(data) > 0: 38 | if EC.HOSTNAME_KEY not in data or EC.DEVICE_ID_KEY not in data or EC.ACCESS_KEY_KEY not in data: 39 | if EC.ACCESS_KEY_NAME in data: 40 | raise KeyError('Please make sure you are using a device connection string ' 41 | 'instead of an IoT Hub connection string') 42 | else: 43 | raise KeyError('Error parsing connection string. ' 44 | 'Please make sure you wrap the connection string with double quotes when supplying it via CLI') 45 | return data 46 | else: 47 | raise KeyError('Error parsing connection string') 48 | 49 | @staticmethod 50 | def _parse_hub_connection_str(hub_connection_string, host_name): 51 | hub_data = Utils._split_connection_string(hub_connection_string) 52 | if len(hub_data) > 0: 53 | if EC.HOSTNAME_KEY not in hub_data or EC.ACCESS_KEY_NAME not in hub_data or EC.ACCESS_KEY_KEY not in hub_data: 54 | if EC.DEVICE_ID_KEY in hub_data: 55 | raise KeyError('Please make sure you are using a IoT Hub connection string ' 56 | 'instead of an device connection string') 57 | else: 58 | raise KeyError('Error parsing connection string. ' 59 | 'Please make sure you wrap the connection string with double quotes when supplying it via CLI') 60 | elif hub_data[EC.HOSTNAME_KEY] != host_name: 61 | raise KeyError('Please make sure the device belongs to the IoT Hub') 62 | return hub_data 63 | else: 64 | raise KeyError('Error parsing IoT Hub connection string') 65 | 66 | @staticmethod 67 | def _split_connection_string(connection_string): 68 | data = dict() 69 | if connection_string is not None: 70 | parts = connection_string.split(';') 71 | for part in parts: 72 | if "=" in part: 73 | subparts = [s.strip() for s in part.split("=", 1)] 74 | data[subparts[0]] = subparts[1] 75 | return data 76 | 77 | @staticmethod 78 | def get_hostname(): 79 | try: 80 | return socket.getfqdn() 81 | except IOError as ex: 82 | raise ex 83 | 84 | @staticmethod 85 | def check_if_file_exists(file_path): 86 | if file_path is None \ 87 | or os.path.exists(file_path) is False \ 88 | or os.path.isfile(file_path) is False: 89 | return False 90 | return True 91 | 92 | @staticmethod 93 | def check_if_directory_exists(dir_path): 94 | if dir_path is None \ 95 | or os.path.exists(dir_path) is False \ 96 | or os.path.isdir(dir_path) is False: 97 | return False 98 | return True 99 | 100 | @staticmethod 101 | def delete_dir(dir_path): 102 | try: 103 | if os.path.exists(dir_path): 104 | shutil.rmtree(dir_path, onerror=Utils._remove_readonly_callback) 105 | except OSError as ex: 106 | raise ex 107 | 108 | @staticmethod 109 | def mkdir_if_needed(dir_path): 110 | try: 111 | os.makedirs(dir_path) 112 | except OSError as ex: 113 | if ex.errno != errno.EEXIST: 114 | raise ex 115 | 116 | @staticmethod 117 | def delete_file(file_path, file_type_diagnostic): 118 | try: 119 | if os.path.exists(file_path): 120 | os.unlink(file_path) 121 | except OSError as ex: 122 | msg = 'Error deleteing {0}: {1}. ' \ 123 | 'Errno: {2}, Error: {3}'.format(file_type_diagnostic, 124 | file_path, str(ex.errno), ex.strerror) 125 | raise EdgeFileAccessError(msg, file_path) 126 | 127 | @staticmethod 128 | def create_file(file_path, data, file_type_diagnostic, mode=0o644): 129 | try: 130 | fd = os.open(file_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode) 131 | with os.fdopen(fd, 'w') as output_file: 132 | output_file.write(data) 133 | except OSError as ex: 134 | msg = 'Error creating {0}: {1}. ' \ 135 | 'Errno: {2}, Error: {3}'.format(file_type_diagnostic, 136 | file_path, str(ex.errno), ex.strerror) 137 | raise EdgeFileAccessError(msg, file_path) 138 | 139 | @staticmethod 140 | def get_iot_hub_sas_token(uri, key, policy_name, expiry=3600): 141 | ttl = time() + expiry 142 | sign_key = "%s\n%d" % ((quote_plus(uri)), int(ttl)) 143 | signature = b64encode( 144 | HMAC(b64decode(key), sign_key.encode("utf-8"), sha256).digest()) 145 | 146 | rawtoken = { 147 | "sr": uri, 148 | "sig": signature, 149 | "se": str(int(ttl)) 150 | } 151 | 152 | if policy_name is not None: 153 | rawtoken["skn"] = policy_name 154 | 155 | return "SharedAccessSignature " + urlencode(rawtoken) 156 | 157 | @staticmethod 158 | def copy_files(src_path, dst_path): 159 | try: 160 | shutil.copy2(src_path, dst_path) 161 | except OSError as ex: 162 | raise ex 163 | 164 | @staticmethod 165 | def _remove_readonly_callback(func, path, excinfo): 166 | del func, excinfo 167 | os.chmod(path, stat.S_IWRITE) 168 | os.unlink(path) 169 | 170 | @staticmethod 171 | def exe_proc(params, shell=False, cwd=None, suppress_out=False): 172 | try: 173 | subprocess.check_call(params, shell=shell, cwd=cwd) 174 | except KeyboardInterrupt: 175 | raise 176 | except Exception as e: 177 | raise Exception("Error while executing command: {0}. {1}".format(' '.join(params), str(e))) 178 | 179 | @staticmethod 180 | @suppress_all_exceptions() 181 | def hash_connection_str_hostname(hostname): 182 | """Hash connection string hostname to count distint IoT Hub number""" 183 | if not hostname: 184 | return ("", "") 185 | 186 | # get hostname suffix (e.g., azure-devices.net) to distinguish national clouds 187 | if "." in hostname: 188 | hostname_suffix = hostname[hostname.index(".") + 1:] 189 | else: 190 | hostname_suffix = "" 191 | 192 | return (Utils.get_sha256_hash(hostname), hostname_suffix) 193 | 194 | @staticmethod 195 | def get_sha256_hash(val): 196 | hash_object = sha256(val.encode('utf-8')) 197 | 198 | return str(hash_object.hexdigest()).lower() 199 | 200 | @staticmethod 201 | def get_device_ca_file_paths(root_dir, cert_id): 202 | result = {} 203 | result[EC.CERT_SUFFIX] = os.path.join(root_dir, cert_id + EC.CERT_SUFFIX) 204 | result[EC.KEY_SUFFIX] = os.path.join(root_dir, cert_id + EC.KEY_SUFFIX) 205 | result[EC.CHAIN_CERT_SUFFIX] = os.path.join(root_dir, cert_id + EC.CHAIN_CERT_SUFFIX) 206 | return result 207 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from iotedgehubdev.cli import main 2 | import multiprocessing 3 | 4 | if __name__ == '__main__': 5 | multiprocessing.freeze_support() 6 | main() 7 | -------------------------------------------------------------------------------- /pyinstaller/hook-iotedgehubdev.cli.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('iotedgehubdev') 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | autopep8 2 | backports.unittest-mock 3 | click 4 | docker==5.0.3 5 | flake8==4.0.1 6 | pyOpenSSL==22.0.0 7 | python-dotenv 8 | requests>=2.25.1 9 | applicationinsights==0.11.9 10 | prompt_toolkit 11 | rope 12 | tox 13 | pyyaml>=5.4 14 | jsonpath_rw 15 | docker-compose==1.29.1 16 | pytest 17 | pyinstaller==4.10 18 | urllib3>=1.26.4 19 | regex 20 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/iotedgehubdev/9bb0a5c8c2abc77563bae09744f54dcd5bd97166/setup.cfg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Azure IoT EdgeHub Dev Tool 3 | """ 4 | from setuptools import find_packages, setup 5 | 6 | VERSION = '0.14.18' 7 | # If we have source, validate that our version numbers match 8 | # This should prevent uploading releases with mismatched versions. 9 | try: 10 | with open('iotedgehubdev/__init__.py', 'rb') as f: 11 | content = f.read().decode('utf-8') 12 | except OSError: 13 | pass 14 | else: 15 | import re 16 | import sys 17 | m = re.search(r'__version__\s*=\s*[\'"](.+?)[\'"]', content) 18 | if not m: 19 | print('Could not find __version__ in iotedgehubdev/__init__.py') 20 | sys.exit(1) 21 | if m.group(1) != VERSION: 22 | print('Expected __version__ = "{}"; found "{}"'.format(VERSION, m.group(1))) 23 | sys.exit(1) 24 | 25 | with open('README.md', 'rb') as f: 26 | readme = f.read().decode('utf-8') 27 | 28 | dependencies = [ 29 | 'click', 30 | 'docker==5.0.3', 31 | 'pyOpenSSL==22.0.0', 32 | 'requests>=2.25.1', 33 | 'applicationinsights==0.11.9', 34 | 'pyyaml>=5.4', 35 | 'jsonpath_rw', 36 | 'docker-compose==1.29.1', 37 | 'regex' 38 | ] 39 | 40 | setup( 41 | name='iotedgehubdev', 42 | version=VERSION, 43 | url='https://github.com/Azure/iotedgehubdev', 44 | license='MIT', 45 | author='iotedgehubdev', 46 | author_email='vsciet@microsoft.com', 47 | description='Azure IoT EdgeHub Dev Tool', 48 | long_description=readme, 49 | long_description_content_type='text/markdown', 50 | packages=find_packages(exclude=['tests']), 51 | include_package_data=True, 52 | zip_safe=False, 53 | platforms='any', 54 | install_requires=dependencies, 55 | entry_points={ 56 | 'console_scripts': [ 57 | 'iotedgehubdev = iotedgehubdev.cli:main', 58 | ], 59 | }, 60 | python_requires='>=3.5, <3.10', 61 | classifiers=[ 62 | # As from http://pypi.python.org/pypi?%3Aaction=list_classifiers 63 | # 'Development Status :: 1 - Planning', 64 | # 'Development Status :: 2 - Pre-Alpha', 65 | # 'Development Status :: 3 - Alpha', 66 | # 'Development Status :: 4 - Beta', 67 | 'Development Status :: 5 - Production/Stable', 68 | # 'Development Status :: 6 - Mature', 69 | # 'Development Status :: 7 - Inactive', 70 | 'Environment :: Console', 71 | 'Intended Audience :: Developers', 72 | 'License :: OSI Approved :: MIT License', 73 | 'Operating System :: POSIX', 74 | 'Operating System :: MacOS', 75 | 'Operating System :: Unix', 76 | 'Operating System :: Microsoft :: Windows', 77 | 'Programming Language :: Python', 78 | 'Programming Language :: Python :: 3', 79 | 'Programming Language :: Python :: 3.5', 80 | 'Programming Language :: Python :: 3.6', 81 | 'Programming Language :: Python :: 3.7', 82 | 'Programming Language :: Python :: 3.8', 83 | 'Programming Language :: Python :: 3.9', 84 | 85 | 'Topic :: Software Development :: Libraries :: Python Modules', 86 | ] 87 | ) 88 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | workingdirectory = os.getcwd() 5 | filename = os.path.join(workingdirectory, '.env') 6 | if os.path.exists(filename): 7 | load_dotenv(filename) 8 | print('env file loaded from {0}'.format(filename)) 9 | -------------------------------------------------------------------------------- /tests/assets/certs/iotedgehubdev-test-only.root.ca.cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGwzCCBKugAwIBAgICA+gwDQYJKoZIhvcNAQELBQAwgZoxCzAJBgNVBAYTAlVT 3 | MRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdSZWRtb25kMSIwIAYDVQQK 4 | DBlEZWZhdWx0IEVkZ2UgT3JnYW5pemF0aW9uMSIwIAYDVQQLDBlEZWZhdWx0IEVk 5 | Z2UgT3JnYW5pemF0aW9uMRwwGgYDVQQDDBNFZGdlIFRlc3QgRGV2aWNlIENBMB4X 6 | DTE5MTEyOTAxMTQyNVoXDTIyMTEyODAxMTQyNVowgZoxCzAJBgNVBAYTAlVTMRMw 7 | EQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdSZWRtb25kMSIwIAYDVQQKDBlE 8 | ZWZhdWx0IEVkZ2UgT3JnYW5pemF0aW9uMSIwIAYDVQQLDBlEZWZhdWx0IEVkZ2Ug 9 | T3JnYW5pemF0aW9uMRwwGgYDVQQDDBNFZGdlIFRlc3QgRGV2aWNlIENBMIICIjAN 10 | BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5J2neIMjPQvVZGo4RaoxMjsSoONh 11 | 8a+x+jC+i1Yt4N3Oy0SVs6ydeS7V/RU9wLncIejLVf/mBBxoSQ3U9MkDRzRCJFDt 12 | nEigGceLiSzaSjqGhMu9rrgSOB6R2Hc47ktL53C9H/XS2JNWo5lBbEiFCtrVOiHA 13 | vVtHH0pkWyVnHherPrzViFgN+2xKfdGSf3b5x7Vkydgd8VyfPSqOCaBmoepLiTeQ 14 | do8UfgQe/q8Ok/Ko8to0guYmWzM5iruuNfGScNkyiOlG8D6csmRglYJ0MS14bBv/ 15 | kpDGZ8wwxVnwn9hZUWJ2wjcc4bAUgZBslxAlgULykBinmbER/cHx1IPgF80jBM8X 16 | XnbBLF5nUcny+lgQatXs54SWi3cHz70pY6+NROG77Idoay5IunLmhhNYNFD+9d37 17 | KIBvHyI4+QJuPXv6lVUPpyAk+prWPtjxOkt1fh2LGpG5YRVGK0kETJJy6IGJH3HG 18 | IxAa3KsOU+k0o/JVFMmZKHHKhqlh9nWNLL4WvPPKYmy9TNVDm48V/I4WHeP7TR8k 19 | w+CIXynDTVuUaWkHPuJG1Cr3zk0qYIqEFarWT8VOLSXzcuykWE76JZj3vKgYwgTl 20 | 6JTFUuqvnTYLNq9BR3fpY56bdcXFcj99Bh+N5osaRzR7Ht75Lp7Nob7d3XMzm/F3 21 | hZgAbNVt16xBn8cCAwEAAaOCAQ8wggELMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O 22 | BBYEFHdJ5o0u67XMj/7sgqfTMABpDBJRMA4GA1UdDwEB/wQEAwIBhjCByAYDVR0j 23 | BIHAMIG9gBR3SeaNLuu1zI/+7IKn0zAAaQwSUaGBoKSBnTCBmjELMAkGA1UEBhMC 24 | VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1JlZG1vbmQxIjAgBgNV 25 | BAoMGURlZmF1bHQgRWRnZSBPcmdhbml6YXRpb24xIjAgBgNVBAsMGURlZmF1bHQg 26 | RWRnZSBPcmdhbml6YXRpb24xHDAaBgNVBAMME0VkZ2UgVGVzdCBEZXZpY2UgQ0GC 27 | AgPoMA0GCSqGSIb3DQEBCwUAA4ICAQCVLtykUHAg8sZQ8mCa90rvNIA+hMGG//mU 28 | k+2fciuK/e/4/ed+1drFfU3X3PilgCk9V6zgm3YMp9ou4ea/ulV4GrW1IhwV5nWE 29 | SbUaDmXf64EX5BYFWwsUgtEjMlRRZtvJ0jFuS2d+wNv1nqhHr2jR/Zf1fv4uBhVX 30 | a8bvXxs2ueANZ/a3SteJMOU+VLrBTihPeVqS9CbZacyTPXSmtq4wsu0g38+ya/50 31 | nQWq2Vx9QO8HlxDVCAf6us68H9tlMXArMh8pgFylaWhQzNrgb+e6Y/o14l9VNwFo 32 | nmy4vqTI8tQsMlUDH9j9cbJ0eFNguvVb+nANWF+mfRWFsnRxBzrIczPa9SuWku4L 33 | yG52PZNwVX5AYzzCBDOgwK86EYOqu0wlrwFvj37//xP1rQIstjiW22kpjhq455VU 34 | Vd+UD4QI3zIVrgM1bLQzWJtunmKy2z7vRYh+AZ7Kf3q/I7DmCsiiCCnwMU3r/Dzo 35 | jxH5zcHssAv3FYBBn5XZUcKbgxjEfPrFaDXL9D3KpxyP99L8zAtdRPmPAFbb5Dxc 36 | Y9LUysn7tteIr/+sQQCc2J4tqmbu6C0Ko/LTlcduCQKBr65NuNGkCn50Kf/SSKJA 37 | dck7wd0H/lgP1fvOO/tESopGeUm7ic8J72syrq4SScrZc4EN+dxZ0mzVqttI06bv 38 | JWqNQPthkQ== 39 | -----END CERTIFICATE----- 40 | -------------------------------------------------------------------------------- /tests/assets/certs/iotedgehubdev-test-only.root.ca.key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIuTv2QubjsEUCAggA 3 | MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBDXC9MHzO+PA26ETjuRjwmUBIIJ 4 | UEmc6O1i/lU3Vt3tXHLLl354UndpkBmbgO9kk5YUvhLRM6/z0lHmlbL7dBrKTtkE 5 | U/w52s9vnCMFfdMHNJrNLT/mvDUJfRP8C9Qfs90pDNja9XgQh8uI4vG8cNvAEKFi 6 | urOxjl7p64sNKdtmzFUhSh16uQjQ78QT9Ydk7ureqgABJMgKRR+UdQE3C0lmEtN+ 7 | erj/s2dQwuOkjd7OfH7YwsMox6MozVfq2udxmaDm9c7lArr+6BKyOaqNVWQ3Xx6z 8 | XErvt4xgvplSLkbTO4y+sQ6jqmOHjiE7wp9eueMLxjCSE/69dxWRHqwULR4BTPAF 9 | pUES1Rn6whf58h6IBzIZp2tGks3xWXO3abgm5eLAYT6H3tTDehdmDqooS5/D6T1d 10 | 2GRmu48A00yZedbDhfZ8iotuZkB2BnRBkFqSH3+1rveBmSOT2AxUa076jc+2gm9+ 11 | 3F8+PKgpSW4f3gLdUpEhPPRfCRO/IHjhBhoPtIHB8xMhYAx5GLFii1Q40ORNFKT2 12 | EJcUe4K2+U9jVcloYCjigmMhHlWQhRo8lxjxUWqlcX60LvYp6EAdPRv8rWPDBVpD 13 | QNmVMNyLSDUtXWenhLG8D78K7HYLhqummXC9FA3c3GZ2cHch/782UfXwDjtX5Pn/ 14 | jZNZaqfYs+2JN9R4UUqtwkkKqFxPuSv8ZmX3j9l+B2WADK1L7Gq9danvRHdRNkPe 15 | 77L4IyqkXq4T4GSGjlIhe7uQvZ0WfLXp8vnPN7nYdJKeCh4tDgm8RZ1VwHRPs07s 16 | zcegVUCy0qJNrnUe8Y9AjySR5NYhhVdM0P5Q+XUaAfjUPuQBc8RcfzztiGOLyxaH 17 | VxmPiNWron3+RzY5q99SJ6OraW4bdoK/kt4Oc6liDlqdMlbrc4MtXWh9lz6Hh8qf 18 | ZlFMkcPpWjMfu8cWQHvnQW0kRAveTRCN4+uiTDskMQRw4UoxqjEE/ebFovdxmRvn 19 | dYDS4TyLR3cMP8yQymNY8dtMOXGuuYlqTXPqGp6LgH3mxm3cZ4eEEGmeiGxuhc/w 20 | Elvk2Wltf1SMfhbv0Zk4ONzqrD3TGsyQUA8IjudIJ7OUUziJYqNpYbRg0ETcHKx/ 21 | XgRNaKZHsXusixlX1pki6xKBwHdx7Op5gPaIEYRP2RBZKHoQazP6b+sOHSzyCe+z 22 | 4FSBwejx2Ra2h++vk0LaYCcHphH/9ihvdbw6a9kzB33O12y7Ou/7Lqk4lyJHS7w4 23 | SuKOjwL3QLTsMUGFnwFxpha0Lh6TtptCAIkxjSRwRmc1CV+RZTIvkSyGXBzc0ggz 24 | gDi2FAfU80Mr7Zxuq+5/2OqAr2nhSLy8gr4fnyEm1jrEUoTb7G7BYtwM1PVcYx9O 25 | 7m9hqlXgd0avTiyhaqf5yTL3oPEtlm3/tPRQ4VJK0+leM5hMmmgusd0T+biIc2lm 26 | o97RuPC4eaTXLH6Je4RiUelwICZbl7C1dvdNDy/qm0dEO+3/CkQMpJdJ9bQ18A9Y 27 | Meppade9daU2B8XuRLzXrtcYU+pxEaqT1Pylu3xJa6k/77jU1+JGVyEmgdCK3ayD 28 | lvEQBkPenN8uFBS6p1CkxhT3BzAeTxXaSWUOVLGtbj+YFus39TvauV1Oz8+yQGNc 29 | B6MRDDrgxNJ2QH8/hz92qXAJjrvYgEa/0sL92sKiqU6NNIajkYP7DxDZVOXwgMJU 30 | TamQpIz4854r/VmxLvjA1DUkcIJD912iYs3T0tXM2nJ6oBYb8ha9aFKf5fSM/BhR 31 | zbpSbm54QSjnjTPg61lrchUtAYCi3zhaU3D+5tVIocE573S5el5P6YU6g42Qt+Sj 32 | Y4jeVaDcDiQ/BSiLM1WzEZmNRKa7K664NCKVuk2CeqM7lgX20MNNBMqLNPPK12U0 33 | fjjgBsiy09hOXmSFoJIrvC/ReHe48PR0WvhID29oc8xDdOZjcbdVU+3jQ46YrLty 34 | /emY0TIgJ+v2Ycjj1L5FY379EIRzYS8BBnuDL2iatHPMEqERhAcxlpIXsrt1IJrx 35 | ouGVXQfWRb531noyAn4sZfGoPtg6gZbwuyfPxZpjaQsqXyYanRKVcfA8yKQQJHug 36 | UDjqcF5MwCVFvLEc2aO0qMrauRdOfKtD7I6b0sQ6QrnYYSfaH6u+E3FHcspA/BmQ 37 | 2oAau4UDiOxSHMdm+sPLeZkgDdYUv4n/jVncQUKlRScw9d/pSVbkWJgrgyLeQV4m 38 | E7DPEhoMMO6JrI4Gi17ApvGw4YSCvwx95/geGPNNVFwcNgN4t5zgVMzQbZT1QP0V 39 | cNJlCvcmQoJrimxrvk4LUeJO5pUVRW33By7Z7qhK7ZrAijGjSGkWaeZGyVCxC+WH 40 | P/GLi7nbUhFbrvtk/OkStKkYbDjVMAPM6/YXfKs2dfgl737Xey4lyg0AjjReTaY7 41 | lwM/LNjNtK8DZbc/Pp4cB4b/a/eUJKnT50U7TdyxQkcrsw6a/D7CLjck6hOKGPAE 42 | sKpetoZcwTqx0yEB3yprJ0JiIM7+Eb3cIAlkOCGXSYR/UP7CLTKuN8n3L+KDR5EP 43 | f+drd2armJnMR4KbFYNl3+O8MHOZss+wURFrnEoRRFbEIn2IvJEMbXzZye3XMTBG 44 | 7giTDuaU5B7TIzHJmsEseYw+U+nBAcLX7oRhl+1/zCl+JJAMrIcnnL9P9p/v0tmV 45 | HQweJLMqct7xQI1sLuQDB0cdRkvKcgHhUz4raadAck7zj4hZxruV3SdoHzzdDDaS 46 | f68lE/BLdenNOGiyhYWdE5RySYY8rWid7HKGgWPokY1xV7aEzhqD5+azlWSsKNWL 47 | CWy3Bc4UD2B6Da58EnWD1rvKm+e1WU58IrFPxEHGJGxKbYCvZVc1n6zs1xSPYxiR 48 | FknjHGOY6WAeHbL00kwCgGPv0qoqJBF1n9TofU+b28jcDqPYTJXzE9WqbUpjXNgF 49 | YZb6vxahkYU6SgdgAGA/QHpXdk3neSeiH0j7dO9M6Dl49+4q7wmlZmW+CmfNRrnK 50 | nYCPO/9SHt6EsI0luWWFHg7AAPgXURUGaGUOVLElswsVGLzn0sf+O+JLlZbgxu9j 51 | rLSWcCO4I+V3QT0WJM3nowc9z9NRCDZ0VA/UDi+9e+f06F6rRSARL4s7D1SUf9od 52 | iAu8VP6dTnqV4nxD+q2z6SwKJpIV5oFDK5v2hMyRecxDpQ8BNTJua2jr48wG08gw 53 | Rw/ZiE5kb+/nnztY/GPADMQDiOdZxwaUrrwQ2b2/U/kW 54 | -----END ENCRYPTED PRIVATE KEY----- 55 | -------------------------------------------------------------------------------- /tests/assets/config/deployment.json: -------------------------------------------------------------------------------- 1 | { 2 | "modulesContent": { 3 | "$edgeAgent": { 4 | "properties.desired": { 5 | "schemaVersion": "1.0", 6 | "runtime": { 7 | "type": "docker", 8 | "settings": { 9 | "minDockerVersion": "v1.25", 10 | "loggingOptions": "", 11 | "registryCredentials": { 12 | "ytcontainers": { 13 | "username": "", 14 | "password": "", 15 | "address": "" 16 | } 17 | } 18 | } 19 | }, 20 | "systemModules": { 21 | "edgeAgent": { 22 | "type": "docker", 23 | "settings": { 24 | "image": "mcr.microsoft.com/azureiotedge-agent:1.1", 25 | "createOptions": "{}" 26 | } 27 | }, 28 | "edgeHub": { 29 | "type": "docker", 30 | "status": "running", 31 | "restartPolicy": "always", 32 | "settings": { 33 | "image": "mcr.microsoft.com/azureiotedge-hub:1.1", 34 | "createOptions": "{\"HostConfig\":{\"PortBindings\":{\"5671/tcp\":[{\"HostPort\":\"5671\"}],\"8883/tcp\":[{\"HostPort\":\"8883\"}],\"443/tcp\":[{\"HostPort\":\"443\"}]}}}" 35 | } 36 | } 37 | }, 38 | "modules": { 39 | "testingUtility": { 40 | "version": "1.0", 41 | "type": "docker", 42 | "status": "running", 43 | "restartPolicy": "always", 44 | "settings": { 45 | "image": "", 46 | "createOptions": "{}" 47 | } 48 | } 49 | } 50 | } 51 | }, 52 | "$edgeHub": { 53 | "properties.desired": { 54 | "schemaVersion": "1.0", 55 | "routes": {}, 56 | "storeAndForwardConfiguration": { 57 | "timeToLiveSecs": 7200 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/test_certutils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | import os 5 | import unittest 6 | import shutil 7 | from unittest import mock 8 | from iotedgehubdev.certutils import EdgeCertUtil 9 | from iotedgehubdev.constants import EdgeConstants as EC 10 | from iotedgehubdev.errors import EdgeValueError 11 | 12 | VALID_SUBJECT_DICT = { 13 | EC.SUBJECT_COUNTRY_KEY: 'TC', 14 | EC.SUBJECT_STATE_KEY: 'Test State', 15 | EC.SUBJECT_LOCALITY_KEY: 'Test Locality', 16 | EC.SUBJECT_ORGANIZATION_KEY: 'Test Organization', 17 | EC.SUBJECT_ORGANIZATION_UNIT_KEY: 'Test Unit', 18 | EC.SUBJECT_COMMON_NAME_KEY: 'Test CommonName' 19 | } 20 | 21 | WORKINGDIRECTORY = os.getcwd() 22 | 23 | 24 | class TestEdgeCertUtilAPICreateRootCACert(unittest.TestCase): 25 | 26 | def test_create_root_ca_cert_duplicate_ids_invalid(self): 27 | cert_util = EdgeCertUtil() 28 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 29 | with self.assertRaises(EdgeValueError): 30 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 31 | 32 | def test_create_root_ca_cert_validity_days_invalid(self): 33 | cert_util = EdgeCertUtil() 34 | for validity in [-1, 0, 1096]: 35 | with self.assertRaises(EdgeValueError): 36 | cert_util.create_root_ca_cert('root', 37 | subject_dict=VALID_SUBJECT_DICT, 38 | validity_days_from_now=validity) 39 | 40 | def test_create_root_ca_cert_subject_dict_invalid(self): 41 | cert_util = EdgeCertUtil() 42 | with mock.patch('iotedgehubdev.certutils.EdgeCertUtil.is_valid_certificate_subject', 43 | mock.MagicMock(return_value=False)): 44 | with self.assertRaises(EdgeValueError): 45 | cert_util.create_root_ca_cert('root', 46 | subject_dict=VALID_SUBJECT_DICT) 47 | 48 | def test_create_root_ca_cert_without_subject_dict(self): 49 | cert_util = EdgeCertUtil() 50 | with self.assertRaises(EdgeValueError): 51 | cert_util.create_root_ca_cert('root') 52 | 53 | def test_create_root_ca_cert_passphrase_invalid(self): 54 | cert_util = EdgeCertUtil() 55 | with self.assertRaises(EdgeValueError): 56 | cert_util.create_root_ca_cert('root', 57 | subject_dict=VALID_SUBJECT_DICT, 58 | passphrase='') 59 | with self.assertRaises(EdgeValueError): 60 | cert_util.create_root_ca_cert('root', 61 | subject_dict=VALID_SUBJECT_DICT, 62 | passphrase='123') 63 | bad_pass_1024 = 'a' * 1024 64 | with self.assertRaises(EdgeValueError): 65 | cert_util.create_root_ca_cert('root', 66 | subject_dict=VALID_SUBJECT_DICT, 67 | passphrase=bad_pass_1024) 68 | 69 | 70 | class TestEdgeCertUtilAPICreateIntCACert(unittest.TestCase): 71 | def test_create_intermediate_ca_cert_duplicate_ids_invalid(self): 72 | cert_util = EdgeCertUtil() 73 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 74 | with self.assertRaises(EdgeValueError): 75 | cert_util.create_intermediate_ca_cert('root', 'root', common_name='name') 76 | 77 | def test_create_intermediate_ca_cert_validity_days_invalid(self): 78 | cert_util = EdgeCertUtil() 79 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 80 | for validity in [-1, 0, 1096]: 81 | with self.assertRaises(EdgeValueError): 82 | cert_util.create_intermediate_ca_cert('int', 'root', common_name='name', 83 | validity_days_from_now=validity) 84 | 85 | def test_create_intermediate_ca_cert_passphrase_invalid(self): 86 | cert_util = EdgeCertUtil() 87 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 88 | with self.assertRaises(EdgeValueError): 89 | cert_util.create_intermediate_ca_cert('int', 'root', common_name='name', 90 | passphrase='') 91 | 92 | with self.assertRaises(EdgeValueError): 93 | cert_util.create_intermediate_ca_cert('int', 'root', common_name='name', 94 | passphrase='123') 95 | 96 | bad_pass_1024 = 'a' * 1024 97 | with self.assertRaises(EdgeValueError): 98 | cert_util.create_intermediate_ca_cert('int', 'root', common_name='name', 99 | passphrase=bad_pass_1024) 100 | 101 | def test_create_intermediate_ca_cert_common_name_invalid(self): 102 | cert_util = EdgeCertUtil() 103 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 104 | with self.assertRaises(EdgeValueError): 105 | cert_util.create_intermediate_ca_cert('int', 'root') 106 | 107 | with self.assertRaises(EdgeValueError): 108 | cert_util.create_intermediate_ca_cert('int', 'root', common_name=None) 109 | 110 | with self.assertRaises(EdgeValueError): 111 | cert_util.create_intermediate_ca_cert('int', 'root', common_name='') 112 | 113 | bad_common_name = 'a' * 65 114 | with self.assertRaises(EdgeValueError): 115 | cert_util.create_intermediate_ca_cert('int', 'root', common_name=bad_common_name) 116 | 117 | def test_create_intermediate_ca_cert_successfully(self): 118 | cert_util = EdgeCertUtil() 119 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 120 | 121 | valid_common_name = 'testcommonname' 122 | assert not cert_util.create_intermediate_ca_cert('int', 'root', common_name=valid_common_name) 123 | 124 | 125 | class TestEdgeCertUtilAPICreateServerCert(unittest.TestCase): 126 | def test_create_server_cert_duplicate_ids_invalid(self): 127 | cert_util = EdgeCertUtil() 128 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 129 | with self.assertRaises(EdgeValueError): 130 | cert_util.create_server_cert('root', 'root', host_name='name') 131 | 132 | def test_create_server_cert_validity_days_invalid(self): 133 | cert_util = EdgeCertUtil() 134 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 135 | for validity in [-1, 0, 1096]: 136 | with self.assertRaises(EdgeValueError): 137 | cert_util.create_server_cert('server', 'root', host_name='name', 138 | validity_days_from_now=validity) 139 | 140 | def test_create_server_cert_passphrase_invalid(self): 141 | cert_util = EdgeCertUtil() 142 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 143 | with self.assertRaises(EdgeValueError): 144 | cert_util.create_server_cert('server', 'root', host_name='name', passphrase='') 145 | 146 | with self.assertRaises(EdgeValueError): 147 | cert_util.create_server_cert('server', 'root', host_name='name', passphrase='123') 148 | 149 | bad_pass = 'a' * 1024 150 | with self.assertRaises(EdgeValueError): 151 | cert_util.create_server_cert('server', 'root', host_name='name', passphrase=bad_pass) 152 | 153 | def test_create_server_cert_hostname_invalid(self): 154 | cert_util = EdgeCertUtil() 155 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 156 | with self.assertRaises(EdgeValueError): 157 | cert_util.create_server_cert('int', 'root') 158 | 159 | with self.assertRaises(EdgeValueError): 160 | cert_util.create_server_cert('int', 'root', host_name=None) 161 | 162 | with self.assertRaises(EdgeValueError): 163 | cert_util.create_server_cert('int', 'root', host_name='') 164 | 165 | bad_hostname = 'a' * 65 166 | with self.assertRaises(EdgeValueError): 167 | cert_util.create_server_cert('int', 'root', host_name=bad_hostname) 168 | 169 | def test_create_server_cert_successfully(self): 170 | cert_util = EdgeCertUtil() 171 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 172 | 173 | valid_hostname = 'testhostname' 174 | assert not cert_util.create_server_cert('int', 'root', hostname=valid_hostname) 175 | 176 | 177 | class TestEdgeCertUtilAPIExportCertArtifacts(unittest.TestCase): 178 | 179 | def tearDown(self): 180 | test_data_folder = os.path.join(WORKINGDIRECTORY, 'root') 181 | if os.path.exists(test_data_folder): 182 | shutil.rmtree(test_data_folder) 183 | 184 | @mock.patch('iotedgehubdev.utils.Utils.check_if_directory_exists') 185 | def test_export_cert_artifacts_to_dir_incorrect_id_invalid(self, mock_chk_dir): 186 | cert_util = EdgeCertUtil() 187 | with self.assertRaises(EdgeValueError): 188 | mock_chk_dir.return_value = True 189 | cert_util.export_simulator_cert_artifacts_to_dir('root', 'some_dir') 190 | 191 | @mock.patch('iotedgehubdev.utils.Utils.check_if_directory_exists') 192 | def test_export_cert_artifacts_to_dir_invalid_dir_invalid(self, mock_chk_dir): 193 | cert_util = EdgeCertUtil() 194 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 195 | with self.assertRaises(EdgeValueError): 196 | mock_chk_dir.return_value = False 197 | cert_util.export_simulator_cert_artifacts_to_dir('root', 'some_dir') 198 | 199 | def test_get_cert_artifacts_file_path(self): 200 | cert_util = EdgeCertUtil() 201 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 202 | cert_util.export_simulator_cert_artifacts_to_dir('root', WORKINGDIRECTORY) 203 | assert cert_util.get_cert_file_path('root', WORKINGDIRECTORY) 204 | 205 | def test_get_chain_ca_certs(self): 206 | cert_util = EdgeCertUtil() 207 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 208 | cert_util.chain_simulator_ca_certs('root', {'root'}, WORKINGDIRECTORY) 209 | assert cert_util.get_cert_file_path('root', WORKINGDIRECTORY) 210 | 211 | def test_get_pfx_cert_file_path(self): 212 | cert_util = EdgeCertUtil() 213 | cert_util.create_root_ca_cert('root', subject_dict=VALID_SUBJECT_DICT) 214 | cert_util.chain_simulator_ca_certs('root', {'root'}, WORKINGDIRECTORY) 215 | cert_util.export_pfx_cert('root', WORKINGDIRECTORY) 216 | assert cert_util.get_cert_file_path('root', WORKINGDIRECTORY) 217 | -------------------------------------------------------------------------------- /tests/test_compose_resources/deployment.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleContent": { 3 | "$edgeAgent": { 4 | "properties.desired": { 5 | "schemaVersion": "1.0", 6 | "runtime": { 7 | "type": "docker", 8 | "settings": { 9 | "minDockerVersion": "v1.25", 10 | "loggingOptions": "", 11 | "registryCredentials": {} 12 | } 13 | }, 14 | "systemModules": { 15 | "edgeAgent": { 16 | "type": "docker", 17 | "settings": { 18 | "image": "mcr.microsoft.com/azureiotedge-agent:1.1", 19 | "createOptions": "" 20 | } 21 | }, 22 | "edgeHub": { 23 | "type": "docker", 24 | "status": "running", 25 | "restartPolicy": "always", 26 | "settings": { 27 | "image": "mcr.microsoft.com/azureiotedge-hub:1.1", 28 | "createOptions": "{\"HostConfig\":{\"PortBindings\":{\"8883/tcp\":[{\"HostPort\":\"8883\"}],\"443/tcp\":[{\"HostPort\":\"443\"}]}}}" 29 | }, 30 | "env": { 31 | "OptimizeForPerformance": { 32 | "value": "false" 33 | } 34 | } 35 | } 36 | }, 37 | "modules": { 38 | "tempSensor": { 39 | "version": "1.0", 40 | "type": "docker", 41 | "status": "running", 42 | "restartPolicy": "on-failure", 43 | "settings": { 44 | "image": "mcr.microsoft.com/azureiotedge-simulated-temperature-sensor:1.0", 45 | "createOptions": "" 46 | } 47 | }, 48 | "SampleModule": { 49 | "version": "1.0", 50 | "type": "docker", 51 | "status": "running", 52 | "restartPolicy": "on-unhealthy", 53 | "settings": { 54 | "image": "localhost:5000/samplemodule:0.0.1-amd64", 55 | "createOptions": "{\"Env\":[\"FOO=bar\",\"BAZ=quux\",\"test1\"]}" 56 | }, 57 | "env":{ 58 | "FOO":{ 59 | "value":"rab" 60 | }, 61 | "BAR":{ 62 | "value":"rua" 63 | }, 64 | "test2":{} 65 | } 66 | } 67 | } 68 | } 69 | }, 70 | "$edgeHub": { 71 | "properties.desired": { 72 | "schemaVersion": "1.0", 73 | "routes": { 74 | "sensorToSampleModule": "FROM /messages/modules/tempSensor/outputs/temperatureOutput INTO BrokeredEndpoint(\"/modules/SampleModule/inputs/input1\")", 75 | "SampleModuleToIoTHub": "FROM /messages/modules/SampleModule/outputs/output1 INTO $upstream" 76 | }, 77 | "storeAndForwardConfiguration": { 78 | "timeToLiveSecs": 7200 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/test_compose_resources/deployment_with_chunked_create_options.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleContent": { 3 | "$edgeAgent": { 4 | "properties.desired": { 5 | "schemaVersion": "1.0", 6 | "runtime": { 7 | "type": "docker", 8 | "settings": { 9 | "minDockerVersion": "v1.25", 10 | "loggingOptions": "", 11 | "registryCredentials": {} 12 | } 13 | }, 14 | "systemModules": { 15 | "edgeAgent": { 16 | "type": "docker", 17 | "settings": { 18 | "image": "mcr.microsoft.com/azureiotedge-agent:1.1", 19 | "createOptions": "" 20 | } 21 | }, 22 | "edgeHub": { 23 | "type": "docker", 24 | "status": "running", 25 | "settings": { 26 | "image": "mcr.microsoft.com/azureiotedge-hub:1.1", 27 | "createOptions": "{\"HostConfig\":{\"PortBindings\":", 28 | "createOptions01": "{\"8883/tcp\":[{\"HostPort\":\"8883\"}],\"443/tcp\":[{\"HostPort\":\"443\"}]}}}" 29 | } 30 | } 31 | }, 32 | "modules": { 33 | "tempSensor": { 34 | "version": "1.0", 35 | "type": "docker", 36 | "status": "running", 37 | "restartPolicy": "never", 38 | "settings": { 39 | "image": "mcr.microsoft.com/azureiotedge-simulated-temperature-sensor:1.0", 40 | "createOptions": "{\r\n \"Hostname\": \"\",\r\n \"Domainname\": \"\",\r\n \"User\": \"\",\r\n \"AttachStdin\": false,\r\n \"AttachStdout\"", 41 | "createOptions01": ": true,\r\n \"AttachStderr\": true,\r\n \"Tty\": false,\r\n \"OpenStdin\": false,\r\n \"StdinOnce\": false,\r\n \"Env\"", 42 | "createOptions02": ": [\r\n \"FOO=bar\",\r\n \"BAZ=quux\"\r\n ],\r\n \"Cmd\": [\r\n \"date\"\r\n ],\r\n \"Entrypoint\": \"\",\r\n ", 43 | "createOptions03": "\"Image\": \"ubuntu\",\r\n \"Labels\": {\r\n \"com.example.vendor\": \"Acme\",\r\n \"com.example.license\": \"GPL\",\r", 44 | "createOptions04": "\n \"com.example.version\": \"1.0\"\r\n },\r\n \"Volumes\": {\r\n \"/volumes/data\": {}\r\n },\r\n \"WorkingDir\": \"", 45 | "createOptions05": "\",\r\n \"NetworkDisabled\": false,\r\n \"MacAddress\": \"12:34:56:78:9a:bc\",\r\n \"ExposedPorts\": {\r\n \"22/tcp\": {}\r", 46 | "createOptions06": "\n },\r\n\t\"Healthcheck\": {\r\n\t\"Test\": [\r\n\t\"string\"\r\n\t],\r\n\t\"Interval\": 0,\r\n\t\"Timeout\": 0,\r\n\t\"Retries\"", 47 | "createOptions07": ": 0,\r\n\t\"StartPeriod\": 0\r\n\t},\r\n \"StopSignal\": \"SIGTERM\",\r\n \"StopTimeout\": 10,\r\n \"HostConfig\": {\r\n \"Binds\": [\r\n \"/tmp:/tmp\"\r\n ],\r\n \"Links\": [\r\n \"redis3:redis\"\r\n ],\r\n \"Memory\": 0,\r\n \"MemorySwap\": 0,\r\n \"MemoryReservation\": 0,\r\n \"KernelMemory\": 0,\r\n \"NanoCPUs\": 500000,\r\n \"CpuPercent\": 80,\r\n \"CpuShares\": 512,\r\n \"CpuPeriod\": 100000,\r\n \"CpuRealtimePeriod\": 1000000,\r\n \"CpuRealtimeRuntime\": 10000,\r\n \"CpuQuota\": 50000,\r\n \"CpusetCpus\": \"0,1\",\r\n \"CpusetMems\": \"0,1\",\r\n \"MaximumIOps\": 0,\r\n \"MaximumIOBps\": 0,\r\n \"BlkioWeight\": 300,\r\n \"BlkioWeightDevice\": [\r\n {}\r\n ],\r\n \"BlkioDeviceReadBps\": [\r\n {}\r\n ],\r\n \"BlkioDeviceReadIOps\": [\r\n {}\r\n ],\r\n \"BlkioDeviceWriteBps\": [\r\n {}\r\n ],\r\n \"BlkioDeviceWriteIOps\": [\r\n {}\r\n ],\r\n \"MemorySwappiness\": 60,\r\n \"OomKillDisable\": false,\r\n \"OomScoreAdj\": 500,\r\n \"PidMode\": \"\",\r\n \"PidsLimit\": -1,\r\n \"PortBindings\": {\r\n \"22/tcp\": [\r\n {\r\n \"HostPort\": \"11022\"\r\n }\r\n ]\r\n },\r\n \"PublishAllPorts\": false,\r\n \"Privileged\": false,\r\n \"ReadonlyRootfs\": false,\r\n \"Dns\": [\r\n \"8.8.8.8\"\r\n ],\r\n \"DnsOptions\": [\r\n \"\"\r\n ],\r\n \"DnsSearch\": [\r\n \"\"\r\n ],\r\n \"VolumesFrom\": [\r\n \"parent\",\r\n \"other:ro\"\r\n ],\r\n \"CapAdd\": [\r\n \"NET_ADMIN\"\r\n ],\r\n \"CapDrop\": [\r\n \"MKNOD\"\r\n ],\r\n \"GroupAdd\": [\r\n \"newgroup\"\r\n ],\r\n \"RestartPolicy\": {\r\n \"Name\": \"\",\r\n \"MaximumRetryCount\": 0\r\n },\r\n \"AutoRemove\": true,\r\n \"NetworkMode\": \"bridge\",\r\n \"Devices\": [],\r\n \"LogConfig\": {\r\n \"Type\": \"json-file\",\r\n \"Config\": {}\r\n },\r\n \"SecurityOpt\": [],\r\n \"StorageOpt\": {},\r\n \"CgroupParent\": \"\",\r\n \"VolumeDriver\": \"\",\r\n \"ShmSize\": 67108864\r\n },\r\n \"NetworkingConfig\": {\r\n \"EndpointsConfig\": {\r\n \"isolated_nw\": {\r\n \"IPAMConfig\": {\r\n \"IPv4Address\": \"172.20.30.33\",\r\n \"IPv6Address\": \"2001:db8:abcd::3033\",\r\n \"LinkLocalIPs\": [\r\n \"169.254.34.68\",\r\n \"fe80::3468\"\r\n ]\r\n },\r\n \"Links\": [\r\n \"container_1\",\r\n \"container_2\"\r\n ],\r\n \"Aliases\": [\r\n \"server_x\",\r\n \"server_y\"\r\n ]\r\n }\r\n }\r\n }\r\n}" 48 | } 49 | }, 50 | "SampleModule": { 51 | "version": "1.0", 52 | "type": "docker", 53 | "status": "running", 54 | "restartPolicy": "unknown", 55 | "settings": { 56 | "image": "localhost:5000/samplemodule:0.0.1-amd64", 57 | "createOptions": "" 58 | } 59 | } 60 | } 61 | } 62 | }, 63 | "$edgeHub": { 64 | "properties.desired": { 65 | "schemaVersion": "1.0", 66 | "routes": { 67 | "sensorToSampleModule": "FROM /messages/modules/tempSensor/outputs/temperatureOutput INTO BrokeredEndpoint(\"/modules/SampleModule/inputs/input1\")", 68 | "SampleModuleToIoTHub": "FROM /messages/modules/SampleModule/outputs/output1 INTO $upstream" 69 | }, 70 | "storeAndForwardConfiguration": { 71 | "timeToLiveSecs": 7200 72 | } 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/test_compose_resources/deployment_with_create_options.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleContent": { 3 | "$edgeAgent": { 4 | "properties.desired": { 5 | "schemaVersion": "1.0", 6 | "runtime": { 7 | "type": "docker", 8 | "settings": { 9 | "minDockerVersion": "v1.25", 10 | "loggingOptions": "", 11 | "registryCredentials": {} 12 | } 13 | }, 14 | 15 | "systemModules": { 16 | "edgeAgent": { 17 | "type": "docker", 18 | "settings": { 19 | "image": "mcr.microsoft.com/azureiotedge-agent:1.1", 20 | "createOptions": "" 21 | } 22 | }, 23 | 24 | "edgeHub": { 25 | "type": "docker", 26 | "status": "running", 27 | "restartPolicy": "always", 28 | "settings": { 29 | "image": "mcr.microsoft.com/azureiotedge-hub:1.1", 30 | "createOptions": "{\"HostConfig\":{\"PortBindings\":", 31 | "createOptions01": "{\"8883/tcp\":[{\"HostPort\":\"8883\"}],\"443/tcp\":[{\"HostPort\":\"443\"}]}}}" 32 | } 33 | } 34 | }, 35 | 36 | "modules": { 37 | "tempSensor": { 38 | "version": "1.0", 39 | "type": "docker", 40 | "status": "running", 41 | "restartPolicy": "always", 42 | "settings": { 43 | "image": "mcr.microsoft.com/azureiotedge-simulated-temperature-sensor:1.0", 44 | "createOptions": "{\"Env\"", 45 | "createOptions01": ": [\r\n \"FOO1=bar1\",", 46 | "createOptions02": "\"FOO2=bar2\",\r\n", 47 | "createOptions03": "\"FOO3=bar3\",\r\n", 48 | "createOptions04": "\"FOO4=bar4\",\r\n", 49 | "createOptions05": "\"FOO5=bar5\",\r\n", 50 | "createOptions06": "\"FOO6=bar6\",\r\n", 51 | "createOptions07": "\"FOO7=bar7\"\r\n]}" 52 | } 53 | } 54 | } 55 | } 56 | }, 57 | 58 | "$edgeHub": { 59 | "properties.desired": { 60 | "schemaVersion": "1.0", 61 | "routes": { 62 | "sensorToIoTHub": "FROM /messages/modules/tempSensor/outputs/output1 INTO $upstream" 63 | }, 64 | "storeAndForwardConfiguration": { 65 | "timeToLiveSecs": 7200 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/test_compose_resources/deployment_with_create_options_for_bind.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleContent": { 3 | "$edgeAgent": { 4 | "properties.desired": { 5 | "schemaVersion": "1.0", 6 | "runtime": { 7 | "type": "docker", 8 | "settings": { 9 | "minDockerVersion": "v1.25", 10 | "loggingOptions": "", 11 | "registryCredentials": {} 12 | } 13 | }, 14 | 15 | "systemModules": { 16 | "edgeAgent": { 17 | "type": "docker", 18 | "settings": { 19 | "image": "mcr.microsoft.com/azureiotedge-agent:1.1", 20 | "createOptions": "" 21 | } 22 | }, 23 | 24 | "edgeHub": { 25 | "type": "docker", 26 | "status": "running", 27 | "restartPolicy": "always", 28 | "settings": { 29 | "image": "mcr.microsoft.com/azureiotedge-hub:1.1", 30 | "createOptions": "{\"HostConfig\":{\"PortBindings\":", 31 | "createOptions01": "{\"8883/tcp\":[{\"HostPort\":\"8883\"}],\"443/tcp\":[{\"HostPort\":\"443\"}]}}}" 32 | } 33 | } 34 | }, 35 | 36 | "modules": { 37 | "tempSensor": { 38 | "version": "1.0", 39 | "type": "docker", 40 | "status": "running", 41 | "restartPolicy": "always", 42 | "settings": { 43 | "image": "mcr.microsoft.com/azureiotedge-simulated-temperature-sensor:1.0", 44 | "createOptions": "{\"HostConfig\":{\"Binds\":[\"/usr:/home/moduleuser/usr\",\"/run:/home/moduleuser/run\"]}}" 45 | } 46 | } 47 | } 48 | } 49 | }, 50 | 51 | "$edgeHub": { 52 | "properties.desired": { 53 | "schemaVersion": "1.0", 54 | "routes": { 55 | "sensorToIoTHub": "FROM /messages/modules/tempSensor/outputs/output1 INTO $upstream" 56 | }, 57 | "storeAndForwardConfiguration": { 58 | "timeToLiveSecs": 7200 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/test_compose_resources/deployment_with_custom_volume.json: -------------------------------------------------------------------------------- 1 | { 2 | "modulesContent": { 3 | "$edgeAgent": { 4 | "properties.desired": { 5 | "schemaVersion": "1.0", 6 | "runtime": { 7 | "type": "docker", 8 | "settings": { 9 | "minDockerVersion": "v1.25", 10 | "loggingOptions": "", 11 | "registryCredentials": {} 12 | } 13 | }, 14 | 15 | "systemModules": { 16 | "edgeAgent": { 17 | "type": "docker", 18 | "settings": { 19 | "image": "mcr.microsoft.com/azureiotedge-agent:1.1", 20 | "createOptions": "{}" 21 | } 22 | }, 23 | 24 | "edgeHub": { 25 | "type": "docker", 26 | "status": "running", 27 | "restartPolicy": "always", 28 | "settings": { 29 | "image": "mcr.microsoft.com/azureiotedge-hub:1.1", 30 | "createOptions": "{\"HostConfig\":{\"PortBindings\":{\"5671/tcp\":[{\"HostPort\":\"5671\"}], \"8883/tcp\":[{\"HostPort\":\"8883\"}],\"443/tcp\":[{\"HostPort\":\"443\"}]}}}" 31 | } 32 | } 33 | }, 34 | 35 | "modules": { 36 | "tempSensor": { 37 | "version": "1.0", 38 | "type": "docker", 39 | "status": "running", 40 | "restartPolicy": "always", 41 | "settings": { 42 | "image": "mcr.microsoft.com/azureiotedge-simulated-temperature-sensor:1.0", 43 | "createOptions": "{\"HostConfig\": {\"Mounts\": [{\"Target\": \"/mnt_test\",\"Source\": \"testVolume\",\"Type\": \"volume\"}]}}" 44 | } 45 | } 46 | } 47 | } 48 | }, 49 | 50 | "$edgeHub": { 51 | "properties.desired": { 52 | "schemaVersion": "1.0", 53 | "routes": { 54 | "tempSensorToIoTHub": "FROM /messages/modules/tempSensor/outputs/* INTO $upstream" 55 | }, 56 | 57 | "storeAndForwardConfiguration": { 58 | "timeToLiveSecs": 7200 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/test_compose_resources/deployment_without_custom_module.json: -------------------------------------------------------------------------------- 1 | { 2 | "modulesContent": { 3 | "$edgeAgent": { 4 | "properties.desired": { 5 | "schemaVersion": "1.0", 6 | "runtime": { 7 | "type": "docker", 8 | "settings": { 9 | "minDockerVersion": "v1.25", 10 | "loggingOptions": "", 11 | "registryCredentials": {} 12 | } 13 | }, 14 | 15 | "systemModules": { 16 | "edgeAgent": { 17 | "type": "docker", 18 | "settings": { 19 | "image": "mcr.microsoft.com/azureiotedge-agent:1.1", 20 | "createOptions": "{}" 21 | } 22 | }, 23 | 24 | "edgeHub": { 25 | "type": "docker", 26 | "status": "running", 27 | "restartPolicy": "always", 28 | "settings": { 29 | "image": "mcr.microsoft.com/azureiotedge-hub:1.1", 30 | "createOptions": "{\"HostConfig\":{\"PortBindings\":{\"5671/tcp\":[{\"HostPort\":\"5671\"}], \"8883/tcp\":[{\"HostPort\":\"8883\"}],\"443/tcp\":[{\"HostPort\":\"443\"}]}}}" 31 | } 32 | } 33 | }, 34 | 35 | "modules": { 36 | "tempSensor": { 37 | "version": "1.0", 38 | "type": "docker", 39 | "status": "running", 40 | "restartPolicy": "always", 41 | "settings": { 42 | "image": "mcr.microsoft.com/azureiotedge-simulated-temperature-sensor:1.0", 43 | "createOptions": "{}" 44 | } 45 | } 46 | } 47 | } 48 | }, 49 | 50 | "$edgeHub": { 51 | "properties.desired": { 52 | "schemaVersion": "1.0", 53 | "routes": { 54 | "tempSensorToIoTHub": "FROM /messages/modules/tempSensor/outputs/* INTO $upstream" 55 | }, 56 | 57 | "storeAndForwardConfiguration": { 58 | "timeToLiveSecs": 7200 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/test_compose_resources/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | services: 3 | edgeHubDev: 4 | container_name: edgeHubDev 5 | environment: 6 | - OptimizeForPerformance=false 7 | - routes__r1=FROM /messages/modules/tempSensor/outputs/temperatureOutput INTO 8 | BrokeredEndpoint("/modules/SampleModule/inputs/input1") 9 | - routes__r2=FROM /messages/modules/SampleModule/outputs/output1 INTO $$upstream 10 | - IotHubConnectionString=HostName=HostName;DeviceId=DeviceId;ModuleId=$$edgeHub;SharedAccessKey=SharedAccessKey 11 | - EdgeModuleHubServerCAChainCertificateFile=/mnt/edgehub/edge-chain-ca.cert.pem 12 | - EdgeModuleHubServerCertificateFile=/mnt/edgehub/edge-hub-server.cert.pfx 13 | - configSource=local 14 | - SSL_CERTIFICATE_PATH=/mnt/edgehub/ 15 | - SSL_CERTIFICATE_NAME=edge-hub-server.cert.pfx 16 | image: mcr.microsoft.com/azureiotedge-hub:1.1 17 | labels: 18 | iotedgehubdev: '' 19 | networks: 20 | azure-iot-edge-dev: 21 | aliases: 22 | - gatewayhost 23 | ports: 24 | - 8883:8883/tcp 25 | - 443:443/tcp 26 | restart: always 27 | volumes: 28 | - source: edgehubdev 29 | target: /mnt/edgehub 30 | type: volume 31 | tempSensor: 32 | container_name: tempSensor 33 | depends_on: 34 | - edgeHubDev 35 | environment: 36 | - EdgeModuleCACertificateFile=/mnt/edgemodule/edge-device-ca.cert.pem 37 | - EdgeHubConnectionString=HostName=HostName;DeviceId=DeviceId;ModuleId=tempSensor;SharedAccessKey=SharedAccessKey 38 | image: mcr.microsoft.com/azureiotedge-simulated-temperature-sensor:1.0 39 | labels: 40 | iotedgehubdev: '' 41 | networks: 42 | azure-iot-edge-dev: null 43 | restart: on-failure 44 | volumes: 45 | - source: edgemoduledev 46 | target: /mnt/edgemodule 47 | type: volume 48 | SampleModule: 49 | container_name: SampleModule 50 | depends_on: 51 | - edgeHubDev 52 | environment: 53 | - FOO=rab 54 | - BAZ=quux 55 | - test1= 56 | - BAR=rua 57 | - test2= 58 | - EdgeModuleCACertificateFile=/mnt/edgemodule/edge-device-ca.cert.pem 59 | - EdgeHubConnectionString=HostName=HostName;DeviceId=DeviceId;ModuleId=SampleModule;SharedAccessKey=SharedAccessKey 60 | image: localhost:5000/samplemodule:0.0.1-amd64 61 | labels: 62 | iotedgehubdev: '' 63 | networks: 64 | azure-iot-edge-dev: null 65 | restart: always 66 | volumes: 67 | - source: edgemoduledev 68 | target: /mnt/edgemodule 69 | type: volume 70 | networks: 71 | azure-iot-edge-dev: 72 | external: true 73 | volumes: 74 | edgehubdev: 75 | name: edgehubdev 76 | edgemoduledev: 77 | name: edgemoduledev 78 | -------------------------------------------------------------------------------- /tests/test_compose_resources/docker-compose_with_chunked_create_options.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | services: 3 | tempSensor: 4 | cap_add: 5 | - NET_ADMIN 6 | cap_drop: 7 | - MKNOD 8 | cgroup_parent: '' 9 | command: date 10 | container_name: tempSensor 11 | depends_on: 12 | - edgeHubDev 13 | devices: [] 14 | dns: 15 | - 8.8.8.8 16 | dns_search: 17 | - '' 18 | domainname: '' 19 | entrypoint: '' 20 | environment: 21 | - FOO=bar 22 | - BAZ=quux 23 | - EdgeModuleCACertificateFile=/mnt/edgemodule/edge-device-ca.cert.pem 24 | - EdgeHubConnectionString=HostName=HostName;DeviceId=DeviceId;ModuleId=tempSensor;SharedAccessKey=SharedAccessKey 25 | expose: 26 | - 22/tcp 27 | healthcheck: 28 | interval: 0ms 29 | retries: 0 30 | start_period: 0ms 31 | test: 32 | - string 33 | timeout: 0ms 34 | hostname: '' 35 | image: mcr.microsoft.com/azureiotedge-simulated-temperature-sensor:1.0 36 | labels: 37 | com.example.license: GPL 38 | com.example.vendor: Acme 39 | com.example.version: '1.0' 40 | iotedgehubdev: '' 41 | logging: 42 | driver: json-file 43 | options: {} 44 | mac_address: 12:34:56:78:9a:bc 45 | networks: 46 | isolated_nw: 47 | aliases: 48 | - server_x 49 | - server_y 50 | ipv4_address: 172.20.30.33 51 | ipv6_address: 2001:db8:abcd::3033 52 | pid: '' 53 | ports: 54 | - 11022:22/tcp 55 | privileged: false 56 | read_only: false 57 | restart: 'no' 58 | security_opt: [] 59 | stop_grace_period: 10s 60 | stop_signal: SIGTERM 61 | tty: false 62 | user: '' 63 | volumes: 64 | - source: edgemoduledev 65 | target: /mnt/edgemodule 66 | type: volume 67 | - source: /tmp 68 | target: /tmp 69 | type: bind 70 | working_dir: '' 71 | edgeHubDev: 72 | container_name: edgeHubDev 73 | environment: 74 | - routes__r1=FROM /messages/modules/SampleModule/outputs/output1 INTO $$upstream 75 | - routes__r2=FROM /messages/modules/tempSensor/outputs/temperatureOutput INTO 76 | BrokeredEndpoint("/modules/SampleModule/inputs/input1") 77 | - IotHubConnectionString=HostName=HostName;DeviceId=DeviceId;ModuleId=$$edgeHub;SharedAccessKey=SharedAccessKey 78 | - EdgeModuleHubServerCAChainCertificateFile=/mnt/edgehub/edge-chain-ca.cert.pem 79 | - EdgeModuleHubServerCertificateFile=/mnt/edgehub/edge-hub-server.cert.pfx 80 | - configSource=local 81 | - SSL_CERTIFICATE_PATH=/mnt/edgehub/ 82 | - SSL_CERTIFICATE_NAME=edge-hub-server.cert.pfx 83 | image: mcr.microsoft.com/azureiotedge-hub:1.1 84 | labels: 85 | iotedgehubdev: '' 86 | networks: 87 | azure-iot-edge-dev: 88 | aliases: 89 | - gatewayhost 90 | ports: 91 | - 8883:8883/tcp 92 | - 443:443/tcp 93 | restart: always 94 | volumes: 95 | - source: edgehubdev 96 | target: /mnt/edgehub 97 | type: volume 98 | SampleModule: 99 | container_name: SampleModule 100 | depends_on: 101 | - edgeHubDev 102 | environment: 103 | - EdgeModuleCACertificateFile=/mnt/edgemodule/edge-device-ca.cert.pem 104 | - EdgeHubConnectionString=HostName=HostName;DeviceId=DeviceId;ModuleId=SampleModule;SharedAccessKey=SharedAccessKey 105 | image: localhost:5000/samplemodule:0.0.1-amd64 106 | labels: 107 | iotedgehubdev: '' 108 | networks: 109 | azure-iot-edge-dev: null 110 | restart: 'no' 111 | volumes: 112 | - source: edgemoduledev 113 | target: /mnt/edgemodule 114 | type: volume 115 | networks: 116 | azure-iot-edge-dev: 117 | external: true 118 | isolated_nw: 119 | external: true 120 | volumes: 121 | edgehubdev: 122 | name: edgehubdev 123 | edgemoduledev: 124 | name: edgemoduledev 125 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | import os 6 | import unittest 7 | from iotedgehubdev import configs 8 | from iotedgehubdev.hostplatform import HostPlatform 9 | 10 | 11 | class TestGetIniConfig(unittest.TestCase): 12 | @classmethod 13 | def cleanup(cls): 14 | iniFile = HostPlatform.get_setting_ini_path() 15 | if os.path.exists(iniFile): 16 | os.remove(iniFile) 17 | 18 | @classmethod 19 | def update_setting_ini_as_firsttime(cls): 20 | config = configs._prod_config.config 21 | config.set('DEFAULT', 'firsttime', 'yes') 22 | configs._prod_config.update_config() 23 | 24 | @classmethod 25 | def setUpClass(cls): 26 | cls.update_setting_ini_as_firsttime() 27 | 28 | @classmethod 29 | def tearDownClass(cls): 30 | cls.cleanup() 31 | 32 | def test(self): 33 | from iotedgehubdev import configs 34 | iniConfig = configs.get_ini_config() 35 | self.assertEqual(iniConfig.get('DEFAULT', 'firsttime'), 'yes') 36 | 37 | 38 | class TestCoreTelemetry(unittest.TestCase): 39 | def test_suppress_all_exceptions(self): 40 | self._impl(Exception, 'fallback') 41 | self._impl(Exception, None) 42 | self._impl(ImportError, 'fallback_for_import_error') 43 | self._impl(None, None) 44 | 45 | def _impl(self, exception_to_raise, fallback_return): 46 | from iotedgehubdev.decorators import suppress_all_exceptions 47 | 48 | @suppress_all_exceptions(fallback_return=fallback_return) 49 | def _error_fn(): 50 | if not exception_to_raise: 51 | return 'positive result' 52 | else: 53 | raise exception_to_raise() 54 | 55 | if not exception_to_raise: 56 | self.assertEqual(_error_fn(), 'positive result') 57 | else: 58 | self.assertEqual(_error_fn(), fallback_return) 59 | -------------------------------------------------------------------------------- /tests/test_connectionstr.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | import pytest 6 | from iotedgehubdev.constants import EdgeConstants as EC 7 | from iotedgehubdev.utils import Utils 8 | 9 | empty_string = "" 10 | valid_connectionstring = "HostName=testhub.azure-devices.net;DeviceId=testdevice;SharedAccessKey=othergibberish=" 11 | invalid_connectionstring = "HostName=testhub.azure-devices.net;SharedAccessKey=othergibberish=" 12 | 13 | 14 | def test_empty_connectionstring(): 15 | with pytest.raises(KeyError): 16 | connection_str_dict = Utils.parse_connection_strs(empty_string) 17 | assert not connection_str_dict 18 | 19 | 20 | def test_valid_connectionstring(): 21 | connection_str_dict = Utils.parse_connection_strs(valid_connectionstring) 22 | assert connection_str_dict[EC.HOSTNAME_KEY] == "testhub.azure-devices.net" 23 | assert connection_str_dict[EC.DEVICE_ID_KEY] == "testdevice" 24 | assert connection_str_dict[EC.DEVICE_ACCESS_KEY_KEY] == "othergibberish=" 25 | 26 | 27 | def test_invalid_connectionstring(): 28 | with pytest.raises(KeyError): 29 | connection_str_dict = Utils.parse_connection_strs(empty_string) 30 | assert not connection_str_dict 31 | -------------------------------------------------------------------------------- /tests/test_edgecert.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | import os 5 | import shutil 6 | import unittest 7 | from iotedgehubdev.edgecert import EdgeCert 8 | 9 | WORKINGDIRECTORY = os.getcwd() 10 | 11 | 12 | class TestEdgeCertAPICreateSelfSignedCerts(unittest.TestCase): 13 | 14 | def tearDown(self): 15 | self._delete_directory(WORKINGDIRECTORY, 'edge-agent-ca') 16 | self._delete_directory(WORKINGDIRECTORY, 'edge-chain-ca') 17 | self._delete_directory(WORKINGDIRECTORY, 'edge-device-ca') 18 | self._delete_directory(WORKINGDIRECTORY, 'edge-hub-server') 19 | 20 | def _delete_directory(self, folder_path, foldername): 21 | data_path = os.path.join(folder_path, foldername) 22 | if os.path.exists(data_path): 23 | shutil.rmtree(data_path) 24 | 25 | def test_get_self_signed_certs(self): 26 | edge_cert = EdgeCert(WORKINGDIRECTORY, 'testhostname') 27 | edge_cert.generate_self_signed_certs() 28 | assert edge_cert.get_cert_file_path('edge-agent-ca') 29 | assert edge_cert.get_cert_file_path('edge-chain-ca') 30 | assert edge_cert.get_cert_file_path('edge-device-ca') 31 | assert edge_cert.get_cert_file_path('edge-hub-server') 32 | assert edge_cert.get_pfx_file_path('edge-hub-server') 33 | -------------------------------------------------------------------------------- /tests/test_edgedockerclient_int.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | import subprocess 5 | import time 6 | import unittest 7 | import docker 8 | from iotedgehubdev.errors import EdgeDeploymentError 9 | from iotedgehubdev.edgedockerclient import EdgeDockerClient 10 | 11 | 12 | class TestEdgeDockerClientSmoke(unittest.TestCase): 13 | IMAGE_NAME = 'mcr.microsoft.com/dotnet/core/runtime:3.1' 14 | NETWORK_NAME = 'ctl_int_test_network' 15 | CONTAINER_NAME = 'ctl_int_test_container' 16 | VOLUME_NAME = 'ctl_int_int_test_mnt' 17 | LABEL_NAME = 'ctl_int_test_label' 18 | 19 | def test_get_os_type(self): 20 | with EdgeDockerClient() as client: 21 | exception_raised = False 22 | try: 23 | os_type = client.get_os_type() 24 | except EdgeDeploymentError: 25 | exception_raised = True 26 | self.assertFalse(exception_raised) 27 | permitted_os_types = ['windows', 'linux'] 28 | self.assertIn(os_type, permitted_os_types) 29 | 30 | def test_pull(self): 31 | with EdgeDockerClient() as client: 32 | exception_raised = False 33 | image_name = self.IMAGE_NAME 34 | try: 35 | local_sha_1 = client.get_local_image_sha_id(image_name) 36 | is_updated = client.pull(image_name, None, None) 37 | local_sha_2 = client.get_local_image_sha_id(image_name) 38 | if is_updated: 39 | self.assertNotEqual(local_sha_1, local_sha_2) 40 | else: 41 | self.assertEqual(local_sha_1, local_sha_2) 42 | except EdgeDeploymentError: 43 | exception_raised = True 44 | self.assertFalse(exception_raised) 45 | 46 | def _create_container(self, client): 47 | image_name = self.IMAGE_NAME 48 | os_type = client.get_os_type().lower() 49 | if os_type == 'linux': 50 | volume_path = '/{0}'.format(self.VOLUME_NAME) 51 | script = 'sleep 20s' 52 | elif os_type == 'windows': 53 | volume_path = 'c:/{0}'.format(self.VOLUME_NAME) 54 | script = 'ping -n 20 127.0.0.1 > nul' 55 | env_dict = {} 56 | env_dict['TEST_VOLUME_NAME'] = self.VOLUME_NAME 57 | client.pull(image_name, None, None) 58 | client.create_network(self.NETWORK_NAME) 59 | client.create_volume(self.VOLUME_NAME) 60 | host_config = client.create_host_config( 61 | mounts=[docker.types.Mount(volume_path, self.VOLUME_NAME)] 62 | ) 63 | network_config = client.create_config_for_network(self.NETWORK_NAME) 64 | client.create_container( 65 | image_name, 66 | name=self.CONTAINER_NAME, 67 | networking_config=network_config, 68 | host_config=host_config, 69 | volumes=[volume_path], 70 | environment=env_dict, 71 | command=script) 72 | client.copy_file_to_volume(self.CONTAINER_NAME, 73 | self.VOLUME_NAME, 74 | 'test_file_name.txt', 75 | volume_path, 76 | __file__) 77 | 78 | def _destroy_container(self, client): 79 | client.stop(self.CONTAINER_NAME) 80 | time.sleep(5) 81 | status = client.status(self.CONTAINER_NAME) 82 | self.assertEqual('exited', status) 83 | client.remove(self.CONTAINER_NAME) 84 | status = client.status(self.CONTAINER_NAME) 85 | self.assertIsNone(status) 86 | client.remove_volume(self.VOLUME_NAME) 87 | client.destroy_network(self.NETWORK_NAME) 88 | 89 | def _reset_host_network(self, client): 90 | os_type = client.get_os_type().lower() 91 | if (os_type == 'windows'): 92 | subprocess.Popen('powershell.exe Remove-NetNat -Confirm:$False') 93 | subprocess.Popen('powershell.exe Restart-Service hns') 94 | 95 | def test_create(self): 96 | with EdgeDockerClient() as client: 97 | exception_raised = False 98 | try: 99 | self._reset_host_network(client) 100 | status = client.status(self.CONTAINER_NAME) 101 | if status is not None: 102 | self._destroy_container(client) 103 | self._create_container(client) 104 | status = client.status(self.CONTAINER_NAME) 105 | self.assertEqual('created', status) 106 | client.start(self.CONTAINER_NAME) 107 | status = client.status(self.CONTAINER_NAME) 108 | self.assertEqual('running', status) 109 | time.sleep(5) 110 | self._destroy_container(client) 111 | except EdgeDeploymentError as ex: 112 | print(ex) 113 | exception_raised = True 114 | self.assertFalse(exception_raised) 115 | -------------------------------------------------------------------------------- /tests/test_edgemanager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import unittest 4 | from iotedgehubdev.edgemanager import EdgeManager 5 | from iotedgehubdev.errors import RegistriesLoginError 6 | 7 | 8 | class TestEdgeManager(unittest.TestCase): 9 | 10 | def test_Login_registries_fail(self): 11 | module_content = { 12 | "$edgeAgent": { 13 | "properties.desired": { 14 | "schemaVersion": "1.0", 15 | "runtime": { 16 | "type": "docker", 17 | "settings": { 18 | "minDockerVersion": "v1.25", 19 | "loggingOptions": "", 20 | "registryCredentials": { 21 | "a": { 22 | "username": "sa", 23 | "password": "pd", 24 | "address": "a" 25 | }, 26 | "b": { 27 | "address": "b" 28 | }, 29 | "c": { 30 | "address": "c", 31 | "username": "cpd" 32 | }, 33 | "d": { 34 | "address": "d", 35 | "username": "du", 36 | "password": "dpwd" 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | try: 45 | EdgeManager.login_registries(module_content) 46 | except RegistriesLoginError as e: 47 | self.assertEqual(4, len(e.registries())) 48 | return 49 | self.fail("No expception throws when registries login fail") 50 | 51 | def test_no_registries(self): 52 | module_content = { 53 | "$edgeAgent": { 54 | "properties.desired": { 55 | "schemaVersion": "1.0", 56 | "runtime": {} 57 | } 58 | } 59 | } 60 | try: 61 | EdgeManager.login_registries(module_content) 62 | except Exception: 63 | self.fail("No expception should be raised when there is no registry") 64 | 65 | def test_update_module_twin(self): 66 | module_content = { 67 | "$edgeAgent": {}, 68 | "$edgeHub": {}, 69 | "testtwin": { 70 | "properties.desired": { 71 | "sequence": 1, 72 | "value": "test" 73 | } 74 | } 75 | } 76 | hub_conn_str = os.environ['IOTHUB_CONNECTION_STRING'] 77 | device_conn_str = os.environ[platform.system().upper() + '_DEVICE_CONNECTION_STRING'] 78 | edge_manager = EdgeManager(device_conn_str, 'localhost', '', hub_conn_str) 79 | edge_manager.getOrAddModule('testtwin', True) 80 | try: 81 | edge_manager.update_module_twin(module_content) 82 | except Exception: 83 | self.fail("No exception should be raised to update module twin here") 84 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | 5 | import errno 6 | import stat 7 | import unittest 8 | from unittest import mock 9 | from iotedgehubdev.utils import Utils 10 | 11 | 12 | class TestUtilAPIs(unittest.TestCase): 13 | 14 | def test_parse_connection_strs_valid(self): 15 | """" Test a valid invocation of API parse_connection_strs""" 16 | device_connstr = 'HostName=testhub.azure-devices.net;DeviceId=mylaptop2;SharedAccessKey=XXXXX' 17 | hub_connstr = 'HostName=testhub.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=XXXXX' 18 | data = Utils.parse_connection_strs(device_connstr, hub_connstr) 19 | self.assertEqual('iothubowner', data['SharedAccessKeyName']) 20 | 21 | def test_parse_connection_strs_invalid(self): 22 | """" Test an invalid invocation of API parse_connection_strs""" 23 | device_connstr = 'HostName=testhub.azure-devices.net;DeviceId=mylaptop2;SharedAccessKey=XXXXX' 24 | hub_connstr = device_connstr 25 | with self.assertRaises(KeyError) as err: 26 | Utils.parse_connection_strs(device_connstr, hub_connstr) 27 | self.assertTrue('instead of an device' in err.exception.args[0]) 28 | 29 | def test_parse_connection_strs_notmatch_device(self): 30 | """" Test an invalid invocation of API parse_connection_strs. Mismatch between the connection strings""" 31 | device_connstr = 'HostName=testhub.azure-devices.net;DeviceId=mylaptop2;SharedAccessKey=XXXXX' 32 | hub_connstr = 'HostName=hub.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=XXXXX' 33 | with self.assertRaises(KeyError) as err: 34 | Utils.parse_connection_strs(device_connstr, hub_connstr) 35 | self.assertTrue('belongs to' in err.exception.args[0]) 36 | 37 | @mock.patch('shutil.rmtree') 38 | @mock.patch('os.path.exists') 39 | def test_delete_dir_when_dir_exists(self, mock_exists, mock_rmtree): 40 | """ Test a valid invocation of API delete_dir when dir to be deleted exists""" 41 | # arrange 42 | dir_path = 'blah' 43 | mock_exists.return_value = True 44 | 45 | # act 46 | Utils.delete_dir(dir_path) 47 | 48 | # assert 49 | mock_exists.assert_called_with(dir_path) 50 | mock_rmtree.assert_called_with(dir_path, onerror=Utils._remove_readonly_callback) 51 | 52 | @mock.patch('os.unlink') 53 | @mock.patch('os.chmod') 54 | def test_delete_dir_execute_onerror_callback(self, mock_chmod, mock_unlink): 55 | """ Test rmtree onerror callback invocation""" 56 | # arrange 57 | dir_path = 'blah' 58 | ignored = 0 59 | 60 | # act 61 | Utils._remove_readonly_callback(ignored, dir_path, ignored) 62 | 63 | # assert 64 | mock_chmod.assert_called_with(dir_path, stat.S_IWRITE) 65 | mock_unlink.assert_called_with(dir_path) 66 | 67 | @mock.patch('shutil.rmtree') 68 | @mock.patch('os.path.exists') 69 | def test_delete_dir_when_dir_does_not_exist(self, mock_exists, mock_rmtree): 70 | """ Test a valid invocation of API delete_dir when dir to be deleted does not exist""" 71 | # arrange 72 | dir_path = 'blah' 73 | mock_exists.return_value = False 74 | 75 | # act 76 | Utils.delete_dir(dir_path) 77 | 78 | # assert 79 | mock_exists.assert_called_with(dir_path) 80 | mock_rmtree.assert_not_called() 81 | 82 | @mock.patch('shutil.rmtree') 83 | @mock.patch('os.path.exists') 84 | def test_delete_dir_raises_oserror_when_rmtree_fails(self, mock_exists, mock_rmtree): 85 | """ Tests invocation of API delete_dir raises OSError when rmtree raises OSError""" 86 | # arrange 87 | dir_path = 'blah' 88 | mock_exists.return_value = True 89 | mock_rmtree.side_effect = OSError('rmtree error') 90 | 91 | # act, assert 92 | with self.assertRaises(OSError): 93 | Utils.delete_dir(dir_path) 94 | 95 | @mock.patch('os.makedirs') 96 | def test_mkdir_if_needed_when_dir_does_not_exist(self, mock_mkdirs): 97 | """ Test a valid invocation of API mkdir_if_needed when dir to be made does not exist """ 98 | # arrange 99 | dir_path = 'blah' 100 | 101 | # act 102 | Utils.mkdir_if_needed(dir_path) 103 | 104 | # assert 105 | mock_mkdirs.assert_called_with(dir_path) 106 | 107 | @mock.patch('os.makedirs') 108 | def test_mkdir_if_needed_when_dir_exists(self, mock_mkdirs): 109 | """ Test a valid invocation of API mkdir_if_needed when dir to be made already exists """ 110 | # arrange 111 | dir_path = 'blah' 112 | mock_mkdirs.side_effect = OSError(errno.EEXIST, 'Directory exists.') 113 | 114 | # act 115 | Utils.mkdir_if_needed(dir_path) 116 | 117 | # assert 118 | mock_mkdirs.assert_called_with(dir_path) 119 | 120 | @mock.patch('os.makedirs') 121 | def test_mkdir_if_needed_raises_oserror_when_mkdir_fails(self, mock_mkdirs): 122 | """ Tests invocation of API mkdir_if_needed raises OSError when makedirs raises OSError""" 123 | # arrange 124 | dir_path = 'blah' 125 | mock_mkdirs.side_effect = OSError(errno.EACCES, 'Directory permission error') 126 | 127 | # act, assert 128 | with self.assertRaises(OSError): 129 | Utils.mkdir_if_needed(dir_path) 130 | 131 | @mock.patch('os.path.isfile') 132 | @mock.patch('os.path.exists') 133 | def test_check_if_file_exists_returns_true(self, mock_exists, mock_isfile): 134 | """ Test a valid invocation of API check_if_file_exists """ 135 | # arrange #1 136 | file_path = 'blah' 137 | mock_exists.return_value = True 138 | mock_isfile.return_value = True 139 | 140 | # act 141 | result = Utils.check_if_file_exists(file_path) 142 | 143 | # assert 144 | mock_exists.assert_called_with(file_path) 145 | mock_isfile.assert_called_with(file_path) 146 | self.assertTrue(result) 147 | 148 | @mock.patch('os.path.isfile') 149 | @mock.patch('os.path.exists') 150 | def test_check_if_file_exists_returns_false_if_exists_returns_false(self, 151 | mock_exists, mock_isfile): 152 | """ Test a valid invocation of API check_if_file_exists """ 153 | 154 | # arrange 155 | file_path = 'blah' 156 | mock_exists.return_value = False 157 | 158 | # act 159 | result = Utils.check_if_file_exists(file_path) 160 | 161 | # assert 162 | mock_exists.assert_called_with(file_path) 163 | mock_isfile.assert_not_called() 164 | self.assertFalse(result) 165 | 166 | @mock.patch('os.path.isfile') 167 | @mock.patch('os.path.exists') 168 | def test_check_if_file_exists_returns_false_if_isfile_returns_false(self, 169 | mock_exists, mock_isfile): 170 | """ Test a valid invocation of API check_if_file_exists """ 171 | 172 | # arrange 173 | file_path = 'blah' 174 | mock_exists.return_value = True 175 | mock_isfile.return_value = False 176 | 177 | # act 178 | result = Utils.check_if_file_exists(file_path) 179 | 180 | # assert 181 | mock_exists.assert_called_with(file_path) 182 | mock_isfile.assert_called_with(file_path) 183 | self.assertFalse(result) 184 | 185 | @mock.patch('os.path.isfile') 186 | @mock.patch('os.path.exists') 187 | def test_check_if_file_exists_returns_false_path_is_none(self, mock_exists, mock_isfile): 188 | """ Test a valid invocation of API check_if_file_exists """ 189 | 190 | # arrange 191 | file_path = None 192 | 193 | # act 194 | result = Utils.check_if_file_exists(file_path) 195 | 196 | # assert 197 | mock_exists.assert_not_called() 198 | mock_isfile.assert_not_called() 199 | self.assertFalse(result) 200 | 201 | @mock.patch('os.path.isdir') 202 | @mock.patch('os.path.exists') 203 | def test_check_if_dir_exists_returns_true(self, mock_exists, mock_isdir): 204 | # arrange #1 205 | dir_path = 'blah' 206 | mock_exists.return_value = True 207 | mock_isdir.return_value = True 208 | 209 | # act 210 | result = Utils.check_if_directory_exists(dir_path) 211 | 212 | # assert 213 | mock_exists.assert_called_with(dir_path) 214 | mock_isdir.assert_called_with(dir_path) 215 | self.assertTrue(result) 216 | 217 | @mock.patch('os.path.isdir') 218 | @mock.patch('os.path.exists') 219 | def test_check_if_dir_exists_returns_false_if_exists_returns_false(self, 220 | mock_exists, mock_isdir): 221 | # arrange 222 | dir_path = 'blah' 223 | mock_exists.return_value = False 224 | 225 | # act 226 | result = Utils.check_if_directory_exists(dir_path) 227 | 228 | # assert 229 | mock_exists.assert_called_with(dir_path) 230 | mock_isdir.assert_not_called() 231 | self.assertFalse(result) 232 | 233 | @mock.patch('os.path.isdir') 234 | @mock.patch('os.path.exists') 235 | def test_check_if_dir_exists_returns_false_if_isdir_returns_false(self, 236 | mock_exists, mock_isdir): 237 | # arrange 238 | dir_path = 'blah' 239 | mock_exists.return_value = True 240 | mock_isdir.return_value = False 241 | 242 | # act 243 | result = Utils.check_if_directory_exists(dir_path) 244 | 245 | # assert 246 | mock_exists.assert_called_with(dir_path) 247 | mock_isdir.assert_called_with(dir_path) 248 | self.assertFalse(result) 249 | 250 | @mock.patch('os.path.isdir') 251 | @mock.patch('os.path.exists') 252 | def test_check_if_dir_exists_returns_false_path_is_none(self, mock_exists, mock_isdir): 253 | # arrange 254 | dir_path = None 255 | 256 | # act 257 | result = Utils.check_if_directory_exists(dir_path) 258 | 259 | # assert 260 | mock_exists.assert_not_called() 261 | mock_isdir.assert_not_called() 262 | self.assertFalse(result) 263 | 264 | @mock.patch('socket.getfqdn') 265 | def test_get_hostname_valid(self, mock_hostname): 266 | """ Test a valid invocation of API get_hostname """ 267 | # arrange 268 | hostname = 'test_hostname' 269 | mock_hostname.return_value = hostname 270 | # act 271 | result = Utils.get_hostname() 272 | 273 | # assert 274 | mock_hostname.assert_called_with() 275 | self.assertEqual(hostname, result) 276 | 277 | @mock.patch('socket.getfqdn') 278 | def test_get_hostname_raises_ioerror_when_getfqdn_raises_ioerror(self, mock_hostname): 279 | """ Tests invocation of API get_hostname raises IOError when getfqdn raises IOError""" 280 | # arrange 281 | mock_hostname.side_effect = IOError('getfqdn IO error') 282 | 283 | # act, assert 284 | with self.assertRaises(IOError): 285 | Utils.get_hostname() 286 | 287 | def test_get_sha256_hash(self): 288 | assert Utils.get_sha256_hash("foo") == "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" 289 | 290 | def test_hash_connection_str_hostname(self): 291 | hostname = "ChaoyiTestIoT.azure-devices.net" 292 | 293 | assert Utils.hash_connection_str_hostname(hostname) == ( 294 | '6b8fcfea09003d5f104771e83bd9ff54c592ec2277ec1815df91dd64d1633778', 'azure-devices.net') 295 | 296 | assert Utils.hash_connection_str_hostname("") == ("", "") 297 | assert Utils.hash_connection_str_hostname(None) == ("", "") 298 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py37, py38, py39 3 | 4 | [testenv] 5 | commands= 6 | flake8 7 | py.test -s --cov iotedgehubdev {posargs} 8 | deps= 9 | pytest 10 | pytest-cov 11 | flake8 12 | python-dotenv 13 | -rrequirements.txt 14 | passenv = * 15 | 16 | [flake8] 17 | ignore=E741,E302 18 | max-line-length=130 19 | ; Exclude default excluded files as well as virutal environment from check since it will crash flake8 20 | exclude=.svn,CVS,.bzr,.hg,.git,__pycache__,.tox,venv 21 | -------------------------------------------------------------------------------- /vsts_ci/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | schedules: 2 | - cron: "0 12 * * 0,2,4" 3 | displayName: Scheduled build 4 | branches: 5 | include: 6 | - main 7 | always: true 8 | 9 | jobs: 10 | - job: MacOS 11 | pool: 12 | vmImage: macOS-latest 13 | strategy: 14 | matrix: 15 | Python37: 16 | python.version: "3.7" 17 | TOXENV: "py37" 18 | Python38: 19 | python.version: "3.8" 20 | TOXENV: "py38" 21 | Python39: 22 | python.version: "3.9" 23 | TOXENV: "py39" 24 | maxParallel: 1 25 | steps: 26 | - template: darwin/continuous-build-darwin.yml 27 | 28 | - job: Windows 29 | pool: 30 | vmImage: windows-2019 31 | strategy: 32 | matrix: 33 | Python37: 34 | python.version: "3.7" 35 | TOXENV: "py37" 36 | Python38: 37 | python.version: "3.8" 38 | TOXENV: "py38" 39 | Python39: 40 | python.version: "3.9" 41 | TOXENV: "py39" 42 | maxParallel: 1 43 | steps: 44 | - template: win32/continuous-build-win32.yml 45 | 46 | - job: Linux2004 47 | dependsOn: Windows 48 | pool: 49 | vmImage: ubuntu-20.04 50 | strategy: 51 | matrix: 52 | Python37: 53 | python.version: "3.7" 54 | TOXENV: "py37" 55 | Python38: 56 | python.version: "3.8" 57 | TOXENV: "py38" 58 | Python39: 59 | python.version: "3.9" 60 | TOXENV: "py39" 61 | maxParallel: 1 62 | steps: 63 | - template: linux/continuous-build-linux.yml 64 | 65 | - job: Linux2204 66 | dependsOn: Linux2004 67 | pool: 68 | vmImage: ubuntu-22.04 69 | strategy: 70 | matrix: 71 | Python37: 72 | python.version: "3.7" 73 | TOXENV: "py37" 74 | Python38: 75 | python.version: "3.8" 76 | TOXENV: "py38" 77 | Python39: 78 | python.version: "3.9" 79 | TOXENV: "py39" 80 | maxParallel: 1 81 | steps: 82 | - template: linux/continuous-build-linux.yml 83 | 84 | 85 | # - job: ScanForVulnerabilities 86 | # pool: 87 | # vmImage: windows-latest 88 | # steps: 89 | # - task: Bandit@1 90 | # inputs: 91 | # targetsType: 'banditPattern' 92 | # targetsBandit: '$(Build.SourcesDirectory)' 93 | # targetsBanditRecursive: true 94 | # ruleset: 'guardian' 95 | # verbose: true 96 | # aggregate: 'file' 97 | # - script: | 98 | # for /f %%a IN ('dir /b /s "D:\a\1\.gdn\r\*"') DO cat %%a 99 | 100 | - job: LegalStatusPolicyCheck 101 | pool: 102 | vmImage: ubuntu-20.04 103 | steps: 104 | - template: policy/continuous-legal-status-policy-check.yml 105 | 106 | - job: PublishStandaloneBinariesForWin32 107 | dependsOn: 108 | - MacOS 109 | - Windows 110 | - Linux2004 111 | - Linux2204 112 | pool: 113 | name: Azure-IoT-EdgeExperience-1ES-Hosted-Windows 114 | demands: 115 | - ImageOverride -equals MMS2019TLS 116 | steps: 117 | - template: standalone-binaries/continuous-build-standalone-binaries-win32.yml 118 | condition: and(succeeded('Windows'), succeeded('Linux2004'), succeeded('Linux2204')) 119 | 120 | - job: PublishDropFile 121 | dependsOn: 122 | - MacOS 123 | - Windows 124 | - Linux2004 125 | - Linux2204 126 | condition: and(succeeded('Windows'), succeeded('Linux2004'), succeeded('Linux2204')) 127 | pool: 128 | name: Azure-IoT-EdgeExperience-1ES-Hosted-Linux 129 | demands: 130 | - ImageOverride -equals MMSUbuntu18.04TLS 131 | 132 | steps: 133 | - task: UsePythonVersion@0 134 | displayName: "Use Python 3.7" 135 | inputs: 136 | versionSpec: 3.7 137 | addToPath: true 138 | architecture: "x64" 139 | 140 | - powershell: | 141 | if ("$(BUILD.SOURCEBRANCH)" -match "^refs/tags/v?[0-9]+\.[0-9]+\.[0-9]+$") { ((Get-Content -path "$(BUILD.REPOSITORY.LOCALPATH)\iotedgehubdev\__init__.py" -Raw) -replace "__AIkey__ = '.*'","__AIkey__ = '$(AI_KEY)'") | Set-Content -Path "$(BUILD.REPOSITORY.LOCALPATH)\iotedgehubdev\__init__.py" } 142 | displayName: "Replace AI Key for PROD" 143 | 144 | - script: | 145 | pip install setuptools 146 | pip install wheel 147 | pushd $(BUILD.REPOSITORY.LOCALPATH) 148 | python setup.py bdist_wheel 149 | popd 150 | displayName: "Build drop file" 151 | 152 | - task: CopyFiles@2 153 | inputs: 154 | SourceFolder: $(BUILD.REPOSITORY.LOCALPATH)/dist 155 | TargetFolder: $(Build.ArtifactStagingDirectory) 156 | displayName: "Copy Files to: build artifact staging directory" 157 | 158 | - task: EsrpCodeSigning@1 159 | inputs: 160 | ConnectedServiceName: 'IoT Edge NuGet Sign - DDE' 161 | FolderPath: '$(Build.ArtifactStagingDirectory)' 162 | Pattern: 'iotedgehubdev.exe' 163 | signConfigType: 'inlineSignParams' 164 | # 9990 : Microsoft Corporation (Compressed Binaries SHA2 Root - Standard Root) 165 | # https://microsoft.sharepoint.com/teams/codesigninfo/Wiki/Json%20Builder%20-%20ESRP%20Client.aspx?kc1=CP-230012&os1=SigntoolSign&cp1=OpusName%3DMicrosoft%3bOpusInfo%3Dhttp%3a//www.microsoft.com%3bFileDigest%3D/fd%20%5c%22SHA256%5c%22%3bPageHash%3D/NPH%3bTimeStamp%3D/tr%20%5c%22http%3a//rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer%5c%22%20/td%20sha256&kc2=CP-230012&os2=SigntoolVerify 166 | inlineOperation: | 167 | [ 168 | { 169 | "KeyCode" : "CP-230012", 170 | "OperationCode" : "SigntoolSign", 171 | "Parameters" : { 172 | "OpusName" : "Microsoft", 173 | "OpusInfo" : "http://www.microsoft.com", 174 | "FileDigest" : "/fd \"SHA256\"", 175 | "PageHash" : "/NPH", 176 | "TimeStamp" : "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" 177 | }, 178 | "ToolName" : "sign", 179 | "ToolVersion" : "1.0" 180 | }, 181 | { 182 | "KeyCode" : "CP-230012", 183 | "OperationCode" : "SigntoolVerify", 184 | "Parameters" : {}, 185 | "ToolName" : "sign", 186 | "ToolVersion" : "1.0" 187 | } 188 | ] 189 | SessionTimeout: '60' 190 | MaxConcurrency: '50' 191 | MaxRetryAttempts: '5' 192 | 193 | - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 194 | displayName: 'SBOM Generation Task' 195 | inputs: 196 | BuildDropPath: '$(Build.ArtifactStagingDirectory)' 197 | 198 | - task: PublishBuildArtifacts@1 199 | inputs: 200 | pathtoPublish: $(Build.ArtifactStagingDirectory) 201 | artifactName: build-artifact-drop 202 | 203 | - task: Bash@3 204 | inputs: 205 | targetType: 'inline' 206 | script: | 207 | if [[ $(Build.SourceBranch) =~ ^refs/tags/v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 208 | echo "##vso[task.setvariable variable=PYPI_PUBLISH_FLAG;isOutput=true]true" 209 | fi 210 | name: PublishFlag 211 | 212 | - job: UploadToStorage 213 | dependsOn: 214 | - MacOS 215 | - Windows 216 | - Linux2004 217 | - Linux2204 218 | - PublishDropFile 219 | - PublishStandaloneBinariesForWin32 220 | condition: and(succeeded(), eq(dependencies.PublishDropFile.outputs['PublishFlag.PYPI_PUBLISH_FLAG'], 'true')) 221 | pool: 222 | name: Azure-IoT-EdgeExperience-1ES-Hosted-Windows 223 | demands: 224 | - ImageOverride -equals MMS2022TLS 225 | 226 | steps: 227 | - task: DownloadPipelineArtifact@2 228 | inputs: 229 | buildType: 'current' 230 | artifactName: 'build-artifact-drop' 231 | itemPattern: | 232 | **/*.whl 233 | **/*.zip 234 | targetPath: '$(Build.ArtifactStagingDirectory)/Package' 235 | 236 | - task: PowerShell@2 237 | inputs: 238 | targetType: 'inline' 239 | script: | 240 | $VersionFormat = Get-Content .\iotedgehubdev\__init__.py | select-string -pattern "version" 241 | $PackageVersion = (($VersionFormat -split "=")[1] -split "'")[1] 242 | write-host "##vso[task.setvariable variable=version;]$PackageVersion" 243 | 244 | - task: AzureFileCopy@4 245 | condition: succeeded() 246 | inputs: 247 | SourcePath: '$(Build.ArtifactStagingDirectory)/Package/*$(version)*' 248 | azureSubscription: 'azuresdkpartnerdrops-IoTEdgeHub_Dev' 249 | Destination: AzureBlob 250 | storage: 'azuresdkpartnerdrops' 251 | ContainerName: 'drops' 252 | BlobPrefix: 'azure-iot-edge-tools-iotedgehubdev/python/$(version)' 253 | -------------------------------------------------------------------------------- /vsts_ci/darwin/continuous-build-darwin.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: UsePythonVersion@0 3 | inputs: 4 | versionSpec: "$(python.version)" 5 | addToPath: true 6 | architecture: "x64" 7 | 8 | - script: | 9 | az --version 10 | az extension add --name azure-iot 11 | displayName: "Install Azure Cli Extension" 12 | 13 | - script: | 14 | pip install --upgrade pip 15 | pip install --upgrade tox 16 | displayName: "Update and install required tools" 17 | 18 | - script: | 19 | brew update 20 | brew install docker 21 | brew install docker-machine 22 | brew link --overwrite docker-machine 23 | brew unlink docker-machine-driver-xhyve 24 | brew install docker-machine-driver-xhyve 25 | sudo chown root:wheel $(brew --prefix)/opt/docker-machine-driver-xhyve/bin/docker-machine-driver-xhyve 26 | sudo chmod u+s $(brew --prefix)/opt/docker-machine-driver-xhyve/bin/docker-machine-driver-xhyve 27 | mkdir -p /Users/vsts/.docker/machine/cache 28 | curl -Lo /Users/vsts/.docker/machine/cache/boot2docker.iso https://github.com/boot2docker/boot2docker/releases/download/v18.06.1-ce/boot2docker.iso 29 | docker-machine create default --driver xhyve --xhyve-boot2docker-url /Users/vsts/.docker/machine/cache/boot2docker.iso 30 | docker-machine env default 31 | eval $(docker-machine env default) 32 | brew services start docker-machine 33 | brew install docker-compose 34 | docker version 35 | sudo -E `which tox` -e "$(TOXENV)" 36 | displayName: "Run tests against iotedgehubdev source code" 37 | env: 38 | DARWIN_DEVICE_CONNECTION_STRING: $(DARWIN_DEVICE_CONNECTION_STRING) 39 | IOTHUB_CONNECTION_STRING: $(IOTHUB_CONNECTION_STRING) 40 | CONTAINER_REGISTRY_SERVER: $(CONTAINER_REGISTRY_SERVER) 41 | CONTAINER_REGISTRY_USERNAME: $(CONTAINER_REGISTRY_USERNAME) 42 | CONTAINER_REGISTRY_PASSWORD: $(CONTAINER_REGISTRY_PASSWORD) 43 | TEST_CA_KEY_PASSPHASE: $(TEST_CA_KEY_PASSPHASE) 44 | enabled: false 45 | -------------------------------------------------------------------------------- /vsts_ci/linux/continuous-build-linux.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: UsePythonVersion@0 3 | inputs: 4 | versionSpec: "$(python.version)" 5 | addToPath: true 6 | architecture: "x64" 7 | 8 | - script: | 9 | az --version 10 | az extension add --name azure-iot 11 | displayName: "Install Azure Cli Extension" 12 | 13 | - script: | 14 | pip install --upgrade pip 15 | pip install --upgrade tox 16 | sudo -E `which tox` -e "$(TOXENV)" 17 | displayName: "Run tests against iotedgehubdev source code" 18 | env: 19 | LINUX_DEVICE_CONNECTION_STRING: $(LINUX_DEVICE_CONNECTION_STRING) 20 | IOTHUB_CONNECTION_STRING: $(IOTHUB_CONNECTION_STRING) 21 | CONTAINER_REGISTRY_SERVER: $(CONTAINER_REGISTRY_SERVER) 22 | CONTAINER_REGISTRY_USERNAME: $(CONTAINER_REGISTRY_USERNAME) 23 | CONTAINER_REGISTRY_PASSWORD: $(CONTAINER_REGISTRY_PASSWORD) 24 | TEST_CA_KEY_PASSPHASE: $(TEST_CA_KEY_PASSPHASE) 25 | -------------------------------------------------------------------------------- /vsts_ci/policy/continuous-legal-status-policy-check.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 3 | displayName: 'Component Detection' 4 | -------------------------------------------------------------------------------- /vsts_ci/release.ps1: -------------------------------------------------------------------------------- 1 | echo $env:BUILD_SOURCEBRANCH 2 | echo $env:RELEASE_PRIMARYARTIFACTSOURCEALIAS 3 | echo $env:SYSTEM_DEFAULTWORKINGDIRECTORY/.pypirc 4 | $artifact_name="build-artifact-drop" 5 | $drop_file=Get-ChildItem -Path $env:SYSTEM_ARTIFACTSDIRECTORY/$env:RELEASE_PRIMARYARTIFACTSOURCEALIAS/$artifact_name/*.whl 6 | $drop_file_name=$drop_file.Name 7 | echo $drop_file_name 8 | $tool_name=$drop_file_name.Split('-')[0] 9 | echo $tool_name 10 | $tool_version=$drop_file_name.Split('-')[1] 11 | echo $tool_version 12 | if ($env:BUILD_SOURCEBRANCH -match "^refs/tags/[\s\S]+$") { 13 | pip install twine 14 | echo "The current branch is tag" 15 | if ($env:BUILD_SOURCEBRANCH -match "^refs/tags/v?[0-9]+\.[0-9]+\.[0-9]+$") { 16 | echo "Uploading to production pypi" 17 | twine upload -r pypi "$env:SYSTEM_ARTIFACTSDIRECTORY/$env:RELEASE_PRIMARYARTIFACTSOURCEALIAS/$artifact_name/$drop_file_name" -u $(pypiusername) -p $(pypipassword) --repository-url $(pypirepourl) 18 | pip install --no-cache --upgrade "$tool_name==$tool_version" 19 | } else { 20 | echo "Uploading to test pypi" 21 | twine upload -r pypitest "$env:SYSTEM_ARTIFACTSDIRECTORY/$env:RELEASE_PRIMARYARTIFACTSOURCEALIAS/$artifact_name/$drop_file_name"-u $(pytestusername) -p $(pytestuserpassword) --repository-url $(pytestrepourl) 22 | pip install --no-cache --upgrade "$tool_name==$tool_version" --index-url "https://test.pypi.org/simple/" --extra-index-url "https://pypi.org/simple" 23 | } 24 | } else { 25 | echo "The current branch is not a tag" 26 | } 27 | -------------------------------------------------------------------------------- /vsts_ci/standalone-binaries/continuous-build-standalone-binaries-win32.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: UsePythonVersion@0 3 | displayName: 'Use Python 3.7' 4 | inputs: 5 | versionSpec: 3.7 6 | architecture: x86 7 | 8 | - powershell: | 9 | $env:Path += ";C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x86" 10 | $VERSION_TAG = git describe --tags 11 | Write-Host "##vso[task.setvariable variable=VERSION_TAG]$VERSION_TAG" 12 | python -m venv ./venv 13 | .\venv\Scripts\activate 14 | python -m pip install --upgrade pip 15 | if ("$(BUILD.SOURCEBRANCH)" -match "^refs/tags/v?[0-9]+\.[0-9]+\.[0-9]+$") { ((Get-Content -path "$(BUILD.REPOSITORY.LOCALPATH)\iotedgehubdev\__init__.py" -Raw) -replace "__AIkey__ = '.*'","__AIkey__ = '$(AI_KEY)'") | Set-Content -Path "$(BUILD.REPOSITORY.LOCALPATH)\iotedgehubdev\__init__.py" } 16 | ((Get-Content -path "$(BUILD.REPOSITORY.LOCALPATH)\iotedgehubdev\__init__.py" -Raw) -replace "__production__ = 'iotedgehubdev'","__production__ = 'iotedgehubdev-standalone'") | Set-Content -Path "$(BUILD.REPOSITORY.LOCALPATH)\iotedgehubdev\__init__.py" 17 | pip install -e . 18 | pip install -r requirements.txt 19 | pip install --upgrade setuptools>=50 20 | pyinstaller iotedgehubdev.spec 21 | $COMPOSE_VER = pip show docker-compose | select-string -Pattern "Version: (\d+\.\d+\.\d+)" | % {$_.Matches.Groups[1].Value} 22 | Invoke-WebRequest "https://github.com/docker/compose/releases/download/$COMPOSE_VER/docker-compose-Windows-x86_64.exe" -Out "./standalone-binaries/iotedgehubdev/docker-compose.exe" 23 | 24 | - task: ArchiveFiles@2 25 | inputs: 26 | rootFolderOrFile: 'standalone-binaries/iotedgehubdev' 27 | archiveFile: '$(Build.ArtifactStagingDirectory)/iotedgehubdev-$(VERSION_TAG)-win32-ia32.zip' 28 | 29 | - task: GitHubRelease@0 30 | inputs: 31 | gitHubConnection: 'github.com_marianan' 32 | tagPattern: '^v?[0-9]+\.[0-9]+\.[0-9]+-rc[0-9]+$' 33 | releaseNotesSource: input 34 | assets: '$(Build.ArtifactStagingDirectory)/iotedgehubdev-$(VERSION_TAG)-win32-ia32.zip' 35 | isPreRelease: true 36 | addChangeLog: false 37 | 38 | - task: GitHubRelease@0 39 | inputs: 40 | gitHubConnection: 'github.com_marianan' 41 | tagPattern: '^v?[0-9]+\.[0-9]+\.[0-9]+$' 42 | releaseNotesSource: input 43 | assets: '$(Build.ArtifactStagingDirectory)/iotedgehubdev-$(VERSION_TAG)-win32-ia32.zip' 44 | addChangeLog: false 45 | 46 | - task: PublishBuildArtifacts@1 47 | inputs: 48 | pathtoPublish: '$(Build.ArtifactStagingDirectory)/iotedgehubdev-$(VERSION_TAG)-win32-ia32.zip' 49 | artifactName: build-artifact-drop -------------------------------------------------------------------------------- /vsts_ci/win32/continuous-build-win32.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: UsePythonVersion@0 3 | inputs: 4 | versionSpec: "$(python.version)" 5 | addToPath: true 6 | architecture: "x64" 7 | 8 | - powershell: | 9 | az --version 10 | az extension add --name azure-iot 11 | displayName: "Install Azure Cli Extension" 12 | 13 | - powershell: | 14 | pip install --upgrade pip 15 | pip install --upgrade tox 16 | tox -e "$(TOXENV)" 17 | displayName: "Run tests against iotedgehubdev source code" 18 | env: 19 | WINDOWS_DEVICE_CONNECTION_STRING: $(WINDOWS_DEVICE_CONNECTION_STRING) 20 | IOTHUB_CONNECTION_STRING: $(IOTHUB_CONNECTION_STRING) 21 | CONTAINER_REGISTRY_SERVER: $(CONTAINER_REGISTRY_SERVER) 22 | CONTAINER_REGISTRY_USERNAME: $(CONTAINER_REGISTRY_USERNAME) 23 | CONTAINER_REGISTRY_PASSWORD: $(CONTAINER_REGISTRY_PASSWORD) 24 | TEST_CA_KEY_PASSPHASE: $(TEST_CA_KEY_PASSPHASE) 25 | --------------------------------------------------------------------------------