Distribute Software and Firmware to Multiple Devices and Review Results

Distribute software and firmware files to multiple targets with version paths, deployment rules, results, and failure recovery.

IT EngineersDevelopers

Getting Started#

Basic Concept#

Distribute installation files and firmware by device and version

Software and firmware files are used across servers, branch systems, production equipment, edge devices, and other environments.

Software and firmware distribution prepares installation packages, build artifacts, and firmware files, then configures deployment jobs based on the target devices and versions to apply.

You can build deployment flows that match your operating environment, whether distributing one file to multiple devices or applying different files and versions by device group.

Deployment Structure

Deployment File
Set Version Criteria
Select Target Devices
Server Group
Branch Devices
Edge Devices
Review Results

This process lets you manage the files to distribute, target devices, and application criteria in a single flow.

Deployment Flow#

Move from file preparation to deployment and result verification

A deployment job starts by preparing the installation file or firmware and defining the devices and versions that will receive it.

When the deployment runs under the specified conditions, files are transferred to each target device, where you can review per-device progress and file application results.

  1. Prepare deployment files
  2. Configure target devices
  3. Set version and execution criteria
  4. Distribute files to targets
  5. Review application results

If needed, you can rerun the deployment for a specific device or device group.

Deployment Benefits#

Manage multi-device file distribution and version control in one flow

When applying software and firmware across multiple devices, teams need to manage the deployment files, targets, versions, and execution results together.

Software and firmware distribution lets you manage deployment work by device in a single operational flow and track which files were delivered to which targets.

Category Per-Device Management Deployment Flow
Deployment Targets Review targets device by device Manage by device and group
Deployment Files Check applicable targets for each file Configure files as part of the deployment job
Version Application Review version status by device Deploy according to version criteria
Execution Results Review results for each device separately Review results across targets together
Redeployment Reselect required devices Rerun by target or group

This creates a single operational flow from software and firmware distribution through result verification for each target.

IT Engineers#

Deployment Files#

Prepare installation packages and firmware files for deployment

First, prepare the installation packages, build artifacts, or firmware files to distribute.

A file can be used across multiple targets in a single deployment job, or different files can be assigned to different device groups depending on the operating environment.

For example, the following file types can be configured for deployment.

File Type Example Use
Installation Package Application installer
Build Artifact Latest build files and deployment packages
Firmware Firmware for devices and edge systems
Update File Update files for existing environments

After preparing the deployment files, configure the devices and groups that should receive them.

Target Setup#

Organize servers, branch systems, and edge devices by group and device

Deployment targets can be configured as individual devices or grouped together.

For example, targets can be organized around operating criteria such as headquarters servers, nationwide branch systems, specific production equipment, or edge-device groups.

Deployment Targets

  • Server GroupDevice ADevice BDevice C
  • Branch GroupDevice DDevice EDevice F
  • Edge GroupDevice GDevice HDevice I

This lets you select multiple devices in a single deployment job or limit the deployment scope to specific groups and devices.

Deployment Criteria#

Set file versions and execution conditions by device

After configuring the targets, define which files should be deployed and under what conditions.

Link deployment-file versions to target devices, then configure execution to run immediately, on a schedule, or after another task completes.

Criteria to Configure

Setting Configuration
Deployment File Specify the installation package or firmware
Applied Version Specify the file version to deploy to target devices
Target Scope Select all devices, a group, or individual devices
Execution Condition Run immediately, on a schedule, or after another task completes
File Path Set the storage or application location on the target device

For example, you can deploy newly prepared firmware to a specific device group or distribute build artifacts to designated servers after a build job completes.

Multi-Device Deployment#

Transfer files to multiple target devices in one deployment workflow

After defining the deployment criteria, configure a multi-device flow that sends one file to multiple targets.

In Flow Canvas, connect one deployment source to multiple target devices to distribute the same file across servers, branch systems, edge devices, and other environments.

Deployment File

Deployment Job
  • ServerVerify
  • BranchVerify
  • EdgeVerify

You can process all target devices together or select specific groups and devices based on operational requirements.

Result Management#

Review per-target status and rerun deployment for required devices

When a deployment runs, use Runs and the job details to review the overall deployment status and results for each target.

During review, you can check each device's progress, processed files, execution time, and related details.

Deployment Result Flow

