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 ResultsThis 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.
① Prepare deployment files
↓
② Configure target devices
↓
③ Set version and execution criteria
↓
④ Distribute files to targets
↓
⑤ 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 Group Branch Group Edge Group
│ │ │
Device A Device D Device G
Device B Device E Device H
Device C Device F Device IThis 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
┌─────────┼─────────┐
▼ ▼ ▼
Server Branch Edge
│ │ │
▼ ▼ ▼
Verify Verify VerifyYou 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)// InnorixClient.java
public static final String BASE_URL =
env("INNORIX_BASE_URL", "https://app.innorix.com").replaceAll("/+quot;, "");
public static final String WORKSPACE_ID = env("INNORIX_WORKSPACE_ID", null);
public static final int STATUS_COMPLETE = 2;
// States the transfer no longer moves out of
public static final Set<Integer> TERMINAL = Set.of(2, 4, 5, 9, 99);
// Terminal states that are not a full success
public static final Set<Integer> NOT_SUCCEEDED = Set.of(4, 5, 9, 99);
private HttpRequest.Builder headers(HttpRequest.Builder builder) {
builder.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + session.accessToken());
// When omitted the account's current workspace is used.
if (workspaceId != null) builder.header("x-workspace-id", workspaceId);
return builder;
}
/** Unwraps and returns data from the response. Throws ApiError on failure. */
public Object api(String method, String path, Object body, Map<String, Object> params) {
Resp response = request(method, path, body, params);
Object payload = null;
try {
payload = Json.parse(response.text());
} catch (RuntimeException ignored) {
payload = null;
}
if (!response.ok()) {
Map<String, Object> map = Json.asObj(payload);
String message = Json.str(map, "message", Json.str(map, "error", "unknown error"));
throw new ApiError(response.status, message, map);
}
return Json.get(payload, "data");
}
/** Use the server flag when present, otherwise fall back to the status code. */
public static boolean isTerminal(Map<String, Object> record) {
Boolean flag = Json.boolOrNull(record, "isTerminal");
if (flag != null) return flag;
Integer status = Json.intOrNull(record, "status");
return status != null && TERMINAL.contains(status);
}// innorix-client.js
const BASE_URL = (process.env.INNORIX_BASE_URL
|| "https://app.innorix.com").replace(/\/+$/, "");
const TOKEN = process.env.INNORIX_ACCESS_TOKEN;
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID || null;
export const STATUS_COMPLETE = 2;
export const TERMINAL = new Set([2, 4, 5, 9, 99]); // complete / error / cancelled / partial / failed
export const NOT_SUCCEEDED = new Set([4, 5, 9, 99]);
export async function api(method, path, body = null, params = null) {
const url = new URL(BASE_URL + path);
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value === undefined || value === null) continue;
url.searchParams.set(key, String(value));
}
}
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${TOKEN}`,
};
// When omitted the account's current workspace is used.
if (WORKSPACE_ID) headers["x-workspace-id"] = WORKSPACE_ID;
const response = await fetch(url, {
method,
headers,
body: body === null ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(30_000),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(payload.message || `HTTP ${response.status}`);
}
return payload.data;
}
export function isTerminal(detail) {
return detail.isTerminal !== undefined
? detail.isTerminal
: TERMINAL.has(detail.status);
}// InnorixClient.cs
public static readonly string BaseUrl =
Env("INNORIX_BASE_URL", "https://app.innorix.com").TrimEnd('/');
public static readonly string WorkspaceIdFromEnv = Env("INNORIX_WORKSPACE_ID", null);
public const int StatusComplete = 2;
/// <summary>States the transfer no longer moves out of</summary>
public static readonly HashSet<int> Terminal = new HashSet<int> { 2, 4, 5, 9, 99 };
/// <summary>Terminal states that are not a full success</summary>
public static readonly HashSet<int> NotSucceeded = new HashSet<int> { 4, 5, 9, 99 };
// Applied on every request
request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + Session.AccessToken);
// When omitted the account's current workspace is used.
if (WorkspaceId != null) request.Headers.TryAddWithoutValidation("x-workspace-id", WorkspaceId);
public async Task<JsonNode> ApiAsync(string method, string path, JsonNode body = null,
IDictionary<string, object> parameters = null)
{
Resp response = await RequestAsync(method, path, body, parameters).ConfigureAwait(false);
JsonNode payload = null;
try
{
payload = J.Parse(response.Text());
}
catch (Exception)
{
payload = null;
}
if (!response.Ok)
{
JsonObject map = J.AsObj(payload);
string message = J.Str(map, "message", J.Str(map, "error", "unknown error"));
throw new ApiError(response.Status, message, map);
}
return J.Get(payload, "data");
}
/// <summary>Use the server flag when present, otherwise fall back to the status code.</summary>
public static bool IsTerminal(JsonObject record)
{
bool? flag = J.BoolOrNull(record, "isTerminal");
if (flag != null) return flag.Value;
int? status = J.IntOrNull(record, "status");
return status != null && Terminal.Contains(status.Value);
}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()
}String resolveDevice(String name) {
Map<String, Object> result = client.apiObj("GET", "/api/devices/resolve", null,
Json.newObj("name", name));
List<Object> devices = Json.arrOf(result, "devices");
int count = Json.intOr(result, "matchCount", devices.size());
if (count != 1 || devices.isEmpty()) {
throw new RuntimeException(name + ": " + count + " matches");
}
return Json.str(Json.asObj(devices.get(0)), "deviceId");
}
Map<String, List<String>> groups = Map.of(
"server", List.of("srv-01", "srv-02", "srv-03"),
"branch", List.of("branch-seoul", "branch-busan"),
"edge", List.of("edge-line-01", "edge-line-02"));
Map<String, List<String>> resolved = new LinkedHashMap<>();
groups.forEach((group, names) -> {
List<String> ids = new ArrayList<>();
for (String n : names) ids.add(resolveDevice(n));
resolved.put(group, ids);
});async function resolveDevice(name) {
const result = (await client.api("GET", "/api/devices/resolve",
null, { name })) || {};
const devices = result.devices || [];
const count = result.matchCount ?? devices.length;
if (count !== 1 || devices.length === 0) {
throw new Error(`${name}: ${count} matches`);
}
return devices[0].deviceId;
}
const GROUPS = {
server: ["srv-01", "srv-02", "srv-03"],
branch: ["branch-seoul", "branch-busan"],
edge: ["edge-line-01", "edge-line-02"],
};
const RESOLVED = {};
for (const [group, names] of Object.entries(GROUPS)) {
RESOLVED[group] = [];
for (const n of names) RESOLVED[group].push(await resolveDevice(n));
}async Task<string> ResolveDeviceAsync(string name)
{
JsonObject result = await client.ApiObjAsync("GET", "/api/devices/resolve",
null, new Dictionary<string, object> { ["name"] = name });
var devices = J.ArrOf(result, "devices");
int count = J.Int(result, "matchCount", devices.Count);
if (count != 1 || devices.Count == 0)
{
throw new Exception(quot;{name}: {count} matches");
}
return J.Str(J.AsObj(devices[0]), "deviceId");
}
var groups = new Dictionary<string, string[]>
{
["server"] = new[] { "srv-01", "srv-02", "srv-03" },
["branch"] = new[] { "branch-seoul", "branch-busan" },
["edge"] = new[] { "edge-line-01", "edge-line-02" },
};
var resolved = new Dictionary<string, List<string>>();
foreach (var (group, names) in groups)
{
var ids = new List<string>();
foreach (string n in names) ids.Add(await ResolveDeviceAsync(n));
resolved[group] = ids;
}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-")]Map<String, Object> result = client.apiObj("GET", "/api/devices", null,
Json.newObj("page", 1, "size", 200));
List<String> edgeDevices = new ArrayList<>();
for (Object node : Json.arrOf(result, "devices")) {
Map<String, Object> device = Json.asObj(node);
if (Json.str(device, "name").startsWith("edge-")) {
edgeDevices.add(Json.str(device, "deviceId"));
}
}const result = (await client.api("GET", "/api/devices", null,
{ page: 1, size: 200 })) || {};
const edgeDevices = (result.devices || [])
.filter((d) => d.name.startsWith("edge-"))
.map((d) => d.deviceId);JsonObject result = await client.ApiObjAsync("GET", "/api/devices", null,
new Dictionary<string, object> { ["page"] = 1, ["size"] = 200 });
var edgeDevices = J.ArrOf(result, "devices").Select(J.AsObj)
.Where(d => J.Str(d, "name").StartsWith("edge-"))
.Select(d => J.Str(d, "deviceId")).ToList();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"String packagePath(String product, String version) {
return "/release/" + product + "/" + version;
}
String targetPath(String product, String version) {
return "/opt/" + product + "/" + version;
}
String product = "app", version = "2.14.0";const packagePath = (product, version) => `/release/${product}/${version}`;
const targetPath = (product, version) => `/opt/${product}/${version}`;
const PRODUCT = "app";
const VERSION = "2.14.0";string PackagePath(string product, string version) => quot;/release/{product}/{version}";
string TargetPath(string product, string version) => quot;/opt/{product}/{version}";
const string Product = "app";
const string Version = "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)Map<String, String> deploy(String source, List<String> targets,
String product, String version, List<String> targetPaths) {
String src = packagePath(product, version);
// pass a list when targets need different paths; the order must match
List<String> paths = targetPaths;
if (paths == null) {
paths = new ArrayList<>();
for (int i = 0; i < targets.size(); i++) paths.add(targetPath(product, version));
}
Map<String, String> transfers = new LinkedHashMap<>();
for (int i = 0; i < targets.size(); i++) {
String device = targets.get(i);
Map<String, Object> transfer = client.apiObj("POST", "/api/transfers/manual", Json.newObj(
"sourceDevice", source,
"targetDevice", device,
"targetPath", paths.get(i),
"sourcePaths", List.of(src),
"sendAllFolder", true,
"checkIntegrity", true,
"transferOptions", Json.newObj("target-action", "overwrite")));
transfers.put(device, Json.str(transfer, "monitorId"));
}
return transfers;
}
Map<String, String> transfers = deploy("device-build-01", resolved.get("edge"),
product, version, null);async function deploy(source, targets, product, version, targetPaths = null) {
const src = packagePath(product, version);
// pass a list when targets need different paths; the order must match
const paths = targetPaths || targets.map(() => targetPath(product, version));
const transfers = {};
for (const [i, device] of targets.entries()) {
const transfer = await client.api("POST", "/api/transfers/manual", {
sourceDevice: source,
targetDevice: device,
targetPath: paths[i],
sourcePaths: [src],
sendAllFolder: true,
checkIntegrity: true,
transferOptions: { "target-action": "overwrite" },
});
transfers[device] = transfer.monitorId;
}
return transfers;
}
const transfers = await deploy("device-build-01", RESOLVED.edge, PRODUCT, VERSION);async Task<Dictionary<string, string>> DeployAsync(string source,
List<string> targets, string product, string version, List<string> targetPaths = null)
{
string src = PackagePath(product, version);
// pass a list when targets need different paths; the order must match
List<string> paths = targetPaths
?? targets.Select(_ => TargetPath(product, version)).ToList();
var transfers = new Dictionary<string, string>();
for (int i = 0; i < targets.Count; i++)
{
string device = targets[i];
JsonObject transfer = await client.ApiObjAsync("POST", "/api/transfers/manual",
new JsonObject
{
["sourceDevice"] = source,
["targetDevice"] = device,
["targetPath"] = paths[i],
["sourcePaths"] = new JsonArray { src },
["sendAllFolder"] = true,
["checkIntegrity"] = true,
["transferOptions"] = new JsonObject { ["target-action"] = "overwrite" },
});
transfers[device] = J.Str(transfer, "monitorId");
}
return transfers;
}
Dictionary<string, string> transfers = await DeployAsync("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.
"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)public Map<String, Object> wait(String monitorId, int timeoutSeconds, int intervalSeconds) {
long deadline = System.currentTimeMillis() + timeoutSeconds * 1000L;
while (System.currentTimeMillis() < deadline) {
Map<String, Object> detail = apiObj("GET", "/api/transfers/" + monitorId);
if (isTerminal(detail)) return detail;
sleep(intervalSeconds * 1000L);
}
throw new ApiError(0, "timeout waiting for " + monitorId);
}
public List<Map<String, Object>> failedFiles(String monitorId) {
List<Map<String, Object>> failed = new ArrayList<>();
for (Map<String, Object> row : transferFiles(monitorId)) {
Integer status = Json.intOrNull(row, "status");
if (status != null && NOT_SUCCEEDED.contains(status)) failed.add(row);
}
return failed;
}
public int retryFailed(String monitorId) {
List<Map<String, Object>> rows = failedFiles(monitorId);
if (rows.isEmpty()) return 0;
List<Object> filesRetry = new ArrayList<>();
for (Map<String, Object> row : rows) {
String path = Json.str(row, "sourceFilePath");
if (path == null) continue;
filesRetry.add(Json.newObj("filePath", path, "isDir", Json.bool(row, "isFolder", false)));
}
api("POST", "/api/transfers/" + monitorId + "/retry", Json.newObj("filesRetry", filesRetry));
return rows.size();
}const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export async function wait(monitorId, { timeout = 3600, interval = 3 } = {}) {
const deadline = Date.now() + timeout * 1000;
while (Date.now() < deadline) {
const detail = await api("GET", `/api/transfers/${monitorId}`);
if (isTerminal(detail)) return detail;
await sleep(interval * 1000);
}
throw new Error(`timeout waiting for ${monitorId}`);
}
export async function failedFiles(monitorId) {
const result = (await api("GET", `/api/transfers/${monitorId}/files`, null, {
state: "any", size: 500,
})) || {};
return (result.children || []).filter((row) => NOT_SUCCEEDED.has(row.status));
}
export async function retryFailed(monitorId) {
const rows = await failedFiles(monitorId);
if (rows.length === 0) return 0;
await api("POST", `/api/transfers/${monitorId}/retry`, {
filesRetry: rows
.filter((row) => row.sourceFilePath)
.map((row) => ({ filePath: row.sourceFilePath, isDir: Boolean(row.isFolder) })),
});
return rows.length;
}public async Task<JsonObject> WaitAsync(string monitorId, int timeoutSeconds = 3600,
int intervalSeconds = 3)
{
long deadline = Environment.TickCount64 + timeoutSeconds * 1000L;
while (Environment.TickCount64 < deadline)
{
JsonObject detail = await ApiObjAsync("GET", "/api/transfers/" + monitorId)
.ConfigureAwait(false);
if (IsTerminal(detail)) return detail;
await Task.Delay(intervalSeconds * 1000).ConfigureAwait(false);
}
throw new ApiError(0, "timeout waiting for " + monitorId);
}
public async Task<List<JsonObject>> FailedFilesAsync(string monitorId)
{
List<JsonObject> rows = await TransferFilesAsync(monitorId).ConfigureAwait(false);
return rows.Where(row =>
{
int? status = J.IntOrNull(row, "status");
return status != null && NotSucceeded.Contains(status.Value);
}).ToList();
}
/// <summary>Only callable once the transfer has reached a terminal state.</summary>
public async Task<int> RetryFailedAsync(string monitorId)
{
List<JsonObject> rows = await FailedFilesAsync(monitorId).ConfigureAwait(false);
if (rows.Count == 0) return 0;
var filesRetry = new JsonArray();
foreach (JsonObject row in rows)
{
string path = J.Str(row, "sourceFilePath");
if (path == null) continue;
filesRetry.Add(new JsonObject
{
["filePath"] = path,
["isDir"] = J.Bool(row, "isFolder", false),
});
}
await ApiAsync("POST", "/api/transfers/" + monitorId + "/retry",
new JsonObject { ["filesRetry"] = filesRetry }).ConfigureAwait(false);
return rows.Count;
}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')}")record DeployResult(List<String> succeeded, List<String> failed,
Map<String, Map<String, Object>> results) {}
DeployResult deployReport(Map<String, String> transfers, int timeout) {
Map<String, Map<String, Object>> results = new LinkedHashMap<>();
transfers.forEach((device, monitorId) -> {
try {
results.put(device, client.await(monitorId, timeout, null));
} catch (RuntimeException error) {
results.put(device, Json.newObj("status", null));
}
});
List<String> succeeded = new ArrayList<>();
List<String> failed = new ArrayList<>();
results.forEach((device, detail) -> {
if (Json.intOr(detail, "status", -1) == InnorixClient.STATUS_COMPLETE) succeeded.add(device);
else failed.add(device);
});
return new DeployResult(succeeded, failed, results);
}
DeployResult report = deployReport(transfers, 3600);
System.out.printf("%d succeeded / %d failed%n",
report.succeeded().size(), report.failed().size());
for (String device : report.failed()) {
Map<String, Object> detail = report.results().get(device);
System.out.println(" " + device + " " + Json.str(detail, "statusName"));
}async function deployReport(transfers, timeout = 3600) {
const results = {};
for (const [device, monitorId] of Object.entries(transfers)) {
try {
results[device] = await wait(monitorId, { timeout });
} catch {
results[device] = { status: null };
}
}
const succeeded = Object.entries(results)
.filter(([, r]) => r.status === STATUS_COMPLETE).map(([d]) => d);
const failed = Object.keys(results).filter((d) => !succeeded.includes(d));
return { succeeded, failed, results };
}
const { succeeded, failed, results } = await deployReport(transfers);
console.log(`${succeeded.length} succeeded / ${failed.length} failed`);
for (const device of failed) {
console.log(` ${device} ${results[device].statusName ?? results[device].status}`);
}async Task<(List<string> Succeeded, List<string> Failed,
Dictionary<string, JsonObject> Results)> DeployReportAsync(
Dictionary<string, string> transfers, int timeout = 3600)
{
var results = new Dictionary<string, JsonObject>();
foreach (var (device, monitorId) in transfers)
{
try
{
results[device] = await client.WaitAsync(monitorId, timeout);
}
catch
{
results[device] = new JsonObject { ["status"] = null };
}
}
var succeeded = results
.Where(kv => J.IntOrNull(kv.Value, "status") == InnorixClient.StatusComplete)
.Select(kv => kv.Key).ToList();
var failed = results.Keys.Where(d => !succeeded.Contains(d)).ToList();
return (succeeded, failed, results);
}
var (succeeded, failed, results) = await DeployReportAsync(transfers);
Console.WriteLine(quot;{succeeded.Count} succeeded / {failed.Count} failed");
foreach (string device in failed)
{
Console.WriteLine(quot; {device} {J.Str(results[device], "statusName")}");
}| 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')})")for (String device : failed) {
String monitorId = transfers.get(device);
List<Map<String, Object>> rows = client.failedFiles(monitorId);
if (!rows.isEmpty()) {
// reached the device, but some files failed
System.out.println(device + ": retried " + client.retryFailed(monitorId) + " files");
} else {
// no per file record means the transfer never started
Map<String, Object> state = client.apiObj("GET",
"/api/devices/" + device + "/connectivity");
System.out.println(device + ": transfer never started - connected "
+ Json.bool(state, "isConnected", false)
+ " (" + Json.str(state, "stateLabel") + ")");
}
}for (const device of failed) {
const monitorId = transfers[device];
const rows = await failedFiles(monitorId);
if (rows.length) {
// reached the device, but some files failed
console.log(`${device}: retried ${await retryFailed(monitorId)} files`);
} else {
// no per file record means the transfer never started
const state = (await client.api("GET",
`/api/devices/${device}/connectivity`)) || {};
console.log(`${device}: transfer never started - connected ${state.isConnected}`
+ ` (${state.stateLabel})`);
}
}foreach (string device in failed)
{
string monitorId = transfers[device];
List<JsonObject> rows = await client.FailedFilesAsync(monitorId);
if (rows.Count > 0)
{
// reached the device, but some files failed
Console.WriteLine(quot;{device}: retried {await client.RetryFailedAsync(monitorId)} files");
}
else
{
// no per file record means the transfer never started
JsonObject state = await client.ApiObjAsync("GET",
quot;/api/devices/{device}/connectivity");
Console.WriteLine(quot;{device}: transfer never started - "
+ quot;connected {J.Bool(state, "isConnected", false)} "
+ quot;({J.Str(state, "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)if (stillOffline) {
Map<String, String> retryTransfers = deploy("device-build-01", recovered,
product, version, null);
DeployResult retry = deployReport(retryTransfers, 3600);
}if (stillOffline) {
const retryTransfers = await deploy("device-build-01", recovered, PRODUCT, VERSION);
const { failed: stillFailed } = await deployReport(retryTransfers);
}if (stillOffline)
{
Dictionary<string, string> retryTransfers = await DeployAsync(
"device-build-01", recovered, Product, Version);
var (_, stillFailed, _) = await DeployReportAsync(retryTransfers);
}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"))List<Map<String, Object>> history = client.paginate(
"/api/devices/" + deviceId + "/transfer-history",
Json.newObj("startDate", start, "endDate", end), 200, 50);
for (Map<String, Object> row : history) {
System.out.println(Json.str(row, "startDate") + " "
+ Json.str(row, "statusName") + " " + Json.str(row, "monitorId"));
}for await (const row of paginate(`/api/devices/${deviceId}/transfer-history`, {
startDate: start,
endDate: end,
})) {
console.log(row.startDate, row.statusName, row.monitorId);
}List<JsonObject> history = await client.PaginateAsync(
quot;/api/devices/{deviceId}/transfer-history",
new Dictionary<string, object> { ["startDate"] = start, ["endDate"] = end });
foreach (JsonObject row in history)
{
Console.WriteLine(quot;{J.Str(row, "startDate")} {J.Str(row, "statusName")} "
+ quot;{J.Str(row, "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",
})db.insert("deployments", Json.newObj(
"product", product,
"version", version,
"device", device,
"monitorId", transfers.get(device),
"result", succeeded.contains(device) ? "done" : "failed"));await db.insert("deployments", {
product: PRODUCT,
version: VERSION,
device,
monitorId: transfers[device],
result: succeeded.includes(device) ? "done" : "failed",
});await db.InsertAsync("deployments", new
{
product = Product,
version = Version,
device,
monitorId = transfers[device],
result = succeeded.Contains(device) ? "done" : "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 |