Example: clone a node onto another node
This article refers to SDK version v0.5.0. The current SDK version is N/A.
Overview
clone_node.py replicates what a node is running: its workloads (Docker, Marketplace, and Model), their configuration (App Config, Compose Config, and App Secrets, whichever each type has), the node's Global Config, and its named docker volumes, onto another node. It's useful when swapping in a replacement node, or setting up a new one to match an existing template.
This is one of the more involved SDK example scripts: it reads a node's full deployment shape back out of the API, and can recreate it elsewhere. Read the warning below before running it with --apply.
This script clones directly from one source node to one target node, in a single run. For batch targets (by group or tag), saving a backup to disk, or restoring only selected sections later, see Clone tool instead.
Get the script
Download clone_node.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 gets cloned
Workloads (Docker, Marketplace, and Model), their configuration (App Config, Compose Config, and App Secrets, whichever each type has), the node's Global Config, and its named docker volumes are all recreated on the target, the same configuration layers covered in App & global configuration and Docker credentials & volumes.
Two things can't be cloned automatically, because the Barbara API doesn't expose them for reading back, their names are reported instead so nothing is silently dropped:
- Global Secret values are write-only, only their names can be read (see Node & cluster secrets). The plan lists them under
global_secret_names, recreate them manually withclient.nodes.create_global_secrets(...). - Docker credential passwords are never returned by the API either, only the
server/userare. The plan lists those underdocker_credentials_to_recreate, recreate them withclient.nodes.create_docker_credentials(...).
Walk through the code
read_workloads() follows the source node's spaces (its workload ids), reads each one with client.nodes.workloads.get(...), and branches on workload.raw["type"]:
SPACE_DOCKER = 0
SPACE_MARKETPLACE = 1
SPACE_MODEL = 2
These three type codes are the API's own raw values. Marketplace and Model workloads store their application/version reference under different field names than Docker ones (marketApplicationId/marketAppVersionId, modelApplicationId/modelAppVersionId, vs. applicationId/appVersionId), which is what _app_reference() branches on. Each workload's plan only includes the fields that kind of workload actually has: Compose Config for Marketplace and Model, App Config for Docker and Marketplace, App Secrets for Marketplace only, matching the same per-kind shape covered in Marketplace workload deployment and Model deployment.
if kind in (SPACE_MARKETPLACE, SPACE_MODEL):
workload_plan["compose_config"] = client.nodes.workloads.get_compose_config(node_id, workload_id)
if kind == SPACE_MARKETPLACE:
workload_plan["app_secrets"] = client.nodes.workloads.get_app_secrets(node_id, workload_id)
Reading Compose Config and App Secrets back is a single typed call each, get_compose_config and get_app_secrets, already decoded and in the same shape their respective create_*_workload/update_*_workload calls expect, see Marketplace workload deployment for that shape in depth.
_extract_app_config() normalizes a workload's or node's App Config, which comes back as either a reference (appConfigId) or an inline base64-encoded document (appConfig):
config_id = config_block.get("appConfigId")
if config_id and config_id != ZERO_OBJECT_ID:
return {"config_id": config_id}
Barbara uses an all-zero ObjectId as a "not set" sentinel for appConfigId rather than omitting the field, and an unset appConfig still comes back as valid base64 for {}. Both cases need an explicit check rather than a plain truthiness test, or an empty config would be cloned as if it were a real one. The returned dict's config_id key matches the keyword create_docker_workload/create_marketplace_workload take, so apply_workload() can unpack it straight into the call with **.
read_docker_volumes() and read_docker_credentials() follow the same read-only-what-you-can pattern, client.nodes.list_docker_volumes(...)/list_docker_credentials(...), each wrapped in a try/except BarbaraApiError that downgrades a permission error on these optional sub-resources to a warning on stderr rather than failing the whole clone, the same treatment read_global_config() and read_global_secret_names() give Global Config and Global Secrets.
apply_docker_volumes() creates each of the source's named volumes on the target, empty, a volume's data itself is never copied, only its name:
existing = {v.get("name") for v in client.nodes.list_docker_volumes(target_id)}
for volume in source_volumes:
if volume["name"] in existing:
results.append({"name": volume["name"], "status": "skipped (already exists on target)"})
continue
client.nodes.create_docker_volume(target_id, volume["name"])
clean_target() deletes every workload currently on the target, returning the deleted ids, so applying the plan can't produce duplicates. A workload's App Config is a sub-resource of the workload itself, so deleting the workload removes it too; volumes are handled separately by apply_docker_volumes() above, since they're node-level, not workload-level.
main() assembles a plan (source, target, docker_volumes, workloads, global_config, global_secret_names, docker_credentials_to_recreate) and, without --apply, just prints it. With --apply, it cleans the target's workloads, creates the volumes, recreates each workload via apply_workload(), applies the Global Config, and warns about any secrets or docker credentials you'll need to recreate by hand.
Run it
With --apply, this script deletes every workload currently on the target node first, so re-running a clone doesn't produce duplicates. It's meant to fully overwrite the target's workloads, not merge into them. Only point it at a node you intend to wipe. Pre-existing docker volumes are the exception: they're never deleted, only skipped if their name collides with a cloned one, since a volume may hold real data this example has no way to restore.
# Read-only: print the clone plan. Nothing is deleted or created.
python clone_node.py source-node target-node
# Delete the target's existing workloads, create volumes + the cloned workloads.
python clone_node.py source-node target-node --apply
Try it
Run it without --apply first and inspect the plan's workloads and docker_volumes lists before touching the target node. Then point it at a source node with a mix of Docker, Marketplace, and Model apps, so all three creation paths get exercised. After an --apply run, check "cleaned_workloads" to see which of the target's previous workloads were removed, "docker_volumes" for which volumes were created vs. skipped (already present on the target), and "secrets_to_recreate"/"docker_credentials_to_recreate" in the plan: since those values can't be read back through the public API, names/server/user are surfaced instead of silently dropped.
Extend it
- Clone to several targets. Wrap the
--applystep in a loop over a list of target nodes, or resolve them from a group, instead of hardcoding a single target, or reach for Clone tool, which already does this. - Recreate secrets interactively. Prompt for each secret's value using
secrets_to_recreateas the list of names to ask for, then create it on the target withclient.nodes.create_global_secrets(...). - Filter what gets cloned. Add a
--onlyflag (for example--only marketplace) so the script clones just one workload type instead of everything.
Summary
You read a node's full deployment shape, workloads, per-workload configuration, Global Config, and named volumes, and replicated it onto another node, with a safe read-only plan by default.
That plan-then-apply shape, read everything first, act only when explicitly told to, is worth carrying into any script of your own that changes fleet state.
For batch targets, saved snapshots, and a clean/wipe mode on top of the same idea, continue to Clone tool. For the full method list behind everything you used here, see client.nodes.workloads and client.nodes in the Reference.