Skip to main content

vME APIs

Overview​

vME's API allows you to programatically manage the vME instances and Docker engine.

Base URL​

The API is served over HTTPS and reverse-proxied under the /api path on the appliance. Every endpoint documented here is relative to that base:

https://<your-vme-host>/api

For example, the Show all pools route documented as /pool/show is called at https://<your-vme-host>/api/pool/show.

To keep the examples below concise, they assume a base_url (Python) / BASE_URL (curl) variable that you set once to your appliance's API base:

# Set this once; every example below reuses it.
base_url = "https://<your-vme-host>/api"

Authentication​

All endpoints are authenticated according to the following headers schema:

{
"username" : "xxxxxx", # your username
"api-key" : "xxxxxx" # your api-key
}

A request that leaves out either header, or whose username and api-key do not match a user, is refused with a JSON body carrying "status": 0 and a message. The status code is 401, or 400 on the routes that check for a missing header before checking the key.

The examples assume these are defined once as headers (Python) or passed as -H flags (curl). Some routes additionally require an administrator account; those are marked Admin only in this reference. A valid username and api-key that belong to a non-administrator account are refused on those routes with 403.

Getting an API key​

Sign in with POST /login to get your api-key. The key is tied to your account and stays the same on every sign-in until it is rotated (see Modify User and Modify a User (admin)), so you can sign in once and reuse it.

POST /login

  • AUTHENTICATIONS: none (username and password in the body)

  • REQUEST BODY SCHEMA:

    {
    "username" : "jane",
    "password" : "MyPassword123!"
    }
    NameTypeDescription
    username
    required
    StringYour login name. A value containing @ is matched against the account's email address instead.
    password
    required
    StringYour password. For an LDAP account, this is your directory password.
    method
    optional
    Stringpassword (local account) or ldap. When given, it must match how the account is set up, or the sign-in is refused with 401. When omitted, vME uses the account's own sign-in type.

    Both local and LDAP accounts sign in through this route. An account set up as ldap is checked against the configured LDAP directory. A username that vME does not know yet is also tried against LDAP (unless method is password); when the LDAP configuration has auto_provision switched on, a successful sign-in creates the vME account as a non-administrator.

  • RESPONSE: 200 on success. api-key is the value to send in the api-key header; is_admin tells you whether the account can call the Admin only routes. Wrong credentials answer 401; a missing username or password answers 400.

    {
    "action" : "sign in",
    "status" : 1,
    "message" : "log in success",
    "username" : "jane",
    "api-key" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "group_id" : 1,
    "is_admin" : false
    }
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "username" : "jane",
    "password" : "MyPassword123!"
    }
    response = requests.post(f"{base_url}/login", json=body)
    login = response.json()
    if login.get("status") == 1:
    # Reuse these headers for every other call in this reference.
    headers = {
    "username" : login["username"],
    "api-key" : login["api-key"]
    }
    else:
    print(login)

Show your own account​

GET /user returns the profile of the account named in the headers, including its api-key, group and sign-in type. Use it to check that a username and api-key pair is valid.

GET /user

  • AUTHENTICATIONS: headers

  • RESPONSE:

    {
    "action" : "get user info",
    "status" : 1,
    "message" : "log in success",
    "username" : "jane",
    "name" : "Jane Doe",
    "mail" : "jane@company.com",
    "api-key" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "group_id" : 1,
    "is_admin" : false,
    "auth_type" : "local"
    }
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    response = requests.get(f"{base_url}/user", headers=headers)
    if response.status_code < 500:
    print(response.json())
Automating agents and onboarding workflows?

The admin API for managing agents, dispatching jobs, and orchestrating onboarding workflows is documented below under Agent API and Workflow API. The appliances a workflow can send to are registered through the Appliance Registry.

Disk Partition Details​

This API call retrieves all disk details like name, partitions, model and size.

GET /zfs/disks

  • AUTHENTICATIONS: headers

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    response = requests.get(f"{base_url}/zfs/disks", headers=headers)
    if response.status_code < 500:
    print(response.json())

Pool Routes​

Show all pools​

This API Call is used to show all the current pools that are existing.

GET /pool/show

  • AUTHENTICATIONS: headers

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    response = requests.get(f"{base_url}/pool/show", headers=headers)
    if response.status_code < 500:
    print(response.json())

Create a pool​

Here, we are creating a pool from this API call and the respective body schema