Run Deployment
Review Overall Job Status
Completed
Review per-target application results
Running
Review progress and processing status
Needs Review
Review Details
Select Target Device
Rerun Deployment

Item Details
Target Device Server or device that received the files
Progress Status Current processing status of the deployment job
File Result File application result for each target device
Execution Time Deployment start and completion times
Redeployment Target Device or group to rerun

If a deployment result for a specific device needs to be reviewed again, rerun the required deployment for that target or device group.

Developers#

Distribute one artifact to multiple devices and aggregate the application results by device

Integration Setup#

Prepare shared API calls and status values

import os
import requests

BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com").rstrip("/")
TOKEN = os.environ["INNORIX_ACCESS_TOKEN"]
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")   # optional; falls back to the current workspace

STATUS_COMPLETE = 2
TERMINAL = {2, 4, 5, 9, 99}          # complete / error / cancelled / partial / failed
NOT_SUCCEEDED = {4, 5, 9, 99}


def api(method, path, body=None, params=None):
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {TOKEN}",
    }

    if WORKSPACE_ID:
        headers["x-workspace-id"] = WORKSPACE_ID

    response = requests.request(
        method, BASE_URL + path,
        headers=headers, json=body, params=params, timeout=30,
    )

    payload = response.json() if response.content else {}

    if not response.ok:
        raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")

    return payload.get("data")


def is_terminal(detail):
    return detail.get("isTerminal", detail.get("status") in TERMINAL)

Use the following values to determine transfer status. There are five terminal states, and Complete (2) is the successful state.

Status Value Meaning Terminal
2 Complete Yes
4 Error Yes
5 Cancelled Yes
9 Partial Complete Yes
99 Failed Yes
1, 6, 12, 13 Starting, Transferring, Synchronizing, Receiving No

Configure Deployment Targets#

Define device groups as lists and resolve their identifiers

Manage deployment targets as lists. If you work with device names, resolve them to identifiers first.

def resolve_device(name):
    result = api("GET", "/api/devices/resolve", params={"name": name}) or {}
    devices = result.get("devices") or []
    count = result.get("matchCount", len(devices))

    if count != 1 or not devices:
        raise RuntimeError(f"{name}: {count} matches")

    return devices[0]["deviceId"]


GROUPS = {
    "server": ["srv-01", "srv-02", "srv-03"],
    "branch": ["branch-seoul", "branch-busan"],
    "edge":   ["edge-line-01", "edge-line-02"],
}

RESOLVED = {
    group: [resolve_device(name) for name in names]
    for group, names in GROUPS.items()
}

You can also filter the complete device list by condition.

result = api("GET", "/api/devices", params={"page": 1, "size": 200}) or {}

edge_devices = [d["deviceId"] for d in result.get("devices") or []
                if d["name"].startswith("edge-")]

The device list is returned in the data.devices array.

Design Versioned Paths#

Make the deployment-file version visible in the path

Record the deployed version in the path so you can see which version was applied where.

def package_path(product, version):
    return f"/release/{product}/{version}"


def target_path(product, version):
    return f"/opt/{product}/{version}"


PRODUCT, VERSION = "app", "2.14.0"

Keeping separate version folders preserves previous versions and makes rollback easier if a problem occurs. If every release overwrites the same path, there is no previous version to roll back to.

Multi-Device Deployment#

Send one file to multiple devices

A single transfer handles one target device. Create a transfer for each target and store the returned monitorId by device to track progress and results.

def deploy(source, targets, product, version, target_paths=None):
    src = package_path(product, version)

    # pass a list when targets need different paths; the order must match
    paths = target_paths or [target_path(product, version)] * len(targets)

    return {
        device: api("POST", "/api/transfers/manual", {
            "sourceDevice": source,
            "targetDevice": device,
            "targetPath": path,
            "sourcePaths": [src],
            "sendAllFolder": True,
            "checkIntegrity": True,
            "transferOptions": {"target-action": "overwrite"},
        })["monitorId"]
        for device, path in zip(targets, paths)
    }


transfers = deploy("device-build-01", RESOLVED["edge"], PRODUCT, VERSION)

Within a version folder, redeployments usually send the same files again, so use overwrite. Using numbering creates an additional copy every time you redeploy.

Enable integrity verification. Applying corrupted firmware can prevent a device from booting.

