Skip to main content

Example: clone tool

This article refers to SDK version v0.5.0. The current SDK version is N/A.

Overview

clone_tool.py is a fuller backup/restore/clean workflow than Clone a node: it saves a node's configuration to a JSON snapshot on disk, restores selected sections of it onto a whole target set of nodes in a single run, or wipes a node's contents entirely. It's useful for recovering a node after a reinstall, keeping an offline template of a "known good" configuration, or rolling a fleet of replacement units forward from the same snapshot.

It has three subcommands, backup, restore, and clean, plus a login command that just reports what the current credentials are allowed to do.

Get the script

Download clone_tool.py from the SDK's examples/ directory. If you haven't installed the SDK yet, see Getting started.

Set up credentials

The script reads the four Barbara API Credentials from environment variables, or from a .env file placed next to it (loaded automatically, or pointed at with --env-file). See Set up credentials in the Hello World walkthrough for the full picture.

See what a snapshot captures

SectionRead fromNotes
workloadsThe node document plus each workloadDocker, Marketplace, Model; App Config, Compose Config, App Secrets, whichever each type has
global_configThe node's Global Config
volumesThe node's docker volumesNames only, volume data isn't captured
secretsThe node's Global SecretsNames only, values are write-only
docker_credentialsThe node's docker credentialsServer and user only, the API never returns the password
networkclient.nodes.networkInterfaces, VLANs, Proxy, NTP servers; only captured with --sections network

backup captures every section above except network by default. restore applies volumes, workloads, and global_config by default, in that order, so a workload that mounts a volume finds it already there. secrets and docker_credentials are captured for visibility, but never restored automatically, there's nothing to restore since values and passwords were never captured, only names, recreate them by hand with client.nodes.create_global_secrets(...)/create_docker_credentials(...).

Walk through the code

print_role_summary() runs at the start of every subcommand, purely as a local heads-up:

token = client._get_token()
claims = decode_jwt_claims(token)

It decodes the access token's own claims to show which of Barbara's four role levels the account holds, Viewer/Editor/Supervisor/Administrator, matching the read/edit/edit_plus/admin roles the SDK's error handling section covers. This is a convenience only, the API's own 401/403 response is always the real authority, not something this tool enforces itself.

resolve_targets() is what lets restore target more than one node at once:

def resolve_targets(client, target_identifiers, group_identifiers, tags) -> list[Node]:
nodes: dict[str, Node] = {}
for identifier in target_identifiers:
nodes[resolve_node(client, identifier).id] = ...
for identifier in group_identifiers:
for node_id in resolve_group(client, identifier).node_ids:
nodes.setdefault(node_id, client.nodes.get(node_id))
if tags:
for node in client.nodes.list_by_tags(tags):
nodes.setdefault(node.id, node)
return list(nodes.values())

Explicit target names/ids, --group, and --tags combine as a union, built with a dict keyed by node id, so a node reachable through more than one of the three is only restored to once. client.nodes.list_by_tags is what makes the --tags selector possible.

backup_workloads() follows the same per-kind shape as Clone a node: Compose Config for Marketplace/Model via get_compose_config, App Secrets for Marketplace only via get_app_secrets, App Config for Docker/Marketplace via the same appConfigId/appConfig extraction. restore_workload() recreates each one the same way apply_workload() does in that script.

backup_network()/restore_network() are new to this script:

def restore_network(client, target_id, network) -> list[dict[str, Any]]:
for iface in network["interfaces"]:
if iface["type"] == "ethernet":
client.nodes.network.update_ethernet_interface(target_id, iface["name"], ...)
elif iface["type"] == "mobile":
client.nodes.network.update_mobile_interface(target_id, iface["name"], ...)
else:
continue # WiFi/VLAN need the PSK/full config this snapshot doesn't decode.
for server in network.get("ntp_servers") or []:
client.nodes.network.create_ntp_server(target_id, server)

Ethernet and Mobile interfaces are reapplied by name, so the target needs an interface with the same name (for example eno1) as the source for that entry to take effect. WiFi and VLAN interfaces are skipped: update_wifi_interface needs the full configuration including the PSK on every call, which this snapshot doesn't capture, and a VLAN is a distinct interface to create, not one to update in place. NTP servers are additive rather than name-matched, so they're just recreated on the target.

cmd_clean() supports a --from-backup FILE option that narrows deletion to only what a given snapshot contains, matching each target's workloads by (application_id, app_version_id) and its volumes by name, instead of deleting everything on the node:

if from_backup is not None:
backup_app_versions = {(w["application_id"], w["app_version_id"]) for w in from_backup["workloads"]}
workload_ids = [wid for wid in workload_ids if _workload_app_version_key(client, node.id, wid) in backup_app_versions]

This is what makes clean --from-backup useful for reverting a specific restore, without touching anything the node already had before it.

Run it

Destructive: restore and clean overwrite the target

restore --apply and clean --apply are destructive, the same way clone_node.py --apply is. clean --apply additionally asks for a typed delete confirmation unless --yes is given. The network section is never touched unless you pass --sections network explicitly, reapplying interface settings, especially a static IP, to a different node can cut its connectivity.

# Show the token's role and what it can do — no other arguments needed.
python clone_tool.py login

# Save a snapshot of a node to disk (workloads, global config, volumes by default).
python clone_tool.py backup my-node -o my-node-backup.json

# Read-only: print what restoring that snapshot to one or more targets would do.
python clone_tool.py restore my-node-backup.json target-node-1 target-node-2

# Actually restore it, stopping the batch at the first target that errors.
python clone_tool.py restore my-node-backup.json target-node-1,target-node-2 --apply --stop-on-error

# Target a group and a tag set instead of naming nodes — the union of both, deduplicated.
python clone_tool.py restore my-node-backup.json --group "Production Line 3" --tags edge,gpu --apply

# Read-only: print what cleaning a node would delete.
python clone_tool.py clean my-node

# Actually delete it (asks for typed confirmation unless --yes).
python clone_tool.py clean my-node --apply

Try it

Run login first against your own credentials to see the role decoded from your token, purely as a local heads-up. Back up a node, then restore it onto itself with --apply, a safe way to exercise the full restore path without touching another node's state. Add --sections network to backup and restore to include Ethernet/Mobile interfaces, VLANs, Proxy, and NTP servers, and read the network warning above before using --apply with it. Combine --group/--tags with an explicit target to see the union at work, a node in both sets only appears once in the results.

Extend it

  • Filter what gets restored by kind. Add a --only <kind> filter to restore to recreate just one workload type.
  • Schedule regular backups. Run backup on a cron to keep a rolling snapshot history per node.
  • Restore WiFi/VLAN interfaces too. restore_network() currently skips them, since their full configuration (PSK, parent interface) isn't captured by backup_network(), capturing and restoring those would need network.raw on each interface rather than just its decoded fields.

Summary

You backed up a node's configuration to disk, restored it onto a batch of targets selected by name, group, or tag, and cleaned a node down to nothing, all behind the same dry-run-unless---apply gate as every other script in this section.

Keeping a snapshot on disk, outside Panel, gives you a recovery path that doesn't depend on the fleet being reachable or healthy at the moment you need it, whether that's a reinstall, a replacement unit, or a known-good template you roll forward to new nodes.

For a lighter, single-target version of the same idea with no snapshot file, see Clone a node. For the full method list, see client.nodes, client.nodes.workloads, and client.nodes.network in the Reference.