POST /pool/create

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

      {
    "name" : "vme-pool",
    "device" : ["test1","test2"]
    }
    NameTypeDescription
    name
    required
    String
    Name of pool to be created.
    device
    required
    ListList of device names.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "name" : "vme-pool",
    "device" : ["nvme0n2", "nvme0n3"]
    }
    response = requests.post(f"{base_url}/pool/create", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Destroy a pool​

This API call is responsible for destroying a pool that already exists

DELETE /pool/destroy

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

      {
    "name" : "vme-pool"
    }
    NameTypeDescription
    name
    required
    StringName of pool to be destroyed.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "name" : "vme-pool"
    }
    response = requests.delete(f"{base_url}/pool/destroy", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Show current status of a pool​

This API call will let us know what the current status is for a specific pool, which could be active,creating or destroyed.

GET /pool/status

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

      {
    "name" : "vme-pool"
    }
    NameTypeDescription
    name
    required
    StringThe name of the pool to show status on.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    params = {
    "name" : "vme-pool"
    }
    response = requests.get(f"{base_url}/pool/status", params=params, headers=headers)
    if response.status_code < 500:
    print(response.json())

Show history of a pool​

Here, we can check all the operations that have been performed on that pool, from when it was created, till it has been destroyed.

GET /pool/history

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

      {
    "name" : "vme-pool"
    }
    NameTypeDescription
    name
    required
    StringThe name of the pool to run the history command on.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    params = {
    "name" : "vme-pool"
    }
    response = requests.get(f"{base_url}/pool/history", params=params, headers=headers)
    if response.status_code < 500:
    print(response.json())

Register a pool​

Here we can register the pool for creation of sources and other operations like cloning and snapshotting.

POST /pool/register

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

      {
    "name" : "vme-pool"
    }
    NameTypeDescription
    name
    required
    String
    The name of the pool to be registered.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {"name" : "vme-pool"}
    response = requests.post(f"{base_url}/pool/register", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Source Routes​

Show all sources​

This API call returns the sources in one pool, or in every pool.

GET /source/show

  • AUTHENTICATIONS: headers

  • QUERY PARAMETERS:

    NameTypeDescription
    pool_name
    optional
    StringName of the pool to list. If omitted, the sources in all pools are listed.
    source_name
    optional
    StringList only the sources nested under this source. With pool_name, give the source's short name (foo); without it, give the full name (vme-pool/sources/foo).
    properties
    optional
    StringZFS properties to include for each source, comma separated or repeated (for example properties=used,quota). An unknown property is refused with 400.
  • RESPONSE: result is the list of sources and source_names their full names. Each source carries, beside its ZFS properties, its status and dbtype and the fields described in What a source or clone listing carries.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    params = {
    "pool_name" : "vme-pool"
    }
    response = requests.get(f"{base_url}/source/show", params=params, headers=headers)
    if response.status_code < 500:
    print(response.json())

What a source or clone listing carries​

Each row returned by Show all sources and Show all clones carries these fields beside its ZFS properties:

FieldDescription
guidThe dataset's ZFS guid. It identifies the dataset whatever it is called, and does not change when the dataset is renamed.
iqn_nameFor a dataset exported over iSCSI, the target name hosts connect to, otherwise null. It is built from the guid, as iqn.<yyyy-mm>.com.enov8:g-<guid>, so it stays the same when the dataset is renamed.
iqn_schemeguid when iqn_name is built from the guid; name when it is built from the dataset's name.
attached_toWhere the dataset is in use. See the fields below. null for a dataset that is still being created.
used_byThe unfinished onboarding workflows using the dataset, each as { "workflow_id": "...", "name": "..." }. Empty when none is.

attached_to holds:

FieldDescription
typeHow the dataset is reached: iscsi, nfs, or container.
nameWho has it: the host a workflow mounted it on, or for iSCSI the host whose initiator is connected; the NFS clients it is exported to; or the container bound to it.
detailWhere: the folder on the host a workflow mounted it at, or the dataset's mountpoint for a container.
livetrue when that is so right now, false when it is not, null when vME cannot tell (NFS).
stateA word for the screen: connected or not connected (iSCSI), exported (NFS), running or stopped (container), recorded, or not attached.
{
"name" : "vme-pool/clones/app01-test",
"guid" : "8214416316530251783",
"iqn_name" : "iqn.2026-09.com.enov8:g-8214416316530251783",
"iqn_scheme" : "guid",
"attached_to" : {
"type" : "iscsi",
"name" : "SQL-B",
"detail" : "C:\\vME\\clones\\8214416316530251783",
"live" : true,
"state" : "connected"
},
"used_by" : []
}

Create a source​

We can create a source under a particular pool name along with other properties like size/quote,compression type, database method and so on.

POST /source/create

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    • database method: container
      {
      "pool_name" : "vme-pool",
      "name" : "foo",
      "quota" : "1G",
      "compression" : "lzjb"
      }
    • database method network, NFS
      {
      "pool_name" : "vme-pool",
      "name" : "foo",
      "compression" : "lzjb",
      "database_method" : "network",
      "network_info" : {
      "type" : "nfs"
      }
      }
    • database method network, iSCSI
      {
      "pool_name" : "vme-pool",
      "name" : "foo",
      "compression" : "lzjb",
      "database_method" : "network",
      "network_info" : {
      "type" : "iscsi",
      "auto_grow" : true,
      "auto_grow_max_gb" : 16384
      }
      }
    NameTypeDescription
    name
    required
    StringName of source to be created.
    pool_name
    required
    StringName of pool that contains a source.
    dbtype
    optional
    StringDatabase type, such as mysql, mariadb, mssql, mongodb etc., used to label a source, default is unknown.
    quota
    optional
    StringA limit on the amount of disk space a dataset can consume, for example, 10G for ten GB.
    compression
    optional
    StringName of process where data is stored using less disk space, the name can be one of on, off, lzjb, gzip, gzip-1, gzip-2, gzip-3, gzip-4, gzip-5, gzip-6, gzip-7, gzip-8, gzip-9, zle, lz4, False, default is False.
    database_method
    optional
    Stringcontainer (default) or network. Use network to expose the source over NFS or iSCSI so an external host can mount it.
    network_info
    optional
    DictionaryRequired when database_method is network. Holds the network settings below (at minimum type).
    network_info.type
    required
    Stringnfs or iscsi. Required inside network_info.
    network_info.auto_grow
    optional
    BooleaniSCSI only. When true, the volume is thin-provisioned and grows on demand up to auto_grow_max_gb. Default false.
    network_info.auto_grow_max_gb
    optional
    IntegeriSCSI only. Maximum size (GB) the volume can grow to when auto_grow is enabled. Default 16384.
    network_info.vsize
    optional
    IntegeriSCSI only. Fixed volume size (GB). Used when auto_grow is false; if omitted or 0, the volume auto-grows.
    workflow_id
    optional
    StringThe onboarding workflow this source is created for. If that workflow has been cancelled, the call is refused with 409. See Linking calls to a workflow.
    wizard_step, role
    optional
    StringAccepted so a script can send the same linkage fields to every call. This call does not mark a workflow step complete; the source step is recorded when you publish the source with /db/network/create carrying workflow_id and role.
    note

    For iSCSI, creating a source with database_method: network provisions the iSCSI target and LUN automatically as part of source creation - no separate call is needed to expose it. For NFS, use /db/network/create after creating the source to publish the share and set the client allow-list (client_address).

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "pool_name" : "vme-pool",
    "name" : "foo"
    }
    response = requests.post(f"{base_url}/source/create", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Modify a source​

This API call is used to understand and modify the source properties as per use case.

POST /source/modify

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

      {
    "pool_name" : "vme-pool",
    "name" : "foo",
    "property" : {
    "quota":"5G",
    "dbtype":"mysql",
    "compression":"gzip"
    }
    }
    NameTypeDescription
    name
    required
    StringName of source to be modified.
    pool_name
    required
    StringName of pool that contains a source.
    property
    required
    DictionaryThe properties of the source to be modified, for example quota, compression, dbtype (the source's label) or client_address.
    note

    database_method is set when the source is created and cannot be changed afterwards; a database_method inside property is ignored. The source must be in the available state; a source that is still being created or changed is refused with 400.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "pool_name" : "vme-pool",
    "name" : "foo",
    "property" : {
    "quota" : "5G",
    "dbtype" : "mysql",
    "compression" : "gzip"
    }
    }
    response = requests.post(f"{base_url}/source/modify", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Rename a source​

PUT /source/rename

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "pool_name" : "vme-pool",
    "currentname" : "foo",
    "newname" : "foofoo"
    }
    NameTypeDescription
    pool_name
    required
    StringName of pool that contains a source.
    currentname
    required
    StringName of source to be renamed.
    newname
    required
    StringNew source name.
  • RESPONSE: On success, source_name holds the new full name and result describes the export:

    {
    "status" : 1,
    "message" : "Source [vme-pool/sources/foo -> vme-pool/sources/foofoo] rename success",
    "source_name" : "vme-pool/sources/foofoo",
    "result" : {
    "iscsi" : true,
    "iqn_name" : "iqn.2026-09.com.enov8:g-8214416316530251783",
    "backstore_name" : "g-8214416316530251783",
    "warning" : null,
    "workflows_holding_the_previous_name" : []
    }
    }

    workflows_holding_the_previous_name lists finished onboarding workflows that recorded the old name. If the appliance could not save its iSCSI target configuration after the rename, warning says so and is added to message.

  • NOTES:

    • A source exported over iSCSI keeps its target name and backstore when it is renamed, and a host that has it connected keeps its session. The appliance updates its saved target configuration so the export is restored after a restart.
    • A source exported over NFS cannot be renamed: its export path is the dataset's mountpoint. The call is refused with 400.
    • A source an unfinished onboarding workflow is using is refused with 409. message names the workflows, and used_by lists them as { "workflow_id", "name" }.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "pool_name" : "vme-pool",
    "currentname" : "foo",
    "newname" : "foofoo"
    }
    response = requests.put(f"{base_url}/source/rename", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Destroy a source​

This is used to remove a source listed under that specific pool.

DELETE /source/destroy

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "pool_name" : "vme-pool",
    "source_name" : "foo"
    }
    NameTypeDescription
    pool_name
    required
    StringName of pool that contains a source.
    source_name
    required
    StringName of source to be destroyed.
    recursive
    optional
    BooleanThe recursive option, default is False.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "pool_name" : "vme-pool",
    "source_name" : "foo"
    }
    response = requests.delete(f"{base_url}/source/destroy", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Snapshot Routes​

Show all snapshots​

GET /snapshot/show

This API call is used to list all snapshots taken under the particular source or clone, under one pool, or across all pools.

  • AUTHENTICATIONS: headers

  • QUERY PARAMETERS:

    NameTypeDescription
    pool_name
    optional
    StringName of pool. If omitted, the snapshots in all pools are listed and source_name is ignored.
    source_name
    optional
    StringList only the snapshots of this source or clone. A short name (foo) is a source; to name a clone, give its full name (vme-pool/clones/foo-clone).
    properties
    optional
    StringZFS properties to include for each snapshot, comma separated or repeated.
    sort
    optional
    BooleanWhen true, snapshot_names is in the order the snapshots were created.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    params = {
    "pool_name" : "vme-pool"
    }
    response = requests.get(f"{base_url}/snapshot/show", params=params, headers=headers)
    if response.status_code < 500:
    print(response.json())

Create a snapshot​

POST /snapshot/create

This is used to create a snapshot under the particular source name which can be used for cloning.

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "pool_name" : "vme-pool",
    "source_name" : "foo",
    "name" : "snap"
    }
    NameTypeDescription
    pool_name
    required
    StringName of pool.
    source_name
    required
    StringName of source/clone.
    name
    required
    StringName of snapshot/bookmark to be created.
    snapof
    optional
    StringWhether source_name names a source or a clone. Default is source. Send "snapof": "clone" to snapshot a clone. Not needed when source_name is a full name such as vme-pool/clones/foo-clone, which already says which it is.
    recursive
    optional
    BooleanRecursive option, default is False.
    skip_host
    optional
    BooleanTake the snapshot without asking the host that has the source mounted to quiesce it first. Default is false. See the note below.
    workflow_id
    optional
    StringThe onboarding workflow this snapshot is taken for. On success, the workflow's step is marked complete. See Linking calls to a workflow.
    wizard_step
    optional
    StringThe workflow step to mark complete. Default is snapshot.
    role
    optional
    StringThe dataset role this snapshot belongs to, for example data.
  • RESPONSE: On success the response includes a stopped_containers array listing any containers that were stopped to take a crash-consistent snapshot (empty if none).

    {
    "status": 1,
    "message": "Snapshot [vme-pool/sources/foo@snap] create success",
    "pool_name": "vme-pool",
    "source_name": "vme-pool/sources/foo",
    "snapshot_name": "vme-pool/sources/foo@snap",
    "recursive": false,
    "stopped_containers": ["container-a", "container-b"]
    }
    A source or clone a host has mounted

    If a SQL Server host has this dataset attached over iSCSI, vME does not change it under the host. It asks the host's agent to take the databases on the volume offline and the disk with them (Quiesce), makes the snapshot, and brings them back (Resume). Resume runs even when the snapshot fails. The answer tells you which of these happened:

    StatusWhenBody
    200No host has the dataset attached, or skip_host is true.The snapshot is done, as before.
    202A host has it attached and its agent is online.{"task_id": "...", "plan": {"dataset", "host", "agent_id", "targets", "mount_path"}}. Follow it with Follow a coordinated snapshot or rollback.
    409A host has it attached and no online agent can reach it; another coordinated operation is already running on that host; or vME could not work out whether a host has it.message says which, and for an unreachable host lists the steps to do it by hand. Nothing was changed.

    If the host's Quiesce fails, the snapshot is not made. Set skip_host to true only when you have taken the databases on that volume offline yourself.

    Crash-consistent snapshots

    If the source has a container mount, vME finds any running containers bind-mounted on that source and stops them before taking the snapshot, so the captured data is consistent. The containers that were stopped are returned in stopped_containers.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "pool_name" : "vme-pool",
    "source_name" : "foo",
    "name" : "snap"
    }
    response = requests.post(f"{base_url}/snapshot/create", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Rollback a snapshot​

POST /snapshot/rollback

This functionality is used to go back to the last saved instance of the snapshot previously captured

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "pool_name" : "vme-pool",
    "source_name" : "foo",
    "snapshot_name" : "snap",
    "force" : true
    }
    NameTypeDescription
    pool_name
    required
    StringName of pool.
    source_name
    required
    StringName of source/clone.
    snapshot_name
    required
    StringName of the snapshot/bookmark to roll back to.
    snapof
    optional
    StringWhether source_name names a source or a clone. Default is source. Send "snapof": "clone" to roll back a clone. Not needed when source_name (or snapshot_name) is a full name such as vme-pool/clones/foo-clone@snap.
    force
    optional
    BooleanForce option to rollback, default is False.
    skip_host
    optional
    BooleanRoll back without asking the host that has the dataset mounted to quiesce it first. Default is false.
    A source or clone a host has mounted

    If a SQL Server host has this dataset attached over iSCSI, vME does not change it under the host. It asks the host's agent to take the databases on the volume offline and the disk with them (Quiesce), makes the rollback, and brings them back (Resume). Resume runs even when the rollback fails. The answer tells you which of these happened:

    StatusWhenBody
    200No host has the dataset attached, or skip_host is true.The rollback is done, as before.
    202A host has it attached and its agent is online.{"task_id": "...", "plan": {"dataset", "host", "agent_id", "targets", "mount_path"}}. Follow it with Follow a coordinated snapshot or rollback.
    409A host has it attached and no online agent can reach it; another coordinated operation is already running on that host; or vME could not work out whether a host has it.message says which, and for an unreachable host lists the steps to do it by hand. Nothing was changed.

    If the host's Quiesce fails, the rollback is not made. Set skip_host to true only when you have taken the databases on that volume offline yourself.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "pool_name" : "vme-pool",
    "source_name" : "foo",
    "snapshot_name" : "snap",
    "force" : True
    }
    response = requests.post(f"{base_url}/snapshot/rollback", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Follow a coordinated snapshot or rollback​

GET /snapshot/task/<task_id>

How a snapshot or rollback that returned 202 is getting on.

  • AUTHENTICATIONS: headers
  • RESPONSE:
    {
    "status": 1,
    "task_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "state": "PROGRESS",
    "plan": { "dataset": "vme-pool/sources/foo", "host": "SQL-01", "agent_id": "...", "targets": ["iqn..."], "mount_path": "C:\\vME\\sources\\..." },
    "steps": [
    { "key": "quiesce", "label": "Quiesce databases on SQL-01", "state": "completed", "message": "...", "job_id": "..." },
    { "key": "zfs", "label": "Bookmark", "state": "running", "message": "", "job_id": null },
    { "key": "resume", "label": "Resume databases on SQL-01", "state": "pending", "message": "", "job_id": null }
    ],
    "result": null
    }
    FieldDescription
    statePENDING (not started, or no longer known), PROGRESS, SUCCESS or FAILURE.
    stepsAlways the three steps, in order. The middle step is labelled Bookmark for a snapshot and Rollback for a rollback. Each state is pending, queued, running, completed, failed or skipped; job_id is the agent job for the host steps.
    resultOnce finished, what happened, with a message.
  • NOTES: Poll until state is SUCCESS or FAILURE. A failure names the step that failed; when Quiesce fails the ZFS step is skipped, and Resume still runs.

Rename a snapshot​

PUT /snapshot/rename

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "pool_name" : "vme-pool",
    "source_name" : "foo",
    "currentname" : "snap",
    "newname" : "snap-new"
    }
    NameTypeDescription
    pool_name
    required
    StringName of pool.
    source_name
    required
    StringName of source/clone.
    currentname
    required
    StringName of snapshot/bookmark to be renamed.
    newname
    required
    StringNew name of snapshot/bookmark.
    snapof
    optional
    StringWhether source_name names a source or a clone. Default is source. Send "snapof": "clone" to rename a clone's snapshot.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "pool_name" : "vme-pool",
    "source_name" : "foo",
    "currentname" : "snap",
    "newname" : "snap-new"
    }
    response = requests.put(f"{base_url}/snapshot/rename", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Destroy a snapshot​

DELETE /snapshot/destroy

Helps remove a previously existing snapshot in the source list.

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "pool_name" : "vme-pool",
    "source_name" : "foo",
    "snapshot_name" : "snap"
    }
    NameTypeDescription
    pool_name
    required
    StringName of pool.
    source_name
    required
    StringName of source/clone.
    snapshot_name
    required
    StringName of snapshot/bookmark to be destroyed.
    snapof
    optional
    StringWhether source_name names a source or a clone. Default is source. Send "snapof": "clone" to destroy a clone's snapshot, or give the full name (vme-pool/clones/foo-clone@snap).
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "pool_name" : "vme-pool",
    "source_name" : "foo",
    "snapshot_name" : "snap"
    }
    response = requests.delete(f"{base_url}/snapshot/destroy", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Clone Routes​

Show all clones​

GET /clone/show

Lists down all available clones listed under that specific source

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "pool_name" : "vme-pool",
    "source_name" : "foo",
    "snapshot_name" : "snap"
    }
    NameTypeDescription
    pool_name
    optional
    StringName of pool.
    source_name
    optional
    StringName of source.
    snapshot_name
    optional
    StringName of snapshot.
  • RESPONSE: Each clone carries, beside its ZFS properties and its source and snapshot, the fields described in What a source or clone listing carries.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    params = {
    'pool_name' : 'vme-pool'
    }
    response = requests.get(f"{base_url}/clone/show", params=params, headers=headers)
    if response.status_code < 500:
    print(response.json())

Create a clone​

POST /clone/create