If devices share field network links, apply a rate limit so deployment traffic does not interfere with other work.

json
"transferOptions": {
  "target-action": "overwrite",
  "limitRate": 20480
}

The limitRate unit is KB/s.

Aggregate Results#

Collect and review application status by device

def wait(monitor_id, timeout=3600, interval=3):
    deadline = time.time() + timeout

    while time.time() < deadline:
        detail = api("GET", f"/api/transfers/{monitor_id}")

        if is_terminal(detail):
            return detail

        time.sleep(interval)

    raise TimeoutError(monitor_id)


def failed_files(monitor_id):
    result = api("GET", f"/api/transfers/{monitor_id}/files", params={
        "state": "any", "size": 500,
    }) or {}

    return [r for r in (result.get("children") or [])
            if r.get("status") in NOT_SUCCEEDED]


def retry_failed(monitor_id):
    rows = failed_files(monitor_id)

    if not rows:
        return 0

    api("POST", f"/api/transfers/{monitor_id}/retry", {
        "filesRetry": [
            {"filePath": r["sourceFilePath"], "isDir": bool(r.get("isFolder"))}
            for r in rows
        ]
    })

    return len(rows)

Continue checking the remaining devices even if one fails so you can determine how far the deployment was applied.

def deploy_report(transfers, timeout=3600):
    results = {}

    for device, monitor_id in transfers.items():
        try:
            results[device] = wait(monitor_id, timeout=timeout)
        except TimeoutError:
            results[device] = {"status": None}

    succeeded = [d for d, r in results.items()
                 if r.get("status") == STATUS_COMPLETE]
    failed = [d for d in results if d not in succeeded]

    return succeeded, failed, results


succeeded, failed, results = deploy_report(transfers)

print(f"{len(succeeded)} succeeded / {len(failed)} failed")

for device in failed:
    detail = results[device]
    print(f"  {device:20} {detail.get('statusName') or detail.get('status')}")
Item Details
status Transfer status by device
percent Progress
fileCount, totalSize Number and total size of applied files
endDate Completion time

Redeploy Failed Devices#

Skip successful devices and redeploy only failed targets

Rerunning the entire deployment retransfers files to devices that already succeeded. Select only the failed targets and rerun them.

for device in failed:
    monitor_id = transfers[device]
    rows = failed_files(monitor_id)

    if rows:
        # reached the device, but some files failed
        print(f"{device}: retried {retry_failed(monitor_id)} files")
    else:
        # no per file record means the transfer never started
        state = api("GET", f"/api/devices/{device}/connectivity") or {}
        print(f"{device}: transfer never started - connected {state.get('isConnected')}"
              f" ({state.get('stateLabel')})")

If there is no per-file failure record, check device connectivity first. After the connection is restored, rerun the deployment.

if still_offline:
    retry_transfers = deploy("device-build-01", recovered, PRODUCT, VERSION)
    _, still_failed, _ = deploy_report(retry_transfers)

Review Deployment History#

Check which version was applied to each device and when

from datetime import datetime, timedelta, timezone


def paginate(path, params=None, limit=200, max_pages=50):
    query = dict(params or {})
    query["limit"] = limit
    cursor = None

    for _ in range(max_pages):
        if cursor:
            query["cursor"] = cursor

        result = api("GET", path, params=query) or {}

        for record in result.get("data") or []:
            yield record

        pagination = result.get("pagination") or {}

        if not pagination.get("hasMore"):
            return

        cursor = pagination.get("nextCursor")

        if not cursor:
            return


end = datetime.now(timezone.utc)
fmt = "%Y-%m-%dT%H:%M:%SZ"

for row in paginate(f"/api/devices/{device_id}/transfer-history", params={
    "startDate": (end - timedelta(days=30)).strftime(fmt),
    "endDate": end.strftime(fmt),
}):
    print(row.get("startDate"), row.get("statusName"), row.get("monitorId"))

Recording the deployment identifier and version together in the application links deployment history to the corresponding version.

db.insert("deployments", {
    "product": PRODUCT,
    "version": VERSION,
    "device": device,
    "monitorId": transfers[device],
    "result": "done" if device in succeeded else "failed",
})
Item Details
Deployment File Transferred package and version
Target Device and group
Result Success or failure by device
Redeployment Targets rerun and their results
History Recent deployment records by device