This API call assistsis responsible in creating a copy from the snapshot that has been captured.

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    • database method container (default)
      {
      "pool_name" : "vme-pool",
      "source_name" : "foo",
      "snapshot_name" : "snap",
      "name" : "foo-clone"
      }
    • database method network, iSCSI
      {
      "pool_name" : "vme-pool",
      "source_name" : "foo",
      "snapshot_name" : "snap",
      "name" : "foo-clone",
      "database_method" : "network",
      "network_info" : {
      "type" : "iscsi"
      }
      }
    NameTypeDescription
    pool_name
    required
    StringName of pool.
    source_name
    required
    StringName of the source (or clone) the snapshot was taken of.
    snapshot_name
    required
    StringName of snapshot. A full name such as vme-pool/sources/foo@snap can be given instead of pool_name, source_name and snapshot_name.
    snapof
    optional
    StringWhether source_name names a source or a clone. Default is source. Send "snapof": "clone" to clone a snapshot of a clone.
    name
    optional
    StringName of clone to be created. If omitted, the clone is named <snapshot>.<unix time>, for example snap.1760918400.
    dbtype
    optional
    StringDatabase type, such as mysql, mariadb, mssql, mongodb etc., used to label a source, default is unknown.
    size
    optional
    IntegerNumber of clones (clonefarm) to be created, default is 1.
    database_method
    optional
    Stringcontainer (default) or network. Use network to expose the clone over NFS or iSCSI so an external host can mount it.
    network_info
    optional
    DictionaryRequired when database_method is network. Must contain type (nfs or iscsi). A clone inherits its size from the snapshot, so no vsize is needed.
    network_info.type
    required
    Stringnfs or iscsi. Required inside network_info.
    workflow_id
    optional
    StringThe onboarding workflow this clone is created for. On success, the workflow's step is marked complete. See Linking calls to a workflow.
    wizard_step
    optional
    StringThe workflow step to mark complete. Default is clone.
    role
    optional
    StringThe dataset role this clone belongs to, for example data.
  • RESPONSE: The clone is created in the background; the call returns once it is queued. clone_name is the full name of the new clone; poll Show all clones for its status.

    {
    "status": 1,
    "message": "Clone [vme-pool/clones/foo-clone] create success",
    "pool_name": "vme-pool",
    "source_name": "vme-pool/sources/foo",
    "snapshot_name": "vme-pool/sources/foo@snap",
    "clone_name": "vme-pool/clones/foo-clone"
    }
    note

    For an iSCSI clone, the iSCSI target and LUN are created automatically as part of clone creation - no separate call is needed to expose it. For NFS, use /db/network/create after the clone is created to publish the share and set the client allow-list.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "pool_name" : "vme-pool",
    "source_name" : "foo",
    "snapshot_name" : "snap",
    "name" : "foo-clone"
    }
    response = requests.post(f"{base_url}/clone/create", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Modify a clone​

POST /clone/modify

Here we can modify the properties of the clone based on the compression type, db type, quota and so on.

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "pool_name" : "vme-pool",
    "name" : "foo-clone",
    "property" : {
    "quota" : "5G",
    "dbtype" : "mysql",
    "compression" : "gzip"
    }
    }
    NameTypeDescription
    name
    required
    StringName of clone to be modified.
    pool_name
    required
    StringName of pool that contains the clone.
    property
    required
    DictionaryThe properties of the clone to be modified, for example quota, compression, dbtype (the clone's label) or client_address.
    note

    database_method is set when the clone is created and cannot be changed afterwards; a database_method inside property is ignored.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "pool_name" : "vme-pool",
    "name" : "foo-clone",
    "property" : {
    "quota" : "5G",
    "dbtype" : "mysql",
    "compression" : "gzip"
    }
    }
    response = requests.post(f"{base_url}/clone/modify", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Rename a clone​

PUT /clone/rename

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "pool_name" : "vme-pool",
    "currentname" : "foo-clone",
    "newname" : "foofoo-clone"
    }
    NameTypeDescription
    pool_name
    required
    StringName of pool.
    currentname
    required
    StringName of clone to be renamed.
    newname
    required
    StringNew name of clone.
  • RESPONSE: On success, clone_name holds the new full name and result describes the export, as for Rename a source.

  • NOTES:

    • A clone exported over iSCSI keeps its target name and backstore when it is renamed, and a host that has it connected keeps its session. The appliance updates its saved target configuration so the export is restored after a restart.
    • A clone exported over NFS cannot be renamed: its export path is the dataset's mountpoint. The call is refused with 400.
    • A clone an unfinished onboarding workflow is using is refused with 409. message names the workflows, and used_by lists them as { "workflow_id", "name" }.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "pool_name" : "vme-pool",
    "currentname" : "foo-clone",
    "newname" : "foofoo-clone"
    }
    response = requests.put(f"{base_url}/clone/rename", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Destroy a clone​

DELETE /clone/destroy

Removes an existing clone under the specified pool

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "pool_name" : "vme-pool",
    "clone_name" : "foo-clone"
    }
    NameTypeDescription
    pool_name
    required
    StringName of pool.
    clone_name
    required
    StringName of clone to be destroyed.
    recursive
    optional
    BooleanThe recursive option, default is False.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "pool_name" : "vme-pool",
    "clone_name" : "foo-clone"
    }
    response = requests.delete(f"{base_url}/clone/destroy", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Object Routes​

Destroy several objects at once​

Destroy a selection of sources, snapshots and clones in one request, with one result per object.

DELETE /object/destroy

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "objects" : [
    { "kind": "clone", "pool_name": "vme-pool", "name": "vme-pool/clones/app01-test" },
    { "kind": "snapshot", "pool_name": "vme-pool", "name": "vme-pool/sources/app01@nightly" },
    { "kind": "source", "pool_name": "vme-pool", "name": "app01" }
    ],
    "force" : false
    }
    NameTypeDescription
    objects
    required
    ListBetween 1 and 100 objects.
    objects[].pool_name
    required
    StringThe pool the object is in.
    objects[].name
    required
    StringThe object's full name (vme-pool/sources/app01, vme-pool/clones/app01-test), or just the name of a source or clone (app01). A snapshot is always named in full, as <dataset>@<snapshot>.
    objects[].kind
    optional
    Stringsource, snapshot or clone. When left out, a name with @ is a snapshot, a name with /clones/ is a clone, and anything else is a source.
    force
    optional
    BooleanDestroy objects an unfinished onboarding workflow is still using. Default false.
  • ORDER: Clones are destroyed first, then snapshots, then sources, whatever order you list them in. A snapshot that still has a clone, or a source that still has snapshots, is not destroyed unless those are in the same selection.

  • RESPONSE: 200 with a result entry per object, in the order you sent them, and counts:

    {
    "status" : 1,
    "result" : [
    { "key": "clone:vme-pool/clones/app01-test", "kind": "clone", "name": "vme-pool/clones/app01-test",
    "ok": true, "pending": false, "message": "destroyed" },
    { "key": "snapshot:vme-pool/sources/app01@nightly", "kind": "snapshot", "name": "vme-pool/sources/app01@nightly",
    "ok": true, "pending": false, "message": "destroyed" },
    { "key": "source:vme-pool/sources/app01", "kind": "source", "name": "vme-pool/sources/app01",
    "ok": false, "pending": false,
    "message": "has 2 snapshot(s) (vme-pool/sources/app01@weekly, vme-pool/sources/app01@monthly); delete them first, or select them with it",
    "snapshots": ["vme-pool/sources/app01@weekly", "vme-pool/sources/app01@monthly"] }
    ],
    "counts" : { "destroyed": 2, "in_progress": 0, "refused": 0, "failed": 1 }
    }
    FieldDescription
    ok, pendingok: true, pending: false - destroyed. ok: true, pending: true - the destroy of a source or clone has started but had not finished when the answer was sent; check it with Show all sources or Show all clones. ok: false - not destroyed; message says why.
    messagedestroyed, destroy in progress, or the reason it was not destroyed, for example has 2 snapshot(s) (...); delete them first, or select them with it, has 1 clone(s) (...); delete them first, or select them with it, or in use by an unfinished onboarding workflow (<workflow names>), so it was not destroyed. When something earlier in the same selection was still being destroyed, the message says so and waiting_on names its kind: run the request again once it has gone.
    refused, used_bySet on an object an unfinished onboarding workflow is using: used_by lists those workflows as { "workflow_id", "name" }. Send force: true to destroy it anyway.
    snapshots, clonesThe snapshots or clones that kept a source or snapshot from being destroyed.
    warningSet when a workflow had mounted the object on a host, for example was mounted on SQL-B; check the host afterwards.
  • ERRORS: 400 when objects is missing or empty, holds more than 100 entries, has an entry without pool_name and name, or names a snapshot without <dataset>@. Nothing is destroyed.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "objects" : [
    { "pool_name": "vme-pool", "name": "vme-pool/clones/app01-test" },
    { "pool_name": "vme-pool", "name": "vme-pool/sources/app01@nightly" }
    ]
    }
    response = requests.delete(f"{base_url}/object/destroy", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Docker Image Routes​

Pull an image​

Similar to the docker pull command.

POST /docker/image/pull

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "repository" : "mysql",
    "tag" : "8.4.0",
    "credential" : {"username":"","password":""}
    }
    NameTypeDescription
    repository
    required
    StringName of image repository to be pulled.
    tag
    optional
    StringName of tag, default is latest.
    dbtype
    optional
    StringDatabase type, such as mysql, mariadb, mssql, mongodb etc., used to label a source, default is same as repository name.
    credential
    optional
    DictionaryInformation used to verify repository accession, default is None.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "repository" : "mysql",
    }
    response = requests.post(f"{base_url}/docker/image/pull", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Register an image​

POST /docker/image/register

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "name" : "mysql:latest",
    "dbtype" : "mysql"
    }
    NameTypeDescription
    name
    required
    StringName of image name:tag to be registered.
    dbtype
    optional
    StringDatabase type, such as mysql, mariadb, mssql, mongodb etc., used to label a source, default is same as repository name.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "name" : "mysql:latest",
    "dbtype" : "mysql"
    }
    response = requests.post(f"{base_url}/docker/image/register", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

List all images​

Similar to the docker images command.

GET /docker/image/list

  • AUTHENTICATIONS: headers

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    response = requests.get(f"{base_url}/docker/image/list", headers=headers)
    if response.status_code < 500:
    print(response.json())

Save an image as .tar​

POST /docker/image/save

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "image" : "mysql:latest",
    "filename" : "myimage.tar"
    }
    NameTypeDescription
    image
    required
    StringName of image name:tag to be saved.
    filename
    required
    StringFilename of saved image, the tar file will be saved at /home/vme/uploads.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "image" : "mysql:latest",
    "filename" : "myimage.tar"
    }
    response = requests.post(f"{base_url}/docker/image/save", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Load an image from .tar​

POST /docker/image/load

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "filename" : "myimage.tar"
    }
    NameTypeDescription
    filename
    required
    StringFilename of image to be loaded.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "filename" : "myimage.tar"
    }
    response = requests.post(f"{base_url}/docker/image/load", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Remove an image​

DELETE /docker/image/remove

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

      {
    "image" : "mysql:latest"
    }
    NameTypeDescription
    image
    required
    StringName of image name:tag to be removed.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "image" : "mysql:latest"
    }
    response = requests.delete(f"{base_url}/docker/image/remove", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Docker Container Routes​

Create a container​

Similar to the docker run command.

POST /docker/container/create

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "image" : "mysql:latest",
    "name" : "foo-mysql",
    "volumes" : ["/vme-pool/sources/foo:/var/lib/mysql"],
    "ports" : "8888:3306",
    "environment" : ["MYSQL_ROOT_PASSWORD=secret_password"]
    }
    NameTypeDescription
    image
    required
    StringName of image.
    name
    required
    StringName of container to be created.
    detach
    optional
    BooleanBoolean for running container in the background, default is True.
    command
    optional
    StringCommand to run in the container, default is None.
    environment
    optional
    List or DictionaryEnvironment variables to set inside the container, as a dictionary or a list of strings in the format ["SOMEVARIABLE=xxx"], default is None.
    volumes
    optional
    List or DictionaryConfigure volumes mounted inside the container, default is [].
    ports
    optional
    StringPorts to bind inside the container in the format "a:b,c:d,...", for example 8080:80 means map port 8080 on the host to TCP port 80 in the container, default is None.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "image" : "mysql:latest",
    "name" : "foo-mysql",
    "volumes" : ["/vme-pool/sources/foo:/var/lib/mysql"],
    "ports" : "8888:3306",
    "environment" : ["MYSQL_ROOT_PASSWORD=secret_password"]
    }
    response = requests.post(f"{base_url}/docker/container/create", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

List all containers​

Similar to the docker ps -a command. This endpoint is also used to retrieve container connection information, including each container's environment variables (for example database credentials such as POSTGRES_PASSWORD, POSTGRES_USER, and POSTGRES_DB), port mappings, mount paths, and database type.

GET /docker/container/list

  • AUTHENTICATIONS: headers

  • RESPONSE SCHEMA: On success (status is 1), the result field is a list of container objects. Each object includes an env array of strings in KEY=value format (the same values shown in the vME UI under Connection Information for a container).

    NameTypeDescription
    nameStringContainer name.
    idStringFull Docker container ID.
    short_idStringShort Docker container ID.
    imageStringImage reference (for example postgres:17).
    statusStringContainer state from Docker Engine (for example running, paused).
    portObjectDocker port bindings (NetworkSettings.Ports).
    mountedListBind mounts as host_path:container_path strings.
    dbtypeStringDatabase type from the registered image (for example postgres).
    createdStringHuman-readable time since the container was created.
    envListEnvironment variables as KEY=value strings.

    Example excerpt from result for a single container:

    {
    "name": "posttest",
    "id": "205ce1989e609ecf7ffbaa616df26a9bf14cf34600e79d94cd0592b7a2bd2d91",
    "short_id": "205ce1989e60",
    "image": "postgres:17",
    "status": "running",
    "dbtype": "postgres",
    "created": "12 minutes ago",
    "port": {},
    "mounted": ["/testing/sources/postgressource:/var/lib/postgresql/data"],
    "env": [
    "POSTGRES_PASSWORD=Password123",
    "POSTGRES_USER=VME",
    "POSTGRES_DB=VME",
    "PATH=/usr/local/sbin:/usr/local/bin:...",
    "GOSU_VERSION=1.19",
    "LANG=en_US.utf8",
    "PG_MAJOR=17",
    "PG_VERSION=17.10-1.pgdg13+1",
    "PGDATA=/var/lib/postgresql/data"
    ]
    }
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    response = requests.get(f"{base_url}/docker/container/list", headers=headers)
    if response.status_code < 500:
    print(response.json())

Start a container​

Similar to the docker start command.

POST /docker/container/start

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    { 
    "name" : "foo-mysql"
    }
    NameTypeDescription
    name
    required
    StringName of container to be started.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "name" : "foo-mysql"
    }
    response = requests.post(f"{base_url}/docker/container/start", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Stop a container​

Similar to the docker stop command.

POST /docker/container/stop

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    { 
    "name" : "foo-mysql"
    }
    NameTypeDescription
    name
    required
    StringName of container to be stoped.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "name" : "foo-mysql"
    }
    response = requests.post(f"{base_url}/docker/container/stop", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Execute command inside a container​

Similar to the docker exec command.

POST /docker/container/exec

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "name" : "foo-mysql",
    "command" : "mysql --user=root --password=\"secret_password\" -e \"show databases;\""
    }
    NameTypeDescription
    name
    required
    StringName of container to be executed.
    command
    required
    String or ListCommands to be executed.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "name" : "foo-mysql",
    "command" : "mysql --user=root --password=\"secret_password\" -e \"show databases;\""
    }
    response = requests.post(f"{base_url}/docker/container/exec", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Remove a container​

Delete /docker/container/remove

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

      {
    "name" : "foo-mysql"
    }
    NameTypeDescription
    name
    required
    StringName of container to be removed.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "name" : "foo-mysql"
    }
    response = requests.delete(f"{base_url}/docker/container/remove", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Get a container log​

GET /docker/container/log

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    { 
    "name" : "foo-mysql"
    }
    NameTypeDescription
    name
    required
    StringName of container to get the log.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    params = {
    "name" : "foo-con"
    }
    response = requests.get(f"{base_url}/docker/container/log", params=params, headers=headers)
    if response.status_code < 500:
    print(response.json())

Get a container detail​

GET /docker/container/detail

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    { 
    "name" : "foo-mysql"
    }
    NameTypeDescription
    name
    required
    StringName of container to get the detail.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    params = {
    "name" : "foo-con"
    }
    response = requests.get(f"{base_url}/docker/container/detail", params=params, headers=headers)
    if response.status_code < 500:
    print(response.json())

Database Routes​

Connect to database​

POST /db/connect

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "dbtype" : "mysql",
    "host" : "localhost",
    "user" : "root",
    "password" : "secret_password",
    "port" : 8888
    }

    The body is the required credential information, the general formatting is {"dbtype": "<database_type>", "host": "<host>", "user": "<user>", "password": "<password>", "port": "<port>"}.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    credential = {
    "dbtype" : "mysql",
    "host" : "localhost",
    "user" : "root",
    "password" : "secret_password",
    "port" : 8888
    }
    response = requests.post(f"{base_url}/db/connect", json=credential, headers=headers)
    if response.status_code < 500:
    print(response.json())

Receive database​

Receive backup and restore dataset.

POST /db/receive

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "pool_name" : "vme-pool",
    "dataset_name" : "restored-sender-from-backup",
    "dbtype" : "mysql",
    "src" : "demo-pool-sources-sender@now-20230829183552.vmebk"
    }
    NameTypeDescription
    pool_name
    required
    StringName of pool.
    dataset_name
    required
    StringName of dataset to be restored.
    dbtype
    optional
    StringDatabase type, such as mysql, mariadb, mssql, mongodb etc., used to label a source, default is unknown.
    src
    required
    StringPath name of received backup file in .vmebk format, the default absolute path is /enov8/vme/data/<filename> when only the filename is provided.
  • RESPONSE: Returns HTTP 202 with a task_id. The restore runs as a background task; poll its progress with /tasks/<task_id> using the returned task_id.

    {
    "task_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "status": "queued"
    }
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "pool_name" : "vme-pool",
    "dataset_name" : "restored-sender-from-backup",
    "src" : "demo-pool-sources-sender@now-20230829183552.vmebk"
    }
    response = requests.post(f"{base_url}/db/receive", json=body, headers=headers)
    if response.status_code < 500:
    result = response.json()
    print(result)
    # Use the task_id to track the restore
    task_id = result.get('task_id')

Send database​

POST /db/send

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    • pool2pool
      {
      "type" : "pool2pool",
      "dataset" : "demo-pool/sources/sender@now",
      "receiver" : {
      "pool_name" : "vme-pool",
      "dataset_name" : "receiver-snap"
      }
      }
    • sys2sys, to an appliance registered under Appliance Registry Routes
      {
      "type" : "sys2sys",
      "dataset" : "demo-pool/sources/sender@now",
      "receiver" : {
      "peer_id" : "c8d4f2e3b0a54c1c93e10a780a88b373",
      "pool_name" : "vme-pool",
      "dataset_name" : "receiver-sys2sys-snap"
      }
      }
    • sys2sys, naming the receiver directly
      {
      "type" : "sys2sys",
      "dataset" : "demo-pool/sources/sender@now",
      "receiver" : {
      "address" : "https://9.87.654.321",
      "username" : "admin",
      "api-key" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "pool_name" : "vme-pool",
      "dataset_name" : "receiver-sys2sys-snap"
      }
      }
      sys2sys authentication

      For a sys2sys transfer, name a registered appliance with peer_id: the address, admin username, api-key and transfer address stored when it was registered are used, and any of them you also send takes precedence. Otherwise provide the receiver's address, username, and admin api-key. The sending appliance uses the api-key to establish SSH trust with the receiver automatically, registering its own managed public key on the target.

    NameTypeDescription
    type
    required
    StringSending type, pool2pool or sys2sys.
    dataset
    required
    StringName of dataset to be send.
    receiver
    required
    DictionaryInformation of receiver. For pool2pool contains pool_name and dataset_name. For sys2sys also contains address, username, and api-key (the receiver's admin API key, used to automatically establish SSH trust).
    receiver.peer_id
    optional
    Stringsys2sys only. A registered appliance, from List appliances, in place of address, username and api-key. A peer_id that names no registered appliance is refused with 404; a sys2sys send with neither is refused with 400.
    receiver.ssh_address
    optional
    Stringsys2sys only. Override the host used for the SSH/ZFS transfer when it differs from the receiver's API address (e.g. a separate data network). Defaults to the host in address.
    overwrite
    optional
    BooleanIf true, destroy an existing destination dataset on the receiver before sending. Default is false (a name clash fails the transfer). A destination that has clones on the receiver is never overwritten: the transfer fails naming the clones, and nothing on the receiver is changed.
    keep_snapshot
    optional
    BooleanIf true, keep the temporary snapshot created for the send operation. Default is false (snapshot is deleted after send completes).
    workflow_id
    optional
    StringThe onboarding workflow this send belongs to. When the transfer completes, the workflow's step is marked complete. See Linking calls to a workflow.
    wizard_step
    optional
    StringThe workflow step to mark complete. Default is send.
    role
    optional
    StringThe dataset role being sent, for example data.
  • RESPONSE: Returns HTTP 202 with a task_id that can be used to track the transfer status.

    {
    "task_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "status": "queued"
    }
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "type" : "pool2pool",
    "dataset" : "demo-pool/sources/sender@now",
    "receiver" : {
    "pool_name" : "vme-pool",
    "dataset_name" : "receiver-snap"
    }
    }
    response = requests.post(f"{base_url}/db/send", json=body, headers=headers)
    if response.status_code < 500:
    result = response.json()
    print(result)
    # Use the task_id to check status later
    task_id = result.get('task_id')

Get send status​

Check the status of a database send operation by task ID or get all transfer history for a specific dataset.

GET /db/send/status

  • AUTHENTICATIONS: headers

  • QUERY PARAMETERS: (mutually exclusive - provide either task_id or dataset, not both)

    NameTypeDescription
    task_id
    optional
    StringThe task ID returned from the /db/send endpoint. Returns status for a specific transfer.
    dataset
    optional
    StringDataset name (e.g., vme-pool/sources/foo). Returns all transfer history for this dataset.
  • RESPONSE SCHEMA:

    • Query by task_id: Returns information about a single transfer
      {
      "task_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "status": "completed",
      "progress_percentage": 100,
      "message": "Transfer completed successfully",
      "error": null,
      "dataset": "demo-pool/sources/sender@now",
      "dataset_type": "snapshot",
      "send_type": "pool2pool",
      "destination": {
      "pool_name": "vme-pool",
      "dataset_name": "receiver-snap",
      "address": null
      },
      "started_at": "2024-01-15T10:30:00Z",
      "completed_at": "2024-01-15T10:35:00Z",
      "initiated_by": "admin"
      }
    • Query by dataset: Returns all transfers for the dataset
      {
      "dataset": "vme-pool/sources/foo",
      "total_transfers": 2,
      "transfers": [
      {
      "task_id": "...",
      "status": "completed",
      "progress_percentage": 100,
      ...
      },
      {
      "task_id": "...",
      "status": "in_progress",
      "progress_percentage": 45,
      ...
      }
      ]
      }

    Status values:

    • queued - Transfer is waiting to start
    • in_progress - Transfer is currently running
    • completed - Transfer completed successfully
    • failed - Transfer failed (see error field for details)
    • cancelled - Transfer was stopped with Cancel a send
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests

    # Check status by task_id
    params = {
    "task_id" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    }
    response = requests.get(f"{base_url}/db/send/status", params=params, headers=headers)
    if response.status_code < 500:
    print(response.json())

    # Or check all transfers for a dataset
    params = {
    "dataset" : "vme-pool/sources/foo"
    }
    response = requests.get(f"{base_url}/db/send/status", params=params, headers=headers)
    if response.status_code < 500:
    print(response.json())

Cancel a send​

Cancel an in-flight database send (or receive) by its task ID. The running transfer task is signalled to stop and its transfer-history record is marked cancelled.

POST /db/send/cancel

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "task_id" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    }
    NameTypeDescription
    task_id
    required
    StringThe task_id returned by /db/send or /db/receive. May also be supplied as a query parameter (?task_id=).
  • RESPONSE: 200 on success. A transfer that has already finished (completed, failed, or already cancelled) is returned as-is with 200. An unknown task_id returns 404.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = { "task_id" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }
    response = requests.post(f"{base_url}/db/send/cancel", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Backup database​

Get all backup filenames.

GET /db/backup

  • AUTHENTICATIONS: headers

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    response = requests.get(f"{base_url}/db/backup", headers=headers)
    if response.status_code < 500:
    print(response.json())

Create backup file.

POST /db/backup

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    { 
    "dataset" : "vme-pool/sources/foo"
    }
    NameTypeDescription
    dataset
    required
    StringName of dataset to be backup.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "dataset" : "vme-pool/sources/foo"
    }
    response = requests.post(f"{base_url}/db/backup", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Create a network share​

Expose a source or clone dataset over the network (NFS or iSCSI, depending on how the dataset's network type is configured) so a database client can mount it. Use this to serve a restored/cloned database to an external host.

POST /db/network/create

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    • NFS
      {
      "full_name" : "vme-pool/sources/foo",
      "client_address" : ["10.0.0.21", "10.0.0.22"]
      }
    • iSCSI
      {
      "full_name" : "vme-pool/sources/foo"
      }
    NameTypeDescription
    full_name
    required
    StringFully-qualified dataset, in the form <pool>/sources/<source> or <pool>/clones/<clone>. The dataset must already have a supported network type configured.
    client_address
    required for NFS
    String or ListNFS only. One IPv4 address, or a list of IPv4 addresses, permitted to mount the share. Leave it out for iSCSI - see the note below.
    client_username
    optional
    StringClient username for the share. Default admin.
    client_password
    optional
    StringClient password for the share. Default admin.
    anonuid
    optional
    NumberNFS only. Anonymous UID for NFS mapping. Default 117.
    anongid
    optional
    NumberNFS only. Anonymous GID for NFS mapping. Default 122.
    workflow_id
    optional
    StringThe onboarding workflow this source belongs to. When the share is created (or was already configured), the workflow's step is marked complete; for the source step, the dataset's name and iSCSI target name are also recorded on the workflow. See Linking calls to a workflow.
    wizard_step
    optional
    StringThe workflow step to mark complete. Default is source.
    role
    optional
    StringThe dataset role of full_name, for example data. Send it so the workflow records the dataset under that role.
    iSCSI exports

    Every iSCSI export on a vME is served by one portal listening on 0.0.0.0:3260, on every interface of the appliance, so an iSCSI export takes no client_address, anonuid or anongid. Send only full_name. A client_address other than 0.0.0.0 is refused with 400: an iSCSI export listens on 0.0.0.0:3260, on every interface of this vME, so it cannot also be bound to <address>. Leave client_address out.

  • RESPONSE: { "status": 1, "message": "Network create success : <full_name>" }. A dataset that is already exported answers 200 with Network already configured : <full_name>.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "full_name" : "vme-pool/sources/foo",
    "client_address" : ["10.0.0.21", "10.0.0.22"]
    }
    response = requests.post(f"{base_url}/db/network/create", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Delete a network share​

Tear down an existing network share for a dataset.

DELETE /db/network/delete

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "full_name" : "vme-pool/sources/foo"
    }
    NameTypeDescription
    full_name
    required
    StringFully-qualified dataset (<pool>/sources/<source> or <pool>/clones/<clone>) whose share should be removed.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = { "full_name" : "vme-pool/sources/foo" }
    response = requests.delete(f"{base_url}/db/network/delete", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Poll a background task​

Several operations run asynchronously and return a task_id (for example /db/receive and /db/send). Use this endpoint to poll a task's state by its ID.

GET /tasks/<task_id>

  • AUTHENTICATIONS: none. The route does not check the username and api-key headers; sending them does no harm.

  • PATH PARAMETERS:

    NameTypeDescription
    task_id
    required
    StringThe task ID returned by an asynchronous endpoint.
  • RESPONSE: A state of PENDING, STARTED, PROGRESS, SUCCESS, or FAILURE. On SUCCESS the payload is returned in result; otherwise status describes the current state or error. While a long-running task reports how far it has got, state is PROGRESS and status carries its progress details. An unknown task ID also answers PENDING.

    { "state": "SUCCESS", "result": { "status": 1, "message": "done" } }
    tip

    This is the generic Celery task poll used after db and pool operations. It is not the same as the setup wizard's /setup/task/<id> poll. For database send transfers you can alternatively use /db/send/status, which returns richer transfer history.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    task_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    response = requests.get(f"{base_url}/tasks/" + task_id, headers=headers)
    if response.status_code < 500:
    print(response.json())

iSCSI CHAP Routes​

These routes control which host may log in to an iSCSI target, and with what credentials. They add or remove an access entry for one host's initiator on one target, with CHAP and mutual CHAP credentials.

Set CHAP credentials​

Allow one initiator to log in to a target, and set its CHAP and mutual CHAP credentials.

POST /iscsi/chap/set

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "target_iqn" : "iqn.2026-09.com.enov8:g-8214416316530251783",
    "initiator_iqn" : "iqn.1991-05.com.microsoft:sql-b",
    "chap_user" : "sqlb",
    "chap_password" : "TargetSecret01",
    "mutual_chap_password" : "InitiatorSecret1"
    }
    NameTypeDescription
    target_iqn
    required
    StringThe target to protect: the iqn_name of a source or clone, from What a source or clone listing carries.
    initiator_iqn
    required
    StringThe host's iSCSI initiator name. For a host with an agent, this is host_facts.initiator_iqn in List agents. It is also used as the mutual CHAP user name.
    mutual_chap_password
    required
    StringThe secret the target uses to prove itself to the host (mutual CHAP).
    chap_user
    optional
    StringThe CHAP user name the host logs in with. Default is admin.
    chap_password
    optional
    StringThe CHAP secret the host logs in with. If omitted, a random 16-character secret is generated and returned in result.chap_secret.
  • RESPONSE: The credentials to configure on the host.

    {
    "action" : "set_chap",
    "status" : 1,
    "message" : "Mutual CHAP configured for target_iqn: iqn.2026-09.com.enov8:g-8214416316530251783 , initiator_iqn: iqn.1991-05.com.microsoft:sql-b",
    "result" : {
    "status" : "success",
    "chap_user" : "sqlb",
    "chap_secret" : "TargetSecret01",
    "mutual_user" : "iqn.1991-05.com.microsoft:sql-b",
    "target_iqn" : "iqn.2026-09.com.enov8:g-8214416316530251783",
    "initiator_iqn" : "iqn.1991-05.com.microsoft:sql-b"
    }
    }

    The call is refused with 400 when either IQN is missing, or when the target does not exist or already has an entry for that initiator; remove the entry first to change its credentials.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "target_iqn" : "iqn.2026-09.com.enov8:g-8214416316530251783",
    "initiator_iqn" : "iqn.1991-05.com.microsoft:sql-b",
    "chap_user" : "sqlb",
    "mutual_chap_password" : "InitiatorSecret1"
    }
    response = requests.post(f"{base_url}/iscsi/chap/set", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Remove CHAP credentials​

Clear an initiator's CHAP and mutual CHAP credentials on a target and remove its access entry.

DELETE /iscsi/chap/remove

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "target_iqn" : "iqn.2026-09.com.enov8:g-8214416316530251783",
    "initiator_iqn" : "iqn.1991-05.com.microsoft:sql-b"
    }
    NameTypeDescription
    target_iqn
    required
    StringThe target the entry is on.
    initiator_iqn
    required
    StringThe host's iSCSI initiator name.
  • RESPONSE: { "action": "remove_chap", "status": 1, "message": "CHAP and Mutual CHAP removed for <initiator_iqn>" }. If the credentials could not be cleared (for example, there is no entry for that initiator), the call answers 400 with the reason in message.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "target_iqn" : "iqn.2026-09.com.enov8:g-8214416316530251783",
    "initiator_iqn" : "iqn.1991-05.com.microsoft:sql-b"
    }
    response = requests.delete(f"{base_url}/iscsi/chap/remove", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

User Management Routes​

Add User​

This API call adds a new user. Requires an administrator account. POST /admin/user/add takes the same body and behaves the same way.

POST /user/add

  • AUTHENTICATIONS: headers - Admin only

  • REQUEST BODY SCHEMA:

    {
    "username": "jane",
    "name": "Jane Doe",
    "mail": "jane@company.com",
    "group_id": 1,
    "auth_type": "ldap"
    }
    NameTypeDescription
    username
    required
    StringLogin name for the new user.
    name
    required
    StringFull/display name of the user.
    mail
    required
    StringEmail address for the user.
    group_id
    optional
    NumberID of the group to assign the user to: 0 is the administrator group, 1 is the standard user group. The group must already exist. If omitted, the user is added to the administrator group (0).
    auth_type
    optional
    Stringlocal or ldap (password is accepted as another name for local). If omitted, the user is a local user.
    password
    optional
    StringRequired when auth_type is local (or omitted). Initial password for the new local user. Not used for ldap.
    Set group_id for a non-administrator

    A user added without group_id becomes an administrator. To add a standard user, send "group_id": 1 (or the ID of another non-administrator group).

  • RESPONSE: { "action": "add_user", "status": 1, "message": "user add success", "user": "admin", "username": "jane" } (user is the administrator who made the call). An email address that is already in use, or a group_id that does not exist, answers 400.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "username": "jane",
    "name": "Jane Doe",
    "mail": "jane@company.com",
    "group_id": 1,
    "auth_type": "ldap"
    }
    response = requests.post(f"{base_url}/user/add", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Modify User​

This API call changes your own account: your full name, email address, password (local accounts only), or api-key. It does not change your group or sign-in type; an administrator does that with Modify a User (admin).

POST /user/modify

  • AUTHENTICATIONS: the account's own username and current_password in the body. The username and api-key headers are not checked on this route.

  • REQUEST BODY SCHEMA:

    {
    "username": "jane",
    "current_password": "OldPass123!",
    "password": {
    "current": "OldPass123!",
    "new": "NewPass456!",
    "confirm": "NewPass456!"
    }
    }
    NameTypeDescription
    username
    required
    StringLogin name of the account to change. The login name itself cannot be changed.
    current_password
    required
    StringThe account's current password, which authorises the change. For an LDAP account, this is the directory password.
    password
    optional
    ObjectTo change the password, pass an object with current, new, and confirm keys (all three required; new and confirm must match). Omit to leave the password unchanged. Refused for LDAP accounts.
    name
    optional
    StringUpdated full/display name.
    mail
    optional
    StringUpdated email address.
    rotate_api_key
    optional
    BooleanIf true, issue a new api-key for the account; the new key is returned in updated.api_key and the old key stops working.
    note

    /user/modify is authorised by the account's own password, so it only ever changes the account whose password you supply. A password change must be sent as a structured object (password: {current, new, confirm}). To change another user's group, sign-in type or password, use Modify a User (admin).

  • RESPONSE: 200 with an updated object listing the fields that changed (a changed password shows as ***updated***; a rotated key appears as api_key).

    { "status": 1, "message": "user modify success", "updated": { "password": "***updated***" } }
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "username": "jane",
    "current_password": "OldPass123!",
    "password": {
    "current": "OldPass123!",
    "new": "NewPass456!",
    "confirm": "NewPass456!"
    }
    }
    response = requests.post(f"{base_url}/user/modify", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Delete User​

This API call deletes your own account. It is authorised by the account's own password, so in practice a user can only delete themselves. Administrator accounts cannot be deleted through this route; an administrator removes users with Delete a User (admin).

DELETE /user/delete

  • AUTHENTICATIONS: the account's own username and password in the body. The username and api-key headers are not checked on this route.

  • REQUEST BODY SCHEMA:

    {
    "username": "ubuntu123",
    "password": "MyPassword123!",
    "confirm_text": "DELETE ubuntu123"
    }
    NameTypeDescription
    username
    required
    StringLogin name of the account to delete.
    password
    required
    StringThe password of the account being deleted. For an LDAP account, this is the directory password.
    confirm_text
    required
    StringConfirmation phrase; must be the literal string DELETE <username> (e.g. DELETE ubuntu123).
    note

    /user/delete needs the account's own password and a confirm_text value of exactly DELETE <username>. An account in the administrator group is refused with 400.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "username": "ubuntu123",
    "password": "MyPassword123!",
    "confirm_text": "DELETE ubuntu123"
    }
    response = requests.delete(f"{base_url}/user/delete", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Change Password​

Change your own password using your current password. Authenticated users can only change their own password.

POST /user/changepass

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "username": "jane",
    "password": "OldPass123!",
    "new_password": "NewPass456!"
    }
    NameTypeDescription
    username
    required
    StringYour login name. Must match the authenticated user; changing another user's password returns 403.
    password
    required
    StringYour current password.
    new_password
    required
    StringThe new password to set.
  • RESPONSE: { "status": 1, "message": "password changed successfully" }

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "username": "jane",
    "password": "OldPass123!",
    "new_password": "NewPass456!"
    }
    response = requests.post(f"{base_url}/user/changepass", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Get All Users​

This API call returns all users. Requires an administrator account. GET /user/list is another name for the same route and returns the same result.

GET /admin/user/list

  • AUTHENTICATIONS: headers - Admin only

  • RESPONSE: result is a list of users. Passwords and api-keys are not included.

    {
    "action" : "admin_user_list",
    "status" : 1,
    "message" : "success",
    "result" : [
    { "username": "jane", "name": "Jane Doe", "mail": "jane@company.com", "group_id": 1, "auth_type": "local" }
    ]
    }
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    response = requests.get(f"{base_url}/admin/user/list", headers=headers)
    if response.status_code < 500:
    print(response.json())

Add a User (admin)​

POST /admin/user/add adds a user. It takes the same body, has the same defaults and returns the same result as Add User, including that a user added without group_id becomes an administrator. Set group_id to 1 (or another non-administrator group) for a standard user.

POST /admin/user/add

  • AUTHENTICATIONS: headers - Admin only

  • REQUEST BODY SCHEMA: as for Add User.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "username": "sam",
    "name": "Sam Lee",
    "mail": "sam@company.com",
    "password": "MyPassword123!",
    "group_id": 1
    }
    response = requests.post(f"{base_url}/admin/user/add", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Modify a User (admin)​

An administrator changes any user's details, group, sign-in type or password, or rotates their api-key. No current password is needed.

PUT /admin/user/modify

  • AUTHENTICATIONS: headers - Admin only

  • REQUEST BODY SCHEMA:

    {
    "username" : "sam",
    "group_id" : 0
    }
    NameTypeDescription
    username
    required
    StringLogin name of the user to change.
    name
    optional
    StringUpdated full/display name.
    mail
    optional
    StringUpdated email address.
    group_id
    optional
    NumberMove the user to this group (0 administrator, 1 standard user). The group must exist. The last remaining administrator cannot be moved out of group 0.
    auth_type
    optional
    Stringlocal or ldap. Switching to local needs password. Switching to ldap clears the stored password.
    password
    optional
    StringNew password for a local user. Must be sent together with auth_type (send "auth_type": "local" to reset a local user's password).
    rotate_api_key
    optional
    BooleanIf true, issue a new api-key for the user; the new key is returned in updated.api_key and the old key stops working.

    At least one field besides username must be sent.

  • RESPONSE: 200 with an updated object listing what changed.

    { "action": "admin_modify_user", "status": 1, "message": "user modify success", "username": "sam", "updated": { "group_id": 0 } }
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "username" : "sam",
    "auth_type" : "local",
    "password" : "NewPass456!"
    }
    response = requests.put(f"{base_url}/admin/user/modify", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Delete a User (admin)​

An administrator deletes any user, including another administrator. The last remaining administrator cannot be deleted.

DELETE /admin/user/delete

  • AUTHENTICATIONS: headers - Admin only

  • REQUEST BODY SCHEMA:

    {
    "username" : "sam"
    }
    NameTypeDescription
    username
    required
    StringLogin name of the user to delete.
  • RESPONSE: { "action": "admin_delete_user", "status": 1, "message": "account sam was deleted", "username": "sam" }. An unknown username answers 400.

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {"username" : "sam"}
    response = requests.delete(f"{base_url}/admin/user/delete", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

LDAP Configuration Routes​

Admin only

All LDAP configuration endpoints (GET/POST/PUT/DELETE on /user/ldap) require an administrator account. A separate POST /user/ldap/test endpoint (also admin-only) validates a connection without saving it.

Get LDAP Configuration​

This API call retrieves the current LDAP configuration.

GET /user/ldap

  • AUTHENTICATIONS: headers

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    response = requests.get(f"{base_url}/user/ldap", headers=headers)
    if response.status_code < 500:
    print(response.json())

Delete LDAP Configuration​

This API call removes the LDAP configuration.

DELETE /user/ldap

  • AUTHENTICATIONS: headers

  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    response = requests.delete(f"{base_url}/user/ldap", headers=headers)
    if response.status_code < 500:
    print(response.json())

Create LDAP Configuration​

This API call creates the LDAP server.

POST /user/ldap

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "server": "sample.com",
    "port": 389,
    "bind_dn": "cn=readonly,dc=company,dc=com",
    "bind_password": "admin",
    "user_search_base": "ou=users,dc=company,dc=com",
    "user_filter": "(uid={adminuser})",
    "vme_attrs": {"username": "uid", "name": "cn", "mail": "mail"}
    }
    NameTypeDescription
    server
    required
    StringHostname or IP address of the LDAP server.
    port
    required
    NumberPort on which the LDAP server listens (e.g. 389 for LDAP, 636 for LDAPS).
    bind_dn
    required
    StringDistinguished Name (DN) used to bind to the LDAP server.
    bind_password
    required
    StringPassword for the bind_dn account.
    user_search_base
    required
    StringBase DN under which user entries are searched.
    user_filter
    required
    StringLDAP search filter for users; placeholder (e.g. {adminuser}) is replaced by the login name.
    vme_attrs
    required
    ObjectMapping of VME user fields (username, name, mail) to LDAP attribute names.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "server": "sample.com",
    "port": 389,
    "bind_dn": "cn=readonly,dc=company,dc=com",
    "bind_password": "admin",
    "user_search_base": "ou=users,dc=company,dc=com",
    "user_filter": "(uid={adminuser})",
    "vme_attrs": {"username": "uid", "name": "cn", "mail": "mail"}
    }
    response = requests.post(f"{base_url}/user/ldap", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Update LDAP Configuration​

This API call updates the existing LDAP configuration.

PUT /user/ldap

  • AUTHENTICATIONS: headers

  • REQUEST BODY SCHEMA:

    {
    "server": "sample.com",
    "port": 389,
    "bind_dn": "cn=readonly,dc=company,dc=com",
    "bind_password": "admin",
    "user_search_base": "ou=users,dc=company,dc=com",
    "user_filter": "(uid={adminuser})",
    "vme_attrs": {"username": "uid", "name": "cn", "mail": "mail"}
    }
    NameTypeDescription
    server
    required
    StringHostname or IP address of the LDAP server.
    port
    required
    NumberPort on which the LDAP server listens (e.g. 389 for LDAP, 636 for LDAPS).
    bind_dn
    required
    StringDistinguished Name (DN) used to bind to the LDAP server.
    bind_password
    required
    StringPassword for the bind_dn account.
    user_search_base
    required
    StringBase DN under which user entries are searched.
    user_filter
    required
    StringLDAP search filter for users; placeholder (e.g. {adminuser}) is replaced by the login name.
    vme_attrs
    required
    ObjectMapping of VME user fields (username, name, mail) to LDAP attribute names.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    body = {
    "server": "sample.com",
    "port": 389,
    "bind_dn": "cn=readonly,dc=company,dc=com",
    "bind_password": "admin",
    "user_search_base": "ou=users,dc=company,dc=com",
    "user_filter": "(uid={adminuser})",
    "vme_attrs": {"username": "uid", "name": "cn", "mail": "mail"}
    }
    response = requests.put(f"{base_url}/user/ldap", json=body, headers=headers)
    if response.status_code < 500:
    print(response.json())

Setup Routes​

Show first-time setup state​

Where the appliance's first-time setup wizard stands. The web interface reads this before anyone logs in, to decide whether to show the wizard and which step to resume at.

GET /setup/state

  • AUTHENTICATIONS: None.

  • RESPONSE:

    {
    "is_complete" : false,
    "current_step" : 2,
    "steps_done" : ["hostname", "network", "ntp"],
    "step_summary" : { "hostname": { "hostname": "vme-prod-01" } },
    "steps_config" : [ { "name": "hostname", "ui_index": 1, "required": false } ],
    "defaults" : { "ntp_server": "time.example.com" },
    "network_preconfigured" : true,
    "current" : {
    "hostname" : "vme-prod-01",
    "ipv4" : "10.20.30.41/24",
    "ipv4_method" : "static",
    "gateway" : "10.20.30.1",
    "dns" : ["10.20.30.2"]
    }
    }
    FieldDescription
    is_completetrue once setup has finished; the wizard is then closed.
    current_stepThe last step saved, 0 when setup has not started.
    steps_done, step_summaryThe steps recorded so far, and what each one set, leaving out anything secret.
    steps_configThe wizard's steps, the page each is on, and whether it is required.
    defaultsStarting values for wizard fields, such as the appliance's current NTP server.
    network_preconfiguredtrue when the hostname, NTP and network were already set and confirmed in the appliance's console setup, so the wizard skips the page that asks for them. false on an appliance that was not set up from its console, such as a cloud image; the wizard then asks.
    currentThe appliance's hostname and network as they are right now: ipv4 in CIDR form (or not configured), ipv4_method (dhcp, static or unknown), gateway (or none) and dns.
  • EXAMPLE:

    # Download the requests library from https://docs.python-requests.org/en/latest/
    import requests
    response = requests.get(f"{base_url}/setup/state")
    if response.status_code < 500:
    print(response.json())

Agent API​

vME can drive database onboarding and clone workflows on remote hosts through enrolled agents (for example, the Windows iSCSI Agent). The endpoints in this section, together with the Workflow API, are the admin-facing surface for that automation.

Driving a connector end to end

This section documents the endpoints. For how to sequence them into a working clone cycle - job states, polling, timeouts, and error handling - see Agent Jobs. For the phases and config fields of a specific connector, see that connector's API reference, such as the iSCSI API reference.

Admin only

Every endpoint in the Agent API and Workflow API sections requires an administrator account.

How it works:

  1. An administrator mints a one-time enrollment token (POST /agent-token) and gives it to whoever installs the agent.
  2. The agent enrols itself with that token, receives a long-lived agent key, and runs as a background service on the host.
  3. The agent polls vME for assigned jobs, runs each one locally, and reports progress and results back.
  4. An administrator dispatches jobs (POST /agent-job) and monitors them, or drives a higher-level onboarding workflow that sequences jobs across two appliances.

Enrollment, polling, and progress reporting are performed by the agent itself using its own credentials (x-agent-id / x-agent-key). Those routes are part of the agent's protocol and are not called by hand.

Create an enrollment token​

An enrollment token is a single-use, time-limited secret that lets a host register one agent.

POST /agent-token

  • AUTHENTICATIONS: headers - Admin only
  • REQUEST BODY SCHEMA:
    {
    "label" : "prod-sql-01",
    "environment" : "prod",
    "ttl_minutes" : 60
    }
    NameTypeDescription
    label
    optional
    StringFriendly name for the agent that will enrol with this token.
    environment
    optional
    StringEnvironment the host belongs to (e.g. prod, dev, test).
    ttl_minutes
    optional
    NumberHow long the token stays valid. Defaults to a short-lived window suitable for one install.
  • RESPONSE: The raw token is returned once - store it immediately, it cannot be retrieved again.
    { "result": { "token_id": "...", "token": "...", "environment": "prod", "expires_at": "..." } }
  • EXAMPLE:
    curl -X POST \
    -H "Content-Type: application/json" \
    -H "username: xxxxx" -H "api-key: xxxxx" \
    -d '{"label": "prod-sql-01", "environment": "prod", "ttl_minutes": 60}' \
    "$BASE_URL/agent-token"

List enrollment tokens​

GET /agent-token

  • AUTHENTICATIONS: headers - Admin only
  • QUERY PARAMETERS: active_only (optional, on by default): only tokens that are still valid, unused and not revoked are returned. Send active_only=0 (or false) to list every token, including used, expired and revoked ones.
  • RESPONSE: { "result": [ ... ] }, newest first. Each token carries token_id, label, environment, created_by, created_at, expires_at, consumed_at, consumed_by_agent_id and revoked. Raw token values are never returned again.

Revoke an enrollment token​

DELETE /agent-token

  • AUTHENTICATIONS: headers - Admin only
  • REQUEST BODY SCHEMA: { "token_id": "..." }

List agents​

GET /agent

  • AUTHENTICATIONS: headers - Admin only

  • QUERY PARAMETERS (all optional filters): environment, status, enabled, version.

  • RESPONSE: { "result": [ ... ] }. Each agent carries:

    FieldDescription
    agent_idThe agent's ID.
    host_name, display_nameThe host the agent runs on.
    labelThe agent's name in vME: the label an administrator set with Manage an agent, otherwise the label of the enrollment token it enrolled with, otherwise its host name.
    environmentThe environment it belongs to.
    tagsThe tags an administrator set, as a list. Empty when there are none.
    versionThe agent's version.
    statusThe effective status: disabled when the agent is disabled, inactive when its heartbeat has gone quiet, otherwise the status the agent reported (normally online).
    enabledWhether the agent may take jobs.
    enrolled_by, enrolled_at, last_seen_atWho enrolled it, when, and its last heartbeat.
    messageThe agent's last status message.
    attributesWhat the agent reports about itself and its host with each heartbeat, plus the label and tags set in vME. A heartbeat never overwrites label or tags.

    The Windows iSCSI Agent reports these attributes:

    FieldDescription
    capabilitiesWhat this agent can do, by name: folder_mount (sources and clones mount as folders under the host's vME folder), quiesce_resume (the Quiesce and Resume phases), forget_named_target and forget_named_portal (CloneUnmount removes only the targets and portals it is given), attach_as (CloneMount attaches the clone's one database under the name given in attach.attachAs). A name that is absent means the agent cannot do that.
    host_factsCollected when the agent starts and every 15 minutes after. Absent from the first heartbeat after a start.
    host_facts.oscaption, version, build and architecture of Windows.
    host_facts.cpu_logical, host_facts.memory_gbLogical processors and physical memory.
    host_facts.domain, host_facts.part_of_domainThe host's domain or workgroup.
    host_facts.ip_addressesIts IPv4 addresses, without loopback and link-local.
    host_facts.initiator_iqnThe host's iSCSI initiator name.
    host_facts.vme_rootThe host's vME folder, which sources and clones mount under. Empty when it is not set.
    host_facts.sql_instancesOne entry per SQL Server instance: name, service_name, status, database_count, product_name, product_version, major_version, product_level, update_level, edition, collation, is_clustered, is_hadr_enabled. The version fields are absent when the instance could not be reached, for example when it is stopped.
    host_facts.warnings, host_facts.collected_atWhat could not be read, and when the facts were collected (UTC).
  • EXAMPLE:

    curl -X GET -H "username: xxxxx" -H "api-key: xxxxx" \
    "$BASE_URL/agent?environment=prod&status=online"

Manage an agent​

Perform a lifecycle action on an enrolled agent.

PATCH /agent

  • AUTHENTICATIONS: headers - Admin only
  • REQUEST BODY SCHEMA:
    { "agent_id": "...", "action": "disable" }
    NameTypeDescription
    agent_id
    required
    StringThe agent to act on.
    action
    required
    StringOne of enable, disable, rotate, touch, or update. rotate issues a new agent key (returned once in the response); touch refreshes the agent's last-seen timestamp; update changes the agent's label, environment and tags.
    label
    optional
    Stringupdate only. The agent's name in vME, 1 to 128 characters.
    environment
    optional
    Stringupdate only. Up to 64 characters: letters, digits, spaces, dots, underscores and hyphens, starting with a letter or digit.
    tags
    optional
    Listupdate only. Replaces the agent's tags. Up to 20 tags, each up to 32 characters, with the same characters allowed as environment. Tags are compared without regard to case, and a repeat is dropped, keeping the first spelling. Blank entries are ignored. Send [] to remove all tags.
  • RESPONSE: enable, disable and touch answer { "action": "patch_agent", "status": 1, "message": "..." }. rotate adds result: { "agent_id", "agent_key" }; the new agent_key is shown only once.
  • UPDATE: Send only the fields you want to change; update needs at least one of label, environment and tags.
    {
    "agent_id" : "...",
    "action" : "update",
    "label" : "SQL-B (test)",
    "environment" : "test",
    "tags" : ["sql2022", "nightly-refresh"]
    }
    The response returns the updated agent in result, as List agents shows it:
    {
    "action" : "patch_agent",
    "status" : 1,
    "message" : "agent updated",
    "result" : {
    "agent_id" : "...",
    "label" : "SQL-B (test)",
    "environment" : "test",
    "tags" : ["sql2022", "nightly-refresh"],
    "status" : "online",
    "enabled" : true
    }
    }
    A value that breaks a rule is refused with 400 and a message naming the rule, for example label must not be empty, an agent can have at most 20 tags, or nothing to update: send label, environment or tags.

Delete an agent​

DELETE /agent

  • AUTHENTICATIONS: headers - Admin only
  • REQUEST BODY SCHEMA: { "agent_id": "..." }
Agent-side routes

POST /agent (enrolment) and PATCH /agent sent with x-agent-* headers (heartbeat) are used by the agent itself during install and while running. They are not part of the admin workflow and are not documented here.

Create an agent job​

A job is a single unit of work assigned to an agent - one workflow phase run against a target host.

POST /agent-job

  • AUTHENTICATIONS: headers - Admin only

  • REQUEST BODY SCHEMA:

    {
    "agent_id" : "...",
    "phase" : "SourcePrep",
    "config" : { "portal": "10.0.0.10", "manualIQN": "data,log" },
    "secrets" : { "chapPassword": "..." }
    }
    NameTypeDescription
    agent_id
    required
    StringThe agent that should run the job. The agent must be online.
    phase
    required
    StringThe workflow phase to run. The Windows iSCSI Agent runs DiscoverHost, ConnectTargets, SourcePrep, SourcePrepOnly, SourceMigrateOnly, CloneMount, CloneUnmount, Quiesce and Resume; see the iSCSI API reference. The appliance also accepts Preflight, NfsMount, DataCopy, SourceUnmount, OnboardMove, OnboardCopy, SnapshotBegin, SnapshotEnd and CloneAttach, which are reserved for the Oracle connector and are not available in this release. Any other value is refused with 400.
    config
    optional
    ObjectNon-secret configuration for the phase. Which fields are required depends on the phase - see the connector's API reference, for example iSCSI config fields. Mandatory fields are validated before the agent touches the host.
    secrets
    optional
    ObjectSecret values for the phase (e.g. CHAP credentials). Stored encrypted and never returned in job listings.
    workflow_id
    optional
    StringAssociate the job with an onboarding workflow (up to 64 characters). See Linking calls to a workflow.
    wizard_step
    optional
    StringThe workflow step this job carries out, one of the wizard steps (for example source_prep or clone_mount). When the job completes, that step is marked complete on the workflow. Up to 32 characters.
    role
    optional
    StringThe dataset role the job works on, for example data (see the template's roles under List workflow templates). Up to 16 characters.
    job_type
    optional
    StringA label of your own to group jobs by, up to 32 characters. vME itself uses coordination for the jobs of a coordinated snapshot or rollback, and cleanup for the jobs a workflow cleanup queues.
    ingest_path
    optional
    StringA label of your own for how the job's data is loaded, up to 255 characters. vME stores it on the job so you can filter by it; it does not change what the job does.

    workflow_id, wizard_step, role, job_type and ingest_path are stored on the job and also copied into config._meta, so the agent sees them. A value longer than its limit is refused with 400. The job is refused with 400 if the agent is disabled, and with 409 if the agent is not online (no heartbeat yet, or its heartbeat has gone stale).

  • RESPONSE: { "result": { ... } } - the created job (without secret values), in the queued state. A job carries job_id, agent_id, phase, state, requested_by, config, result, message, workflow_id, wizard_step, job_type, role, ingest_path, and the timestamps created_at, claimed_at, started_at, completed_at and updated_at.

List agent jobs​

GET /agent-job

  • AUTHENTICATIONS: headers - Admin only
  • QUERY PARAMETERS:
    NameDescription
    job_idReturn that one job instead of a list (404 if it does not exist). Add include_events=1 to include its progress events.
    agent_id, state, workflow_id, wizard_step, job_type, role, ingest_pathFilters; each matches the value exactly. A job's state is one of queued, claimed, running, completed, failed, cancelled or timed_out.
    limitHow many jobs to return, newest first. Default 100, at most 500.
  • RESPONSE: { "result": [ ... ] }, or { "result": { ... } } with job_id.

Cancel an agent job​

PATCH /agent-job

  • AUTHENTICATIONS: headers - Admin only
  • REQUEST BODY SCHEMA: { "job_id": "...", "action": "cancel" }
note

Agents claim and update their own jobs via GET /agent-job?next=1 and PATCH /agent-job using their agent credentials. As with the agent lifecycle routes, those calls are part of the agent protocol and are not made by an administrator.

Workflow API​

An onboarding workflow is a higher-level orchestrator that sequences agent jobs and ZFS operations to clone a database onto a SQL Server host (the "Onboard Application" wizard). The endpoints below let you list, inspect, and tear down workflows, and validate a target appliance. All require an administrator account.

Parts: what a workflow does​

A workflow runs some or all of five parts, in this order:

PartWhat it does
ingestCreates the source datasets on this appliance and loads the database into them.
snapshotTakes a point-in-time snapshot of the source.
sendReplicates the snapshot to a second appliance.
cloneCreates a writable clone from the snapshot.
mountAttaches the clone to a SQL Server host.

Each part consumes what the part before it produced. Omit parts and all five run, which spans two appliances and takes ten steps. Select a subset and the run is shorter - and whatever it needs but does not do, it starts from an object that already exists on the appliance, named in start_from.

Two combinations cannot run and are refused with 400:

  • mount without clone - there is nothing to mount, and no way to start from an existing clone.
  • clone or send without snapshot while ingest is on - with ingest on, the source does not exist yet, so there is no snapshot to start from either. Either add snapshot, or drop ingest and start from a snapshot that already exists.

Selecting clone + mount gives the most common short run: clone a snapshot you already have and mount it on a host. Repeat it for each host you want a copy on; nothing is ingested or transferred again.

Run a flow via the API

The Onboarding (API) folder in the vME Postman collection has two ordered, chained walk-throughs: Main flow runs all five parts across two appliances, and Clone and mount an existing snapshot runs the short clone + mount version. Run either top to bottom, or use Postman's Code button to copy each call into your own scripts.

List workflows​

GET /workflow

  • AUTHENTICATIONS: headers - Admin only
  • QUERY PARAMETERS (optional filters): state, created_by, workflow_type, source_mode, target_mode, transport_mode.
  • RESPONSE: { "result": [ ... ] }. Each workflow carries workflow_id, name, created_by, state, current_step, template_id, workflow_type, source_mode, target_mode, transport_mode, config, roles, host_agent_available, parts (a {part: boolean} map), live_steps (the steps this run actually has, in order), last_error, has_secrets, created_at and updated_at, so you do not have to derive the shape of a run yourself.

Show a workflow​

GET /workflow/<workflow_id>

  • AUTHENTICATIONS: headers - Admin only

  • RESPONSE: The workflow with every field List workflows returns, plus:

    FieldDescription
    stepsThe agent jobs linked to the workflow, grouped by wizard step (or by phase for a job with no step). Each carries job_id, agent_id, phase, state, role, message, result, created_at and updated_at.
    dataset_iqnsFor each dataset the workflow names, its iSCSI target name, as { "<dataset>": { "iqn_name", "guid", "iqn_scheme", "status" } }, or { "status", "message" } when its export failed. A dataset with no export yet is left out. A dataset on the appliance the run sends to is looked up there.
    secretsThe workflow's stored secrets (for example CHAP credentials), decrypted.

    Answers 404 if the workflow does not exist.

    This response contains secrets

    GET /workflow/<workflow_id> returns the workflow's secrets in clear text. Call it only from a trusted script, do not log or store the response, and use List workflows (which carries only has_secrets) when you just need a workflow's state.

Create a workflow​

POST /workflow

  • AUTHENTICATIONS: headers - Admin only
  • REQUEST BODY SCHEMA:
    NameTypeDescription
    name
    required
    StringA friendly name, 1-128 characters. You can resume a workflow by name later.
    template_id
    optional
    StringWhich flow to run - see List workflow templates. Leave it out and the appliance chooses from the run's choices; for SQL Server that is sql_server_iscsi_one_volume.
    parts
    optional
    Array or ObjectWhich parts this run does. An array of names (["clone","mount"]) or a map ({"ingest": false}). Omit it and all five parts run.
    start_from
    optional
    ObjectThe objects an existing-object run begins from: { "source": { ... } } and/or { "snapshot": { ... } }. Required in practice whenever a part that produces an input is off.
    config
    optional
    ObjectFree-form workflow configuration.
    workflow_type, source_mode, target_mode, transport_mode
    optional
    StringFlow metadata; sensible defaults are applied.
  • RESPONSE: The new workflow, in the draft state, opened at the first step its parts include. live_steps tells you which steps that is.
  • NOTES: Refused with 400 if the parts combination cannot run, with the reason and which tick to change. The web wizard drives most workflow creation; call this directly only when scripting your own automation.
  • ERRORS: Besides the combinations under Parts, these are refused with 400, with this message:
    MessageWhen
    unknown parts: <names>parts names something that is not one of the five parts.
    parts must be an object or a list of part namesparts is neither an array of names nor a {part: boolean} map.
    at least one part must be selectedEvery part is off.
    <template name> is switched off on this vMEThe template_id names a template that is switched off on this appliance.
  • EXAMPLE - clone a snapshot you already have and mount it on a host, with no ingest and no transfer:
    curl -X POST -H "username: xxxxx" -H "api-key: xxxxx" \
    -H "Content-Type: application/json" \
    -d '{
    "name": "Clone app01 onto SQL-B",
    "parts": ["clone", "mount"],
    "start_from": {
    "source": {
    "pool_name": "vme-pool",
    "source_name": "app01",
    "full_name": "vme-pool/sources/app01",
    "data_full_name": "vme-pool/sources/app01",
    "data": { "name": "app01", "full_name": "vme-pool/sources/app01", "role": "data" },
    "picked": true, "completed": true
    },
    "snapshot": {
    "snapshot_name": "nightly-20260910",
    "data": { "name": "nightly-20260910", "source_name": "app01",
    "source_full_name": "vme-pool/sources/app01",
    "full_snapshot_name": "vme-pool/sources/app01@nightly-20260910" },
    "picked": true, "completed": true
    }
    }
    }' "$BASE_URL/workflow"
    Each dataset a run uses is keyed by its role. A SQL Server run holds one dataset per database, role data. A run created before one volume per database holds a <base>_data and a <base>_log dataset, roles data and log, and only a snapshot name taken on both can be cloned. GET /workflow-templates lists the roles of each template.

Update a workflow​

PATCH /workflow/<workflow_id>

  • AUTHENTICATIONS: headers - Admin only
  • REQUEST BODY SCHEMA: any of name, state, current_step, config, or secrets. Use config_mode: "merge" (default) or "replace" to control how config is applied. Send config.parts to change which parts the run does; the same validation, and the same 400 messages, apply as on create.
  • NOTES:
    • current_step is validated against the steps this run has. A step whose part is off is refused with 400, naming the parts that are off and the steps the run does have.
    • A cancelled workflow cannot be changed: the call is refused with 409.
    • state: "completed" is refused with 409 while a clone_mount job is unfinished, or after a step has failed.
    • Pass "force": true to override any of the three refusals above.
    • Changing template_id to a template that is switched off on this appliance is refused with 400: <template name> is switched off on this vME. force does not override this. A workflow already on that template is not affected by this check and stays readable.
    • As a run moves forward its state follows the step being worked, and skips any state whose part is off - a clone-and-mount run never reports itself as source_provisioning.
  • WORKFLOW STATES: draft, source_selection, source_provisioning, source_prep, snapshot_selection, snapshot, sending, cloning, clone_mounting, completed, failed, cancelled.
  • WIZARD STEPS: basics, pick_source, source, prod_agent, discover_prod, source_prep, pick_snapshot, snapshot, send, clone, nonprod_agent, discover_nonprod, clone_mount, complete. pick_source and pick_snapshot replace the steps that would have produced those objects, and appear only when their part is off.

Linking calls to a workflow​

A workflow does not run the work itself. Its steps are marked complete, and its state moves forward, when the calls that do the work name the workflow. When you script a workflow, send these fields on each of those calls:

FieldDescription
workflow_idThe workflow the call belongs to, from Create a workflow.
wizard_stepThe wizard step the call completes. Each call below has a default, so send it only to record the work under a different step. A value that is not a wizard step is ignored.
roleThe dataset role the call works on, from the template's roles (for example data). The source, snapshot, send and clone steps run once per role, and are complete only when every role the template declares has been recorded.
CallDefault stepRecorded when
POST /db/network/createsourceThe share is created, or was already configured. The dataset's full name and iSCSI target name are recorded under its role.
POST /snapshot/createsnapshotThe snapshot is taken, including when it runs as a coordinated task. The snapshot name is recorded under its role.
POST /db/sendsendThe transfer completes.
POST /clone/createcloneThe clone is queued for creation.
POST /agent-jobnone (send wizard_step)The agent reports the job completed.
POST /source/createnot recordedThis call only checks that the workflow has not been cancelled.
  • A step is only ever marked complete, never cleared, and the workflow's state only moves forward.
  • A step this workflow does not run (its part is off) is ignored.
  • A workflow_id that does not exist on the appliance you are calling is ignored. That is the normal case for a job queued on the appliance a run sends to.
  • If an agent job that carries a workflow_id fails, or the agent reports it cancelled, the workflow's state becomes failed and last_error holds the step and the job's message (a workflow that is already completed or cancelled is left as it is). CloneUnmount jobs, and the jobs of a coordinated snapshot or a workflow cleanup, do not change the workflow's state.
  • Any of these calls naming a cancelled workflow is refused with 409 (see below).

List cleanup candidates​

GET /workflow/<workflow_id>/cleanup-candidates

  • AUTHENTICATIONS: headers - Admin only
  • RESPONSE: The objects a cleanup delete would destroy, across both appliances - each with a key, the side (local/remote), its kind (source / snapshot / clone), and a display label. Use this to preview, or to build a subset for a selective delete.

Cancel or delete a workflow​

DELETE /workflow/<workflow_id>

  • AUTHENTICATIONS: headers - Admin only
  • QUERY PARAMETERS:
    NameDescription
    (none)Soft-cancel: the workflow is marked cancelled; its record and the ZFS objects it created are kept.
    purge=1Hard-delete the workflow record. Required before cleanup or force below have any effect.
    purge=1&cleanup=1Also destroy the source / snapshot / clone objects the workflow created on both appliances.
    purge=1&force=1Delete the record even if some cleanup steps failed.
  • REQUEST BODY SCHEMA (optional): { "objects": ["key1", "key2"] } - restrict a cleanup delete to a subset of the cleanup candidates.
  • RESPONSE: { "message": "...", "cleanup": { ... } }. If a cleanup step fails and force is not set, the call returns 409 Conflict with a report of what could not be removed.
  • EXAMPLE - hard-delete and clean up everything on both sides:
    curl -X DELETE -H "username: xxxxx" -H "api-key: xxxxx" \
    "$BASE_URL/workflow/WORKFLOW_ID?purge=1&cleanup=1"
A cancelled workflow refuses further work

Once a workflow is cancelled, any call that carries its workflow_id and would do work on its behalf is refused with 409 - POST /agent-job, /source/create, /db/network/create, /snapshot/create, /clone/create and /db/send, as well as PATCH /workflow/<id>. Create a new workflow to run those steps again.

The CloneUnmount phase is the exception: a cancelled run can have left a clone mounted on a host, so tearing it down stays available. A workflow_id that does not resolve on the appliance you are calling is not affected either - that is the normal case for a job queued against a target appliance.

Validate a target appliance (remote preview)​

Confirm that a target appliance is reachable, list its pools, and establish SSH trust with it (so the clone transfer can run).

POST /workflow/<workflow_id>/remote-preview

  • AUTHENTICATIONS: headers - Admin only
  • REQUEST BODY SCHEMA - either address the target directly:
    {
    "address" : "https://10.0.0.20",
    "admin_username" : "admin",
    "admin_api_key" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    }
    or name one you have registered, and its address, username and api-key are read from the appliance registry:
    { "peer_id" : "c8d4f2e3b0a54c1c93e10a780a88b373" }
    Anything you send explicitly wins over the stored value.
  • RESPONSE: { "result": { "host": "...", "pools": [ ... ], "peer_trust": { "added": true, "fingerprint": "..." } } }.

List workflow templates​

GET /workflow-templates

  • AUTHENTICATIONS: headers - Admin only
  • QUERY PARAMETERS: enabled_only (optional) - return only enabled templates.
  • RESPONSE: { "result": [ ... ], "default_template_id": "..." }. Each descriptor carries:
    FieldDescription
    idThe value to send as template_id on create.
    name, descriptionThe template's display name and a one-line description.
    enabledWhether a new workflow can use this template. A workflow on a template that is switched off is refused on create with 400.
    rolesThe dataset roles a source is made of, as a list of objects: key (the value to send as role), label (its display name), suffix (appended to the dataset name for that role; empty when the dataset is the whole database) and required. SQL Server over iSCSI keeps a database's data and log files on one volume, so it has one role; the earlier SQL Server template kept them on two volumes and has two (_data and _log). Oracle over dNFS has one.
    transportHow the datasets are exported - iscsi or dnfs.
    agent_action_setWhich host-agent behaviour drives the run. Opaque to the appliance.
    host_agent_availableWhether the host agent implements that action set. When this is false, the vME-side steps still run, but the steps that need an agent on the database host have nothing behind them.
  • TEMPLATES: sql_server_iscsi_one_volume (one volume per database over iSCSI, host agent available; the default), sql_server_iscsi (separate data and log volumes, kept for runs created before one volume per database; enabled: false, so not offered for new runs), and oracle_dnfs (a single dataset over dNFS; switched off in this release: enabled: false and host_agent_available: false, so POST /workflow refuses it with 400).

Appliance Registry Routes​

Register the other vME appliances this one sends snapshots to. Credentials are stored once here - the admin api-key encrypted at rest and never returned - so a send names an appliance instead of carrying its api-key on every call. All routes require an administrator account.

The registry is managed from the vME appliances tab on Agent Manager in the web interface; these routes are the same surface for scripting.

List appliances​

GET /peer

  • AUTHENTICATIONS: headers - Admin only
  • RESPONSE: { "result": [ ... ] }. Each row carries:
    FieldDescription
    peer_idThe id to use in the routes below.
    nameThe name you gave it. Unique across the registry.
    base_urlIts address.
    ssh_addressSet only when SSH data transfer uses a different address than base_url.
    admin_usernameThe account used to call it.
    has_api_keyWhether an api-key is stored. The key itself is never returned.
    trusttrusted once this appliance's SSH key has been installed on it, otherwise none.
    statusonline, unreachable, or unknown - what the last test found.
    remote_version, remote_hostnameRead from the appliance during a test.
    poolsIts pool names, from the last test.
    last_seen_at, last_errorWhen it last answered, and why it last did not.

Add an appliance​

POST /peer

  • AUTHENTICATIONS: headers - Admin only
  • REQUEST BODY SCHEMA:
    NameTypeDescription
    name
    required
    StringA friendly name, up to 128 characters. Must be unique.
    base_url
    required
    StringIts address. A bare host is accepted and https:// is assumed; a trailing /api is trimmed.
    admin_api_key
    required
    StringAn administrator api-key on that appliance. Stored encrypted.
    admin_username
    optional
    StringThe account that api-key belongs to. Defaults to admin.
    ssh_address
    optional
    StringSet only if SSH data transfer uses a different address than base_url.
    verify_tls
    optional
    BooleanDefaults to false, since appliances ship a self-signed certificate. Set true only if you have pointed the appliance at a CA bundle.
  • RESPONSE: 201 with the new row. A duplicate name is refused with 409.
  • EXAMPLE:
    curl -X POST -H "username: xxxxx" -H "api-key: xxxxx" \
    -H "Content-Type: application/json" \
    -d '{ "name": "vme-nonprod-01", "base_url": "https://10.0.0.20",
    "admin_username": "admin", "admin_api_key": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }' \
    "$BASE_URL/peer"

Show an appliance​

GET /peer/<peer_id>

  • AUTHENTICATIONS: headers - Admin only
  • RESPONSE: The row, in the same shape as List appliances.

Amend an appliance​

PATCH /peer/<peer_id>

  • AUTHENTICATIONS: headers - Admin only
  • REQUEST BODY SCHEMA: any of name, base_url, admin_username, admin_api_key, ssh_address, verify_tls. Send admin_api_key only when you want to replace the stored key. A name already used by another row is refused with 409.

Remove an appliance​

DELETE /peer/<peer_id>

  • AUTHENTICATIONS: headers - Admin only
  • NOTES: Removes the registry entry. Workflows that send to that appliance will need it registered again. Nothing on the appliance itself is changed - including the SSH key this one installed on it.

Establish trust​

Installs this appliance's managed SSH public key on the other one, so snapshot transfers to it need no SSH setup by hand.

POST /peer/<peer_id>/trust

  • AUTHENTICATIONS: headers - Admin only
  • RESPONSE: { "result": { "installed": true, "fingerprint": "SHA256:..." } }, and the row's trust becomes trusted.
  • NOTES: Uses the stored credentials to call the other appliance's own trust route. 502 if it cannot be reached or refuses the key.

Test an appliance​

POST /peer/<peer_id>/test

  • AUTHENTICATIONS: headers - Admin only
  • RESPONSE: { "result": { "reachable": true, "pools": [ ... ], "agents_count": 2, "remote_version": "1.10.0", "warnings": [ ... ] } }.
  • NOTES: Records status, last_seen_at, last_error and the pool names on the row either way. 502 with the same result shape when the appliance cannot be reached.

List an appliance's pools​

GET /peer/<peer_id>/pools

  • AUTHENTICATIONS: headers - Admin only
  • RESPONSE: The other appliance's pools, fetched with the stored credentials. Use this to choose a destination pool for a send without holding that appliance's api-key yourself.

List an appliance's agents​

GET /peer/<peer_id>/agents

  • AUTHENTICATIONS: headers - Admin only
  • RESPONSE: The other appliance's enrolled agents, fetched with the stored credentials.

Call an appliance through this one​

Carries one API call to a registered appliance, using the credentials stored for it, and hands back its answer. This is how the web interface works with a second appliance during a run without the browser holding that appliance's api-key.

POST /peer/<peer_id>/proxy

  • AUTHENTICATIONS: headers - Admin only
  • REQUEST BODY SCHEMA:
    {
    "method" : "GET",
    "path" : "/agent-job?workflow_id=WORKFLOW_ID"
    }
    NameTypeDescription
    path
    required
    StringThe API path on the other appliance, including any query string. Only paths starting with /agent (which covers /agent-job and /agent-token), /pool/, /source/, /snapshot/, /clone/, /db/network/ or /db/send are carried.
    method
    optional
    StringOne of GET, POST, PUT, PATCH or DELETE. Defaults to GET.
    body
    optional
    Object or ArrayThe JSON body to send with the call.
  • RESPONSE: The other appliance's JSON body and HTTP status, unchanged. The call waits up to 60 seconds for it to answer.
  • ERRORS:
    StatusMessageWhen
    400method not allowedmethod is not one of the five above.
    403path not carried by the appliance proxypath does not start with one of the prefixes above.
    404appliance not foundNo registered appliance has that peer_id.
    502<name> unreachable: ...The appliance could not be reached.
  • EXAMPLE - list a run's agent jobs on the appliance it sent to:
    curl -X POST -H "username: xxxxx" -H "api-key: xxxxx" \
    -H "Content-Type: application/json" \
    -d '{ "method": "GET", "path": "/agent-job?workflow_id=WORKFLOW_ID" }' \
    "$BASE_URL/peer/PEER_ID/proxy"

Peer identity and incoming trust​

These two routes are how appliances pair with each other. In normal use they are called for you by Establish trust rather than by hand.

GET /peer/identity

  • AUTHENTICATIONS: headers - Admin only
  • RESPONSE: { "pubkey": "ssh-ed25519 ...", "fingerprint": "SHA256:..." } - this appliance's own managed public key, generated on first call. Use it to trust this appliance from somewhere else.

POST /peer/trust

  • AUTHENTICATIONS: headers - Admin only
  • REQUEST BODY SCHEMA: { "pubkey": "ssh-ed25519 ...", "label": "vme:10.0.0.10" }
  • RESPONSE: { "added": true, "fingerprint": "SHA256:..." }. Answered by an appliance when another one pairs with it.

API Examples (Postman Collection)​

Click here to download the Postman collection with example requests for various vME API requests.

note

If you're unsure how to import the collection into Postman, please refer to the Postman documentation for a step-by-step guide on importing collection.